RustのStringと&strの違い:使い分けと変換方法まとめ
はじめに
Rustには文字列を扱う型が主に2つあります。String と &str です。どちらも文字列ですが、所有権とメモリの扱いが異なります。最初はどちらを使えばいいか迷いやすいポイントなので、違いと使い分けをまとめます。
String と &str の基本的な違い
String |
&str |
|
|---|---|---|
| 所有権 | あり(ヒープ上) | なし(借用) |
| 変更 | 可能 | 不可能 |
| サイズ | 実行時に変わる | 固定 |
| 主な用途 | 文字列を所有・変更したい | 文字列を参照するだけ |
let s1: String = String::from("hello"); // ヒープ上に確保 let s2: &str = "hello"; // プログラムバイナリに埋め込まれた文字列への参照
String
String はヒープ上にデータを持つ、所有権のある文字列型です。内容を変更できます。
let mut s = String::from("hello"); s.push(' '); // 文字を追記 s.push_str("world"); // 文字列を追記 println!("{}", s); // hello world
String の生成方法
let s1 = String::new(); // 空文字列 let s2 = String::from("hello"); // 文字列リテラルから let s3 = "hello".to_string(); // &str から変換 let s4 = format!("{} {}", "hello", "world"); // フォーマット
&str(文字列スライス)
&str は文字列データへの参照です。所有権を持たず、変更もできません。
let s: &str = "hello"; // 文字列リテラル('static ライフタイム) let owned = String::from("hello world"); let slice: &str = &owned[0..5]; // String の一部への参照 → "hello"
文字列リテラル "hello" の型は &'static str(プログラムが終わるまで有効)です。
変換方法
&str → String
let s: &str = "hello"; let owned1 = s.to_string(); let owned2 = String::from(s); let owned3 = s.to_owned();
String → &str
let s = String::from("hello"); let slice1: &str = &s; // 自動的に変換(Deref) let slice2: &str = s.as_str(); // 明示的に変換 let slice3: &str = &s[..]; // スライス構文
関数の引数にはどちらを使うか
文字列を受け取るだけで変更しない関数には &str を使うのが慣用的です。&str にしておくと String も &str も両方渡せます。
// &str を引数にすると String も &str も渡せる(推奨) fn greet(name: &str) { println!("こんにちは、{}さん", name); } let name_owned = String::from("田中"); let name_borrowed: &str = "鈴木"; greet(&name_owned); // String を渡す(& で借用) greet(name_borrowed); // &str をそのまま渡す
// String を引数にすると &str を渡せない fn greet(name: String) { println!("こんにちは、{}さん", name); } greet(String::from("田中")); // OK greet("鈴木"); // コンパイルエラー
文字列を所有したい(関数内でヒープに保存するなど)場合は String を引数にします。
よく使うメソッド
共通(String・&str 両方で使える)
let s = "hello world"; s.len() // バイト数: 11 s.is_empty() // 空かどうか: false s.contains("world") // 含むか: true s.starts_with("hello") // 先頭: true s.ends_with("world") // 末尾: true s.to_uppercase() // "HELLO WORLD" s.to_lowercase() // "hello world" s.trim() // 前後の空白・改行を除去 s.replace("world", "Rust") // "hello Rust" s.split(" ") // イテレータで分割
String 専用(変更系)
let mut s = String::from("hello"); s.push('!'); // 文字を追加 s.push_str(" world"); // 文字列を追加 s.clear(); // 内容を消去 s.insert(0, 'H'); // 位置を指定して文字を挿入 s.truncate(3); // 長さを3バイトに切り詰める
+ 演算子と format! での結合
let s1 = String::from("hello"); let s2 = String::from(" world"); // + 演算子(s1 の所有権が移動する) let s3 = s1 + &s2; // s1 はここで使えなくなる // format! マクロ(所有権の移動なし・複数結合に便利) let s4 = String::from("hello"); let s5 = String::from(" world"); let s6 = format!("{}{}", s4, s5); // s4・s5 はまだ使える
複数の文字列を結合する場合は format! が読みやすくおすすめです。
まとめ
| 状況 | 使う型 |
|---|---|
| 文字列リテラルを参照するだけ | &str |
| 文字列を変更・追加したい | String |
| 関数で文字列を受け取るだけ | &str(String も渡せる) |
| 関数で文字列を所有したい | String |
&str → String に変換 |
.to_string() / String::from() |
String → &str に変換 |
&s / .as_str() |
基本方針は「変更しないなら &str、変更・所有するなら String」です。迷ったときはまず &str で書いてみて、コンパイラに怒られたら String に変える、という進め方が実践的です。
Rustのトレイトについては「Rustのトレイト入門:定義・実装・ジェネリクスとの組み合わせ方」を参照してください。
RustのVec初期化については「RustのVec初期化まとめ:vec!マクロからVec::newまで」を参照してください。
RustのHashMapの使い方は「RustのHashMap入門:基本操作から頻出パターンまとめ」を参照してください。
Rustのファイル読み書きは「Rustのファイル読み書き入門:fs・BufReader・BufWriterの使い方」を参照してください。