Reputation: 25
In this toy example I'd like to map the items from a HashMap<String, String>
with a helper function. There are two versions defined, one that takes arguments of the form &String
and another with &str
. Only the &String
one compiles. I had thought that String
always dereferences to &str
but that doesn't seem to be the case here. What's the difference between a &String
and a &str
?
use std::collections::HashMap;
// &String works
fn process_item_1(key_value: (&String, &String)) -> String {
let mut result = key_value.0.to_string();
result.push_str(", ");
result.push_str(key_value.1);
result
}
// &str doesn't work (type mismatch in fn arguments)
fn process_item_2(key_value: (&str, &str)) -> String {
let mut result = key_value.0.to_string();
result.push_str(", ");
result.push_str(key_value.1);
result
}
fn main() {
let mut map: HashMap<String, String> = HashMap::new();
map.insert("a".to_string(), "b".to_string());
for s in map.iter().map(process_item_2) { // <-- compile error on this line
println!("{}", s);
}
}
Here's the error for reference:
error[E0631]: type mismatch in function arguments
--> src/main.rs:23:29
|
12 | fn process_item_2(key_value: (&str, &str)) -> String {
| ---------------------------------------------------- found signature of `for<'r, 's> fn((&'r str, &'s str)) -> _`
...
23 | for s in map.iter().map(process_item_2) {
| ^^^^^^^^^^^^^^ expected signature of `fn((&String, &String)) -> _`
Thanks for your help with a beginner Rust question!
Upvotes: 1
Views: 215
Reputation: 70830
It goes even stranger than that:
map.iter().map(|s| process_item_2(s)) // Does not work
map.iter().map(|(s1, s2)| process_item_2((s1, s2))) // Works
The point is that Rust never performs any expensive coercion. Converting &String
to &str
is cheap: you just take the data pointer and length. But converting (&String, &String)
to (&str, &str)
is no so cheap anymore: you have to take the data+length of the first string, then of the second string, then concatnate them together (and also, if it was done for tuple, what about (((&String, &String, &String), &String), (&String, &String))
? And it was probably done then for arrays too, so what about &[&String; 10_000]
?) That's why the first closure fails. The second closure, however, destruct the tuple and rebuild it. That means that instead of coercing a tuple, we coerce &String
twice, and build a tuple from the results. That's fine.
The version without the closure is even more expensive: since you're passing a function directly to map()
, and map produces &String
, someone needs to convert this to &str
! In order to do that, the compiler would have to introduce a shim - a small function that does that works: it takes (&String, &String)
and calls process_item_2()
with the (&String, &String)
coerced to (&str, &str)
. This is a hidden cost, so Rust (almost) never creates shims. This is why it wouldn't work even for &String
and not just for (&String, &String)
. And why |v| f(v)
is not always the same as f
- the first one performs coercions, while the second doesn't.
Upvotes: 3