There's quite a number of things happening here. Let's break it down.
First, for clarity, the actual types in your examples:
- In the first loop,
s
is a &String
. (a reference is taken)
- In the second loop,
s
is a String
. (and it tries to move the string, causing a compile failure)
- In the third loop,
s
is a &&String
. (yes, really)
The "Item" type of an iterator
This is what you are seeing in your first example:
for s in a.iter() { ... }
The type of s
in this case is whatever the Item
type is of the IntoIterator
impl of your iterable. However...
- By "your iterable", I am referring to the type
a.iter()
returns, which is... this... thing.
- That... thing doesn't actually have an explicit
IntoIterator
impl because it is actually itself an Iterator
. (all Iterator
s implicitly implement IntoIterator
with a matching item type)
- By cross referrencing the type parameters used in the signature of
HashSet::iter()
with that Item
type, one can eventually deduce the Item
type.
Easy as pie, right?
...thankfully, you don't usually ever have to look any of the above stuff up. Most iter
methods are kind enough to document the item type. Here, the documentation for HashSet::iter tells us that the item type is &'a T
(i.e. &String
).
Pattern matching
Almost all of the rest of your questions are really about pattern matching. So let's get rid of the iterator, and focus on a let
statement, which shares exactly the same syntax (except that we can now easily put various types of things on the right hand side to experiment!)
Here's various values and their types:
let one = 1i32;
let x = one; // type is i32
let x = &one; // type is &i32
let x = &&one; // type is &&i32
let x = (one,one); // type is (i32, i32)
let x = (&one, &one); // type is (&i32, &i32)
let x = &(one, one); // type is &(i32, i32)
However, x
doesn't have to be an identifier here; it is more generally a pattern, which lets you pick apart the value on the right hand side as you create variables:
let one = 1i32;
let x = one; // x is i32
let &x = &one; // x is i32
let &&x = &&one; // x is i32
let (x, y) = (one, one); // x and y are i32
let (&x, &y) = (&one, &one); // x and y are i32
let &(x, y) = &(one, one); // x and y are i32
The theme here is that syntax used in a pattern effectively "cancels out" parts of the type. So adding a & to a pattern actually removes a & from the type you create!
If we use a non-copy type like String
, then we cannot create more String
variables from it without explicitly cloning it, so many of the above examples will stop compiling (like your second loop). We need to create &String
type variables instead:
let hello = "hello".to_string();
let x = &hello;
let &x = &&hello;
let (x, y) = (&hello, &hello);
ref
patterns
Short version: The pattern ref x
is like the opposite of &x
. It automatically borrows the thing it matches against.
Long version: Why does this exist? Consider this.
let hello = "hello".to_string();
let world = "world".to_string();
let tuple = (hello, world);
let hard_to_match = &tuple; // <-- type &(String, String)
How can we match against hard_to_match
to create two &String
variables? None of these work:
let (x, y) = hard_to_match; // type error; (_,_) vs &(_,_)
let &(x, y) = hard_to_match; // error: move out of borrowed content
The problem is that we cannot reach the innards of the type without dereferencing it, but dereferencing it causes a move. To get around this, we need to reborrow its innards with ref
patterns:
let &(ref x, ref y) = hard_to_match; // x is &String, y is &String
The compiler's suggestion was silly
Your use case is not at all the kind of place where you need to use ref
. You see, when the compiler suggested using ref
, it was actually suggesting that you add a ref
to the &s
pattern: (to prevent the move)
for &(ref s) in a.iter() { ... }
However, this would be identical to:
for s in a.iter() { ... }
So... yeah.
Deref Coercion
Even though your third example creates a &&String
, it generally isn't a huge deal because rust freely coerces between a variety of borrowed forms of types:
let hello = "hello".to_string();
let x: &String = &&hello; // Coerces &&String into &String
let x: &str = &hello; // Coerces &String into &str
This isn't to say &&String
and &String
are identical; but most code that compiles with a &String
will probably also compile with a &&String
.