Understanding HashSet in Rust
Tags: rust, tutorial, beginners, programming
Published: 2024-04-22 18:30:00

In Rust, a `HashSet` is a collection that stores unique elements in no particular order, using a hash-based implementation for efficient membership tests. Essentially, a `HashSet` is similar to a `HashMap` where the value is `()` (unit type), making it a HashMap with keys and no values.

## Creating a HashSet

To use a `HashSet`, you need to include the `HashSet` module from the standard collections library.

```rust
use std::collections::HashSet;
```

You can create a `HashSet` with the `new` method and make it mutable for modifications.

```rust
let mut my_set = HashSet::new();
```

By default, the type is inferred based on the inserted values, but you can also specify types explicitly.

```rust
let mut my_set: HashSet<String> = HashSet::new();
```

We can also create a hashset with default values using the `from()` method when creating it.

```rust
use std::collections::HashSet;

fn main() {
    // Create HashSet with default values using from() method
    let numbers1 = HashSet::from([2, 7, 8, 10]);

    println!("numbers = {:?}", numbers);
}
```

You can also turn a `Vec` into a `HashSet` using the `into_iter()` method(consumes ownership), followed by the `collect()` method.

```rust
use std::collections::HashSet;

fn main() {
    // Turning a vector into a HashSet
    let my_vec: Vec<i32> = vec![1, 2, 3, 4, 5];
    let my_set: HashSet<_> = my_vec.into_iter().collect();

    println!("{:?}", my_set);
}
```
Note that the type of the HashSet is inferred using the `_` placeholder, but you can explicitly specify the type if needed.

## Adding Elements to a HashSet

Elements can be added using the `insert` method, ensuring uniqueness within the set.

```rust
my_set.insert("apple");
my_set.insert("banana");
my_set.insert("orange");
```

> **Note:** Adding a new value to the hashset is only possible when you declare variable as `mut`.

## Printing a HashSet

To print the contents of a `HashSet`, use the `println!` macro with `{:?}` to display the elements.

```rust
println!("{:?}", my_set);
```

## Difference Between HashSet and BTreeSet

In Rust, there are different types of sets available. `HashSet` uses a hash table for fast access, suitable for unordered data. On the other hand, `BTreeSet` uses a binary search tree, maintaining elements in sorted order.

## Accessing, Removing, and Updating Elements

You can check if an element is present using `contains`, remove elements with `remove`, and update elements with `replace`.

```rust
if my_set.contains("apple") {
    println!("The set contains 'apple'");
}

my_set.remove("banana");

my_set.replace("apple", "grape");
```

## Iterating over Values of a HashSet

There are several ways to iterate over the values of a `HashSet`, depending on whether you need references, mutable references, or want to consume and take ownership of the elements.

### Iterating by References (Immutable Borrow)

Using an immutable reference to iterate over the values of a `HashSet`.

```rust
use std::collections::HashSet;

fn main() {
    let my_set: HashSet<i32> = vec![1, 2, 3, 4, 5].into_iter().collect();

    for value in my_set.iter() {
        println!("Reference to: {}", value);
    }
}
```

Here, the `iter` method is used to obtain an iterator over references to the elements.

Using method chaining with closures,

```rust
use std::collections::HashSet;

fn main() {
    let my_set: HashSet<i32> = vec![1, 2, 3, 4, 5].into_iter().collect();

    // Using .iter().for_each() to print each element
    my_set.iter().for_each(|&value| {
        println!("Reference to: {}", value);
    });
}
```
Here, he closure takes an immutable reference to each element in the HashSet and prints it. These methods doesn't consume the elements, and ownership remains with the `HashSet`.

### Iterating by Mutable References (Mutable Borrow)

```rust
use std::collections::HashSet;

fn main() {
    let mut my_set: HashSet<i32> = vec![1, 2, 3, 4, 5].into_iter().collect();

    for value in my_set.iter_mut() {
        *value *= 2;  // Doubling each element in-place
    }

    println!("{:?}", my_set);
}
```

Here, the `iter_mut` method is used to obtain a mutable iterator, allowing modifications of the elements in the set.

Using method chaining with closures,

```rust
use std::collections::HashSet;

fn main() {
    let mut my_set: HashSet<i32> = vec![1, 2, 3, 4, 5].into_iter().collect();

    my_set.iter_mut().for_each(|value| {
        *value *= 2;
    });

    println!("{:?}", my_set);
}
```
Here, the closure takes a mutable reference to each element in the HashSet and doubles its value in-place. This allows modification of the elements in-place while still keeping ownership with the `HashSet`.

### Consuming Iteration (Ownership Transfer)

Using the `into_iter` method to consume the elements of the `HashSet`.

```rust
for value in my_set.into_iter() {
    println!("Owned value: {}", value);
}
```

Here, `into_iter` is used to obtain an iterator that consumes the `HashSet`. The type of `value` is `i32`, indicating ownership transfer.

Using a `for` loop directly on the `HashSet` is concise but it will also consume and transfer ownership.

```rust
for value in my_set {
    println!("Owned value: {}", value);
}
```

## Conclusion

`HashSet` in Rust provides a powerful way to store unique elements efficiently. Understanding how to create, modify, and iterate over a `HashSet` is essential for working with collections in Rust.