Rustというプログラミング言語を学び始めた際、多くの開発者が最初に直面する壁の一つが「文字列」の扱いです。
C++やJava、Pythonといった他の言語とは異なり、Rustには主にStringと&strという2種類の文字列型が存在します。
これらは単なる書き方の違いではなく、Rustの核心である「所有権」や「メモリ管理」の仕組みと密接に関連しています。 本記事では、これら2つの型の違いをメモリ構造から徹底的に紐解き、実務でどのように使い分けるべきかを整理します。
Rustの文字列操作をマスターすることで、より安全で効率的なコードを書けるようになるでしょう。
なぜRustには2種類の文字列があるのか
Rustが2つの主要な文字列型を使い分けている最大の理由は、「メモリの安全性」と「実行効率」を両立させるためです。
多くのスクリプト言語では、文字列は可変(Mutable)かつ自動的にメモリ管理される単一のオブジェクトとして扱われます。
しかし、システムプログラミング言語であるRustでは、データが「どこに配置されているか」や「誰がそのデータを所有しているか」を厳密に管理します。
動的にサイズを変更できるデータと、変更できない固定の参照を明確に区別することで、不要なメモリ割り当て(アロケーション)を排除しています。
この設計思想を理解することが、Stringと&strの違いを理解するための第一歩となります。
String(文字列型)の特徴と役割
Stringは、Rustの標準ライブラリで提供されている「所有権を持つ」可変な文字列型です。
ヒープメモリへの割り当て
Stringのデータ本体は、プログラムの実行中に動的に確保される「ヒープ」と呼ばれるメモリ領域に格納されます。
スタック領域には、ヒープ上のデータを指すポインタ、データの長さ(Length)、および現在の容量(Capacity)の3つの情報が保持されます。
ヒープを使用するため、コンパイル時にはデータのサイズが不明であっても、実行中に文字列を大きくしたり小さくしたりすることが可能です。
所有権の保持
String型の変数は、その文字列データの所有者となります。
変数がスコープを外れると、Rustの所有権システムによってヒープ上のメモリが自動的に解放(ドロップ)されます。
これにより、メモリリークを防ぎつつ、動的な文字列操作を安全に行うことができます。
fn main() {
// String::from を使用して、ヒープ上に String を作成
let mut s = String::from("Hello");
// 文字列を動的に変更(追加)することが可能
s.push_str(", Rust!");
println!("{}", s);
}
Hello, Rust!
&str(文字列スライス)の特徴と役割
&strは「文字列スライス」と呼ばれ、既存の文字列データの一部(または全部)を参照する型です。
所有権を持たない参照
&strはデータの所有権を持たず、あくまで「どこかにあるデータを見ているだけ」の状態を指します。
そのため、&str自体がスコープを外れても、参照先のデータが解放されることはありません。
Rustにおいて、ダブルクォーテーションで囲まれた文字列リテラル(例: "Hello")は、バイナリに直接埋め込まれた不変のデータへの参照であり、型は&'static strとなります。
不変性と軽量さ
&strは基本的に不変であり、参照先のデータを書き換えることはできません。
メモリ上では「ポインタ」と「長さ」の2つの値だけで構成されているため、非常に軽量に受け渡しができます。
新しいメモリを割り当てる必要がないため、読み取り専用の処理においては非常に高いパフォーマンスを発揮します。
fn main() {
// 文字列リテラルは &str 型
let s: &str = "Hello, World!";
// 部分的に参照することも可能(スライス)
let hello = &s[0..5];
println!("Original: {}", s);
println!("Slice: {}", hello);
}
Original: Hello, World!
Slice: Hello
Stringと&strのメモリ構造の比較
これら2つの型の違いをより深く理解するために、メモリ上での構造を比較してみましょう。
| 特性 | String | &str |
|---|---|---|
| メモリ配置 | ヒープ(動的確保) | スタック、ヒープ、またはバイナリ |
| 所有権 | あり(自身がデータを所有) | なし(他者のデータを参照) |
| 可変性 | 可変(mut 指定で変更可能) | 不変(参照先は変更不可) |
| 情報量 | ポインタ、長さ、容量 | ポインタ、長さ |
Stringは「バッファの管理」を目的としており、&strは「データの利用」を目的としていると言い換えることができます。
Stringを所有している場合、そこから&str(参照)を作り出すことは非常に簡単ですが、その逆にはメモリ割り当てを伴う変換が必要になります。
Stringと&strの相互変換
Rustでは、これら2つの型を頻繁に変換して使用します。
&strからStringへの変換
不変の参照から、所有権を持つ動的な文字列を作成する場合です。
これには、to_string()メソッドやString::from()関数、あるいはto_owned()が使われます。
この操作を行うと、ヒープメモリに新しく領域が確保され、データがコピーされます。
let literal: &str = "example";
// 3通りの変換方法(結果はすべて同じ String 型)
let s1 = literal.to_string();
let s2 = String::from(literal);
let s3 = literal.to_owned();
Stringから&strへの変換
所有している文字列を、参照として貸し出す場合です。
変数の前に&を付ける(借用する)か、as_str()メソッドを使用します。
Rustには「Deref型強制」という仕組みがあるため、&Stringは自動的に&strとして扱うことができます。
let s = String::from("hello");
// &String を渡すと、自動的に &str として扱われる
let slice: &str = &s;
// 明示的に変換する場合
let slice_explicit: &str = s.as_str();
実務での使い分けガイドライン
どちらの型を使うべきか迷った際は、以下のガイドラインを参考にしてください。
関数の引数には &str を優先する
関数の引数として文字列を受け取る場合、特別な理由がない限り&strを使用するのがベストプラクティスです。
&strを引数に取れば、文字列リテラル(&str)だけでなく、Stringの参照(&String)もそのまま渡すことができます。
これにより、関数の汎用性が高まり、呼び出し側で余計なメモリコピーが発生するのを防ぐことができます。
// 良い例: &str を引数に取る
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let s = String::from("Alice");
greet("Bob"); // 文字列リテラルを渡せる
greet(&s); // String の参照も渡せる
}
構造体のフィールドでは String を検討する
構造体(struct)に文字列を持たせる場合、基本的にはStringを選択します。
構造体のインスタンス自体が文字列データの所有権を持つことで、ライフタイム(生存期間)の複雑な管理を避けることができるからです。
もし構造体で&strを使おうとすると、参照先のデータが構造体よりも長生きすることを保証するための「ライフタイム注釈」が必要になり、コードが複雑化します。
文字列を加工・蓄積する場合は String を使う
ユーザー入力を受け取る、ループ内で文字列を結合する、あるいはファイルの内容を読み込むといったケースでは、Stringが必要です。
&strはサイズを拡張できないため、動的なコンテンツ生成には適していません。
パフォーマンス上の注意点
Rustの文字列操作において、パフォーマンスを低下させる主な要因は「不要な String の生成」です。
例えば、単に文字列を表示するだけの関数内で毎回 .to_string() を呼び出すと、そのたびにヒープメモリのアロケーションが発生します。
読み取り専用の操作であれば、可能な限り &str を伝播させることで、ゼロコスト抽象化という Rust の恩恵を最大限に受けることができます。
一方で、所有権が必要な場面で無理に参照を使おうとすると、ライフタイムのコンパイルエラーに悩まされることになります。
「変更が必要か」「所有権を移動させる必要があるか」という観点で、適切に選択することが重要です。
まとめ
RustのStringと&strは、メモリ管理と所有権の概念を体現した非常に重要な要素です。
Stringはヒープ領域を確保する「所有権を持つ可変な文字列」であり、データの構築や永続的な保持に適しています。
対して&strは「不変な文字列データへの参照」であり、関数の引数や読み取り専用の操作において高い効率を誇ります。
この2つの違いを正しく理解し、関数の引数には&strを、データの所有にはStringを使うという基本ルールを守るだけで、Rustのコードは格段に書きやすくなります。
メモリの使われ方を意識しながらこれらの型を使い分け、Rustらしい安全で高速なプログラムを構築していきましょう。
