TL; DR;
文字列の2項演算 + が String
+ &str
になっている理由:
- 効率よく計算するため一方はムーブ(
String
)になっている - 足されるほうは借用で事足りるため借用(
&str
)になっている
2つの文字列型
Rust には2つの文字列型があります。
-
&str
- 文字列スライスとも呼ばれるプリミティブな文字列型。 -
String
- 標準ライブラリの提供する文字列型。文字列操作などに使う。
ざっくり理解すると借用が &str
で、所有権があるのが String
と覚えておくとよさそうです。
&str
と String
は相互に変換できます。
// 文字列リテラルは &str
let s = "hello";
// String の初期化
let s = String::from("hello");
// &str -> String
let s = "hello".to_string();
// String -> &str
let s = String::from("hello").as_str();
2項演算 + でコンパイルが通るのは?
それではクイズ。文字列は2項演算 +
で結合できますが、次の4つのパターンのうち、コンパイルが通るのはどれでしょうか?
// &str + &str
"hello" + "world";
// String + &str
String::from("hello") + "world";
// &str + String
"hello" + String::from("world");
// String + String
String::from("hello") + String::from("world");
正解は、2番目の String
+ &str
です。他の3つはコンパイルエラーになります。
なぜ String
+ &str
なのか
なぜ文字列の結合 + は String
+ &str
だけが可能なのでしょうか。答えは2項演算 + の定義にあります。
2項演算 + はトレイト std::ops::Add が実装されている型で使えます。
2項演算は関数呼び出しのシンタックスシュガーと考えられるので、左の項と右の項は関数に与える引数と同じように、Rust の所有権の考え方が適用されます。
そこで、所有権の観点から考えてみましょう。
String
は所有者、&str
は借用なので、上で考えた4パターンは足し算に与える引数を
- (借用) + (借用)
- (ムーブ) + (借用)
- (借用) + (ムーブ)
- (ムーブ) + (ムーブ)
というパターンで考えていることになります。
ところで、引数を借用にすべきかムーブにすべきかは、「必要がない限りはできるだけ借用にし、必要なときにだけムーブにする」という原則で考えるとよいでしょう。
というのも、引数を借用にしておけば、関数呼び出しが完了したら変数を元のスコープに返すことができますが、引数で所有権をムーブすると、基本的には所有権が返ってこないからです。関数に変数を渡しただけで以後その変数が使えなくなるのは不便ですよね。
この原則で考えると、文字列の足し算では少なくとも一方は借用で事足りそうです。たとえば足されるほうの文字列は、どんな文字列なのかを知りたいだけなので所有権までは欲しくないからです。
では、どちらか一方はムーブである必然性があるのでしょうか?
API ドキュメントにその理由が説明されています。 文字列の + は String
の中で定義されています。
これによると、+ 演算で毎回新たな String
をコピーしてメモリを確保すると効率が悪いため、渡された String
を再利用して結合文字列を作るそうです。
それを行うために所有権のムーブが必要だというわけですね。
よって、+ 演算の一方がムーブで他方が借用である理由がわかりました。
最後に 「&str
+ String
」 ではなく「String
+ &str
」になっている理由ですが、これはおそらく String
が文字列操作の標準ライブラリだから String
側に + 演算を実装したからだと思われます。