RustのStringと&strを徹底解説!初心者向けの使い分けと設計判断ガイド
生徒
「Rustを勉強し始めたのですが、文字列にStringと&strの2種類があって混乱しています。どうして2つもあるんですか?」
先生
「それはRustの最大の特徴である『所有権』と『メモリ管理』が深く関わっているからです。Stringは中身を書き換えられる可変の文字列、&strは中身を参照するだけの読み取り専用の文字列、とまずは捉えてみてください。」
生徒
「なるほど。でも、関数の引数にどっちを使えばいいか迷うんです。何か明確な基準はありますか?」
先生
「もちろんです!実行効率やメモリの安全性、そしてプログラムの柔軟性を考えるための『設計判断のコツ』があります。今日はその違いを完璧にマスターしましょう!」
1. Rustの文字列における二大巨頭を知る
Rustというプログラミング言語を扱う上で、避けて通れないのがString型と&str(文字列スライス)の使い分けです。他の言語、例えばPythonやJavaScript、Javaなどでは「String」という一つの型で済むことが多いですが、Rustはシステムプログラミング言語であるため、メモリをどのように管理するかを開発者が明示的に選ぶ必要があります。
まず、Stringは「所有権」を持つ文字列です。これはヒープ領域というメモリ空間にデータを確保し、実行中に文字列の長さを変えたり、中身を編集したりすることができます。一方で&strは「参照」であり、どこか別の場所に保存されている文字列データの一部分を覗き見ているような状態を指します。この違いを理解することが、Rust特有の借用チェッカーやライフタイムという概念を攻略する第一歩になります。
初心者がまず覚えるべきは、Stringは「自分でデータを持っている重厚な箱」、&strは「データがどこにあるかを指し示す軽量な付箋」というイメージです。このイメージを持つだけで、コンパイルエラーの半分以上は解決の糸口が見えてくるはずです。
2. String型の特徴と所有権の仕組み
String型は、標準ライブラリによって提供される「伸長可能なUTF-8エンコードされた文字列」です。この型は所有権を持っているため、その変数がスコープを抜けると、保持しているメモリは自動的に解放されます。これはRustのメモリ安全性を支える重要な仕組みです。
具体的な特徴としては、以下のような点が挙げられます。第一に、String::from("hello")やto_string()メソッドを使って生成されること。第二に、push_strメソッドなどで後から文字を追加できること。そして第三に、データの「所有者」が明確であるため、関数に渡すと所有権が移動(ムーブ)することがある点です。
fn main() {
// String型の生成(ヒープメモリに確保される)
let mut my_string = String::from("Hello");
// 文字列の追加(可変であるため可能)
my_string.push_str(", Rust!");
println!("現在の文字列: {}", my_string);
// 所有権の移動
let new_owner = my_string;
// println!("{}", my_string); // ここでmy_stringを使うとエラーになる!
}
現在の文字列: Hello, Rust!
上記のコードでは、mutキーワードを付けることで、文字列を自由に変更できています。しかし、所有権がnew_ownerに移った後は、元のmy_stringは使えなくなります。これがRustの厳格なルールであり、二重解放などのメモリバグを防ぐ秘訣です。
3. 文字列スライスと参照のメリット
次に&strについて詳しく見ていきましょう。これは正式には「文字列スライス」と呼ばれます。スライスとは、連続したメモリ領域への参照を指します。例えば、バイナリ内に埋め込まれた「文字列リテラル」は、この&str型(正確には&'static str)として扱われます。
&strの最大のメリットは「コピーが非常に軽量であること」と「所有権を奪わないこと」です。単にデータを読み取るだけであれば、重たいStringを丸ごとコピーして渡す必要はありません。データの場所と長さというわずかな情報だけをやり取りするため、パフォーマンスが非常に高いのです。
fn main() {
// 文字列リテラルは &str 型
let literal: &str = "Rustの世界へようこそ";
// Stringの一部をスライスとして参照する
let s = String::from("Rust Programming");
let slice: &str = &s[0..4]; // "Rust"の部分だけを参照
println!("スライスの内容: {}", slice);
println!("元のStringもまだ使える: {}", s);
}
スライスの内容: Rust
元のStringもまだ使える: Rust Programming
この例のように、Stringの一部を切り出して&strとして扱うことができます。このとき、sliceはsのデータを借りている(借用している)状態なので、sが破棄されるまでしか存在できません。これがRustの「生存期間(ライフタイム)」という考え方の基本です。
4. 関数の引数にはどちらを使うべきか
Rustの設計において最も頻出する判断基準が「関数の引数の型」です。結論から言うと、**読み取り専用で使いたい場合は、原則として &str を引数にするべきです。**
なぜなら、&strを引数に取れば、呼び出し側は&strをそのまま渡せるだけでなく、String型の変数に&(アンパサンド)を付けるだけで渡すことができるからです(これを「Derefによる型強制」と呼びます)。逆に引数をStringにしてしまうと、呼び出し側がわざわざStringを作成して所有権を渡さなければならず、不便で非効率なコードになりがちです。
// 良い例: 引数を &str にする
fn greet(name: &str) {
println!("こんにちは、{}さん!", name);
}
fn main() {
let s_literal = "田中"; // &str型
let s_managed = String::from("佐藤"); // String型
greet(s_literal); // そのまま渡せる
greet(&s_managed); // &を付けるだけで String も &str として渡せる
}
こんにちは、田中さん!
こんにちは、佐藤さん!
このように、引数を&strに設計することで、関数の汎用性が劇的に向上します。一方で、関数内でどうしても文字列の所有権を保持し続けたい場合や、受け取った文字列を加工してそのまま返したい場合は、引数にStringを採用することもあります。しかし、初心者のうちは「参照で済むなら&str」というルールを徹底するだけで、Rustらしい綺麗なコードが書けるようになります。
5. 構造体での使い分けとライフタイムの壁
関数の引数だけでなく、構造体のフィールド(メンバ変数)に文字列を持たせたいときも判断が必要です。ここが初心者にとって最初の高いハードルとなります。構造体に文字列を持たせる場合、**基本的には String を使うのが推奨されます。**
もし構造体のフィールドに&strを持たせようとすると、Rustのコンパイラから「ライフタイム('aなど)」を明示するように求められます。これは、構造体が参照しているデータが、構造体自身よりも長く生存していることを保証しなければならないからです。ライフタイムの記述は複雑になりやすいため、初心者が「データそのものを構造体に持たせたい」ときは、潔くStringを使いましょう。
struct User {
username: String, // 所有権を持つ。初心者はこれが安全!
email: String,
}
fn main() {
let user1 = User {
username: String::from("rust_user"),
email: String::from("example@rust-lang.jp"),
};
println!("ユーザー名: {}", user1.username);
}
ユーザー名: rust_user
もしこれを&strにしてしまうと、その文字列がどこに存在しているのか(関数のローカル変数なのか、グローバルな定数なのか)を厳密に管理しなければならず、設計が非常に難しくなります。メモリ効率を極限まで追求するライブラリ開発者でない限り、構造体にはStringを使うのがRustの定石です。
6. パフォーマンスとメモリ使用量の違い
Rustがなぜこれほどまでに文字列の型にこだわるのか、その理由はパフォーマンスにあります。Stringはヒープ領域にメモリを確保するため、アロケーション(メモリ確保)というコストが発生します。プログラムの実行中に何度もStringを生成したり破棄したりすると、わずかながら処理速度に影響が出ます。
一方で&strは、すでにあるデータの場所を指しているだけなので、アロケーションが発生しません。この「ゼロコスト抽象化」こそがRustの魅力です。例えば、数ギガバイトあるテキストファイルの一部を検索して表示する場合、その都度Stringにコピーしていたらメモリが足りなくなりますが、&str(スライス)を使えば、元のデータを読み取るだけで済み、メモリ消費を最小限に抑えられます。
このように、データの「実体」を管理する責任を負うのがString、データの「利用」に特化するのが&strという役割分担がなされています。このバランスを意識して設計できるようになると、Rustの真の力を引き出せるようになります。
7. 変換と相互利用のテクニック
実際の開発では、Stringから&strへ、あるいはその逆への変換が頻繁に発生します。これらの操作をスムーズに行えるようになることが、スムーズなコーディングの鍵です。
まず、Stringから&strへの変換は非常に簡単です。変数の前に&を付けるか、as_str()メソッドを呼ぶだけです。逆に、&strからStringへ変換するには、.to_string()やString::from()、あるいは.to_owned()といったメソッドを使います。これらはすべて新しいメモリ領域を確保し、中身をコピーする操作であることを忘れないでください。
fn main() {
let original_str: &str = "Rustプログラミング";
// &str から String への変換(メモリコピーが発生)
let owned_string: String = original_str.to_string();
// String から &str への変換(参照を渡すだけ)
let borrowed_str: &str = &owned_string;
println!("Owned: {}", owned_string);
println!("Borrowed: {}", borrowed_str);
// 文字列の連結での工夫
let combined = format!("{} - {}", owned_string, original_str);
println!("結合後: {}", combined);
}
Owned: Rustプログラミング
Borrowed: Rustプログラミング
結合後: Rustプログラミング - Rustプログラミング
また、複数の文字列を結合する際には、format!マクロが非常に便利です。これを使えば、Stringと&strが混ざっていても、新しいStringとして一つにまとめることができます。Rustの強力なマクロ機能を活用して、文字列処理をよりシンプルに記述しましょう。
8. 実践的な設計判断のフローチャート
最後に、どのような基準で型を選べばよいか、判断のポイントを整理します。迷ったときは、自分自身に次の問いを投げかけてみてください。
まず「その文字列は、後から文字を付け足したり変更したりする必要があるか?」という点です。もしYesなら、迷わず mut String を選んでください。次に「そのデータは、その関数や構造体が責任を持って保持し続ける必要があるか?」と考えます。これに対する答えがYesであれば、所有権を持てる String が適切です。
一方で、「単に画面に表示したいだけ」「特定の文字が含まれているかチェックしたいだけ」という読み取り専用の用途であれば、常に &str を優先して使うべきです。特に公開するライブラリや共有関数の引数では、&str を使うのがRustコミュニティにおけるベストプラクティスとされています。これにより、利用者は余計なメモリコピーを気にすることなく、あなたの関数を呼び出すことができるようになります。
Rustの学習曲線は緩やかではありませんが、この文字列の使い分けをマスターすれば、メモリ管理の概念が自然と身に付いてくるはずです。最初はコンパイラに怒られることも多いでしょうが、それはあなたのプログラムを安全にしようとしてくれているRustからのアドバイスです。一歩ずつ、確実に理解を深めていきましょう。