1

I'm just getting started with Rust and have hit upon an issue where I'm getting a bit lost in types, pointers, borrowing and ownership.

I have a vector of Person structs, each of which has a name property. I'd like to collect all the names into another vector, and then check if that list contains a given string.

This is my code:

struct Person {
    name: String,
}

fn main() {
    let people = vec![Person {
        name: String::from("jack"),
    }];
    
    let names: Vec<_> = people.iter().map(|p| p.name).collect();

    let some_name = String::from("bob");
    if names.iter().any(|n| n == some_name) {
    }
}

This fails to compile:

error[E0277]: can't compare `&String` with `String`
  --> src/main.rs:13:31
   |
13 |     if names.iter().any(|n| n == some_name) {
   |                               ^^ no implementation for `&String == String`
   |
   = help: the trait `PartialEq<String>` is not implemented for `&String`

I think names should have the type Vec<String>, so I'm a bit lost as to where the &String comes from. Is this a case of me holding it wrong? I'm most familiar with JavaScript so I'm trying to apply those patterns, and may be not doing it in the most "rusty" way!

Jack Franklin
  • 3,765
  • 6
  • 26
  • 34
  • 1
    If you're using VSCode, I'd recommend turning on rust-analyzer's "inlay hints" feature. It will add some virtual text that adds types to some contexts. For example, here, it would change `|p| p.name` to look like `|p: &String| p.name`, which hopefully helps show where there references are coming from (don't worry, it doesn't change the actual text, it just changes the visual representation) – cameron1024 Feb 13 '22 at 18:18
  • Does this answer your question? [What is the difference between iter and into\_iter?](https://stackoverflow.com/questions/34733811/what-is-the-difference-between-iter-and-into-iter) – Chayim Friedman Feb 13 '22 at 23:11

3 Answers3

2

The type of p is not Person but &Person, i.e. a reference to a value. In JavaScript there are no structs. The closest alternative is an object and JS objects are always stored in the heap and are accessed via references (although that's less explicit). In Rust you really need to think more about the memory layout of your data. That can complicate parts of your code, but it also gives you power and allows for better performance.

Here is a correct version of your code:

struct Person {
    name: String,
}

fn main() {
    let people = vec![Person {
        name: String::from("jack"),
    }];
    
    let names: Vec<_> = people.iter().map(|p| &p.name).cloned().collect();
    

    let some_name = String::from("bob");
    if names.iter().any(|n| n == &some_name) {
    }
}

Notice that in n == &some_name we are now comparing a &String with a &String.

Also note I added .cloned(), so that you avoid another error, related to moving the name strings.

A slightly more optimized version would be to only collect the references to the names, like this:

    let names: Vec<_> = people.iter().map(|p| &p.name).collect();   
    let some_name = String::from("bob");
    if names.iter().any(|n| *n == &some_name) {
    }

Note that in this version n is of type &&String, so we dereference it once via *n. That way we are again comparing a &String with a &String. Rust also allows you to write something like this: |&n| n == &some_name, which is practically the same.

at54321
  • 8,726
  • 26
  • 46
2

Several things going on there:

  1. &String comes from the fact that iter() iterates over the collection without consuming it, and therefore can only give you references into items of the collection.

  2. You can address the inability to compare &String to String by borrowing the latter, i.e. changing n == some_name to n == &some_name.

  3. After you do that, the compiler will tell you that p.name attempts to move name out of the reference p. To get rid of it, you can change p.name to &p.name, thus creating a Vec<&String>.

  4. ...after which n will become &&String (!), so you'll need n == &&some_name. The example then compiles.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • About pt 1: in his code `p` is `&Person`, but he gets (moves) the names, so he actually collects a `Vec`, not `Vec<&String>`, right? – at54321 Feb 13 '22 at 18:23
  • @at54321 Yes. More precisely, the OP _attempts_ to collect `Vec`, but that can't work (without cloning `p.name`, which isn't warranted here), except that error is hidden by the other error reported in the question. The answer tries to disentangle that piece by piece. – user4815162342 Feb 13 '22 at 18:55
1

Because Rust does not impl PartialEq<&String> for String by default, here are impl PartialEq<&String> for &String and impl PartialEq<String> for String.

Here the type of n is &String, the type of some_name is String, so you need to balance the references on the left and right side of ==.

Both of the following fixes work. I prefer the first one, because the second one has a dereference which implies extra overhead for me. I'm not sure if Rust explicitly defines that there won't be overhead here, and even if there is, LLVM can usually optimize it.

if names.iter().any(|n| n == &some_name) // &String == &String
// or
if names.iter().any(|n| *n == some_name) // String == String

Then you will encounter another error:

error[E0507]: cannot move out of `p.name` which is behind a shared reference
  --> src/main.rs:10:47
   |
10 |     let names: Vec<_> = people.iter().map(|p| p.name).collect();
   |                                               ^^^^^^ move occurs because `p.name` has type `String`, which does not implement the `Copy` trait

For more information about this error, try `rustc --explain E0507`.

|p| p.name tries to move the name out of p, which is not necessary for the later checks, a &str is enough here. Returns the &str of a String via String::as_str: |p| p.name.as_str()

Because of this fix, the type of n below becomes &&str, you can use names.into_iter() to get an iterator for ownership of names.

Fixed code:

struct Person {
    name: String,
}

fn main() {
    let people = vec![Person {
        name: String::from("jack"),
    }];
    
    let names: Vec<_> = people.iter().map(|p| p.name.as_str()).collect();

    let some_name = String::from("bob");
    if names.into_iter().any(|n| n == some_name) {
    }
}
Sprite
  • 3,222
  • 1
  • 12
  • 29
  • 1
    Your version compiles, but it's probably worth noting that your `names` implementation *moves* (*consumes*) `people` (due to the `into_iter()` call), so it cannot be used anymore. – at54321 Feb 13 '22 at 18:37
  • @at54321 Thanks, I realized I was lost in fixing error messages. I have edited the answer. – Sprite Feb 13 '22 at 18:39