エラーのシナリオ
JavaScript、Python、Javaの経験があるなら、2つの文字列を+演算子で結合することはごく自然に感じられるでしょう。しかし、Rustはメモリの扱いに対して非常に慎重です。おなじみの構文を使って2つの文字列リテラル(&str)を連結しようとすると、おそらく最初に壁にぶつかるはずです。
コンパイラの逆鱗に触れる、よくあるスニペットを見てみましょう:
fn main() {
let hello = "Hello, ";
let world = "world!";
// これはコンパイルに失敗します
let greeting = hello + world;
println!("{}", greeting);
}
cargo buildを実行すると、次のようなエラーメッセージが表示されます:
error[E0369]: binary operation `+` cannot be applied to type `&str`
--> src/main.rs:6:26
|
6 | let greeting = hello + world;
| ----- ^ ----- &str
| |
| &str
なぜRustはこれを拒否するのか
修正方法を理解するには、&strが実際には何であるかを知る必要があります。Rustにおいて、&strは文字列スライスです。これは本質的に、64ビットシステムでは16バイトの構造体であり、UTF-8バイト列へのポインタと長さで構成されています。それはメモリ(多くの場合、読み取り専用メモリ)への「ビュー」に過ぎないため、サイズを拡張することはできません。
Rustの+演算子は、Addトレイトによって提供されています。標準ライブラリはこのトレイトをString + &strに対してのみ実装しており、&str + &strに対しては実装していません。その実装がどのように機能するか、簡略化したものを見てみましょう:
impl Add<&str> for String {
type Output = String;
fn add(mut self, other: &str) -> String {
self.push_str(other);
self
}
}
ここで重要な詳細が明らかになります。左辺は所有権を持つStringである必要があります。この操作は実際にはそのStringを消費し、バッファに新しいテキストを追加して、それを返します。スライス(&str)は自身のバッファを所有していないため、追加の文字を格納する場所がありません。
実践的な解決策
1. 最初の文字列を所有権のあるStringに変換する
最も手っ取り早い修正方法は、最初のスライスをヒープに割り当てられたStringに変換することです。これは.to_string()やString::from()を使って行えます。これにより、拡張可能なバッファが作成されます。
fn main() {
let hello = "Hello, ";
let world = "world!";
// 最初の部分をStringに変換します。2番目の部分はスライスのままで構いません
let greeting = hello.to_string() + world;
println!("{}", greeting);
}
ここでのhelloはリテラルであったことに注意してください。もしhelloがすでに所有権のあるString変数であった場合、+を使用するとその変数はムーブされるため、コードの後半で元の変数を使用することはできなくなります。
2. format! マクロを使用する(最もクリーンな方法)
複数の変数を組み合わせたり、テキストと数値を混ぜたりする必要がある場合、format!が最適です。+演算子を連結するよりもはるかに読みやすく、型変換も自動的に処理してくれます。
fn main() {
let user = "Alice";
let id = 42;
// format! は &str と整数を簡単に処理できます
let message = format!("User: {}, ID: {}", user, id);
println!("{}", message);
}
format!は実行時のテンプレート解析のため、手動での連結よりもわずかに遅くなりますが、ほとんどのアプリケーションにおいてその差は無視できる程度です。
3. パフォーマンス向上のために push_str を使用する
すでに可変(mutable)なStringがある場合は、+で新しい文字列を作成しないでください。push_strを使用しましょう。これは既存のバッファをその場で変更するため、ループ内では大幅に効率的です。
fn main() {
let mut buffer = String::with_capacity(50);
buffer.push_str("First ");
buffer.push_str("Second");
println!("{}", buffer);
}
4. 文字列のコレクションを結合する
文字列のリストやベクタを扱う場合は、joinメソッドが慣用的な選択です。項目間の区切り文字を自動的に処理してくれます。
fn main() {
let words = vec!["Rust", "is", "fast"];
let sentence = words.join(" ");
assert_eq!(sentence, "Rust is fast");
}
修正を確認する方法
これらの方法のいずれかを適用した後、cargo checkで動作を確認してください。これはフルビルドよりもはるかに高速です。コンパイラがエラーを出さなければ、型は正しく整合しています。方法1を使用した場合、もしそれが所有権のあるStringであれば、+演算子がその所有権を奪うため、同じ変数を再度使用しようとしていないか再確認してください。
パフォーマンスに関する考慮事項
- **事前割り当て:** 1MBの文字列を作成することがわかっている場合は、`String::with_capacity(1_000_000)`を使用してください。これにより、バッファがいっぱいになるたびにデータを再コピーする必要がなくなります。
- **メモリスライス:** 可能な限り、データは`&str`として保持してください。テキストを実際に変更する必要がある場合や、所有権を必要とする構造体に格納する場合にのみ、`String`に変換してください。
- **ループを避ける:** 大きなループの中で`s = s + "more"`を決して使用しないでください。これはイテレーションごとに新しいメモリ割り当てを行うため、計算量がO(n²)になってしまいます。

