The answer is in two parts.
The first part is that +
involves using an Add
trait implementation. It is implemented only for:
impl<'a> Add<&'a str> for String
Therefore, string concatenation only works when:
- the Left Hand Side is a
String
- the Right Hand Side is coercible to a
&str
Note: unlike many other languages, addition will consume the Left Hand Side argument.
The second part, therefore, is what kind of arguments can be used when a &str
is expected?
Obviously, a &str
can be used as is:
let hello = "Hello ".to_string();
let hello_world = hello + "world!";
Otherwise, a reference to a type implementing Deref<&str>
will work, and it turns out that String
does so &String
works:
let hello = "Hello ".to_string();
let world = "world!".to_string();
let hello_world = hello + &world;
And what of other implementations? They all have issues.
impl<'a> Add<String> for &'a str
requires prepending, which is not as efficient as appending
impl Add<String> for String
needlessly consume two arguments when one is sufficient
impl<'a, 'b> Add<&'a str> for &'b str
hides an unconditional memory allocation
In the end, the asymmetric choice is explained by Rust philosophy of being explicit as much as possible.
Or to be more explicit, we can explain the choice by checking the algorithmic complexity of the operation. Assuming that the left-hand side has size M and the right-hand side has size N, then:
impl<'a> Add<&'a str> for String
is O(N) (amortized)
impl<'a> Add<String> for &'a str
is O(M+N)
impl<'a, 'b> Add<&'a str> for &'b str
is O(M+N)
impl Add<String> for String
is O(N) (amortized)... but requires allocating/cloning the right-hand side for nothing.