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 strrequires prepending, which is not as efficient as appending
- impl Add<String> for Stringneedlessly consume two arguments when one is sufficient
- impl<'a, 'b> Add<&'a str> for &'b strhides 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 Stringis O(N) (amortized)
- impl<'a> Add<String> for &'a stris O(M+N)
- impl<'a, 'b> Add<&'a str> for &'b stris O(M+N)
- impl Add<String> for Stringis O(N) (amortized)... but requires allocating/cloning the right-hand side for nothing.