Rustの文字列を極める!&str(文字列スライス)の基本概念とString型との違い
生徒
「Rustで文字列を扱おうとしたら、&strとStringの2種類が出てきて混乱しています。何が違うんですか?」
先生
「Rustを学び始めた人が最初に突き当たる壁ですね。簡単に言うと、&strは『どこかにある文字列をのぞき見するための参照(スライス)』で、Stringは『自分で中身を所有して自由に変更できる文字列』のことですよ。」
生徒
「のぞき見……。つまり、&str自体はデータを持っていないということですか?」
先生
「その通りです!&strは、メモリ上のどこかに存在する文字列データが『どこから始まって、どれくらいの長さか』という情報だけを持っています。この仕組みを理解すると、Rust特有のメモリ安全性がより深く理解できますよ。まずは文字列スライスの正体から詳しく見ていきましょう!」
1. 文字列スライスである&strの正体とは?
Rustにおける&strは、一般的に「文字列スライス」と呼ばれます。この型は、メモリ上の連続したUTF-8形式のデータへの参照を保持しています。初心者がまず覚えるべきは、&strは「不変(イミュータブル)」であるという点です。つまり、一度作成した文字列スライスの内容を後から書き換えることはできません。
なぜこのような型が必要なのでしょうか。それは効率のためです。大きな文字列データがあるとき、その一部分だけを使いたい場合に、データをコピーして新しい変数を作るのはメモリの無駄になります。&strを使えば、元のデータをコピーすることなく、特定の範囲を「指し示す」だけで処理が可能になります。これはプログラムの高速化とメモリ節約に大きく貢献します。
また、ダブルクォーテーションで囲ったリテラル(例:"hello")は、プログラムのバイナリ自体に埋め込まれており、その型は正確には&'static strとなります。これはプログラムが実行されている間ずっと有効な文字列スライスであることを意味しています。
2. メモリ構造から見る文字列スライスの仕組み
文字列スライスの仕組みを理解するために、内部でどのようなデータを持っているかを確認しましょう。&strは「ファットポインタ」と呼ばれる構造をしています。具体的には、以下の2つの情報で構成されています。
- ポインタ: 実際の文字列データが格納されているメモリの開始アドレス。
- 長さ(長さ情報): その文字列が何バイト分続いているかという数値。
このように、データそのものを所有しているわけではなく、データの「場所」と「範囲」だけを知っている状態です。これに対して、String型はヒープメモリ上にデータを確保し、ポインタ、長さ、さらに「容量(キャパシティ)」を持っています。Stringが「データの持ち主(オーナー)」であるのに対し、&strは「借りている人(借用者)」というイメージを持つと分かりやすいでしょう。この「貸し借り」の概念こそが、Rustのメモリ安全性を支える根幹部分です。
3. 文字列リテラルと文字列スライスの基本コード
それでは、実際にコードを書いて確認してみましょう。最もシンプルな&strの使い方は、文字列リテラルを直接変数に代入することです。
fn main() {
// 文字列リテラルは &str 型になります
let greeting: &str = "こんにちは、Rustの世界へ!";
println!("メッセージ: {}", greeting);
println!("文字数(バイト数): {}", greeting.len());
}
メッセージ: こんにちは、Rustの世界へ!
文字数(バイト数): 36
上記のコードでは、greetingという変数が静的な領域にある文字列データを指し示しています。Rustの文字列はUTF-8でエンコードされているため、len()メソッドが返す値は「文字数」ではなく「バイト数」であることに注意が必要です。日本語(全角文字)は1文字で3バイト以上を消費するため、期待する数値と異なる場合がありますが、これもRustが正しく多言語を扱うための仕様です。
4. String型から文字列スライスを取り出す方法
実際の開発では、ユーザーからの入力など動的に生成されたString型から、特定の部分を&strとして取り出す場面が非常に多いです。これを「スライシング」と呼びます。スライシングを行うには、範囲指定(レンジ)記法を使用します。
fn main() {
let s = String::from("Rust Programming");
// 0文字目から4文字分(インデックス0から4の手前まで)をスライス
let rust = &s[0..4];
// 5文字目から最後までをスライス
let prog = &s[5..];
println!("スライス1: {}", rust);
println!("スライス2: {}", prog);
}
スライス1: Rust
スライス2: Programming
ここで重要なのは、&s[0..4]という書き方です。これは、Stringが持っているデータの一部を参照していることを示しています。このとき、元の変数sの所有権は移動しません。ただし、スライスが存在している間は、元のStringの内容を変更(破壊的変更)することはできません。Rustのコンパイラが「誰かが参照している間は、中身を変えてはいけない」と厳格にチェックしてくれるため、安全にデータを共有できるのです。
5. 関数での引数としての&strの活用術
Rustの関数設計において、文字列を引数に取る場合は、特別な理由がない限りStringではなく&strを使うのがベストプラクティスとされています。その理由は、汎用性の高さにあります。&strを引数にすれば、文字列リテラルも、Stringから切り出したスライスも、どちらも受け取ることができるからです。
fn print_message(message: &str) {
println!("受け取った文字列: {}", message);
}
fn main() {
let literal = "私はリテラルです";
let dynamic = String::from("私はStringです");
// 両方の型を同じ関数に渡せます
print_message(literal);
print_message(&dynamic); // Stringを&strとして渡す(Derefによる型強制)
}
受け取った文字列: 私はリテラルです
受け取った文字列: 私はStringです
このコードに見られるように、&dynamicと記述することでString型は自動的に&strへと型変換(型強制)されます。これにより、関数側で「メモリをどう管理するか」を気にする必要がなくなり、コードの再利用性が大幅に向上します。初心者の方は、まず「読み取り専用の引数には&strを使う」と覚えておけば間違いありません。
6. 日本語を扱う際の文字列スライスの注意点
Rustで日本語(マルチバイト文字)のスライスを扱うときには、非常に重要な注意点があります。それは「文字の境界」です。Rustの文字列スライスはバイト単位でインデックスを指定しますが、日本語のように複数バイトで構成される文字の途中でスライスを切ろうとすると、プログラムは実行時にパニック(強制終了)を起こします。
fn main() {
let jp_text = "こんにちは";
// 「こ」は3バイトなので、0..3はOK
let first_char = &jp_text[0..3];
println!("最初の文字: {}", first_char);
// 下記のように1バイト目などで切るとエラーになります
// let error_slice = &jp_text[0..1];
}
最初の文字: こ
例えば、&jp_text[0..1]とすると、「こ」という文字の1バイト分だけを取り出そうとしますが、これではUTF-8として不正なデータになってしまいます。Rustは安全性を重視するため、このような不正な状態を許しません。日本語を扱う際は、スライスで範囲を指定するよりも、chars()メソッドを使ってイテレータとして1文字ずつ処理する方が安全で確実です。こうした仕様も、バグを未然に防ぐためのRustの徹底した設計思想の表れです。
7. 文字列の連結と&strの関係性
文字列スライス&strは不変であるため、自分自身に別の文字列を付け加えることはできません。文字列を結合して新しい文章を作りたい場合は、String型へ変換するか、format!マクロを使用するのが一般的です。特に複数の変数を組み合わせて複雑な文字列を作る際は、format!マクロが非常に便利で、可読性も高くなります。
fn main() {
let part1: &str = "Rust";
let part2: &str = "プログラミング";
// &strを連結して新しいStringを作る
let combined = format!("{}を学ぶ{}", part1, part2);
// Stringを再び&strとして利用することも可能
let slice: &str = &combined;
println!("連結結果: {}", slice);
}
連結結果: Rustを学ぶプログラミング
このように、変化しない「部品」としての&strを使い、必要に応じてStringで組み立てるという流れがRustの基本的なパターンです。&strからStringへの変換には.to_string()やString::from()を使い、逆にStringから&strを得るには&(アンパサンド)を付けるだけ、という相互変換をマスターすれば、Rustの文字列処理は一気にスムーズになります。
8. 文字列スライスをより深く理解するためのヒント
Rustの学習を進めていくと、ライフタイム(Lifetime)という概念に出会います。実は&strとライフタイムは密接に関係しています。文字列スライスは他の場所にあるデータを参照しているため、「そのデータがいつまで存在しているか」が重要になるからです。もし参照先のデータが先にメモリから消えてしまったら、スライスは存在しない場所を指す危険な存在(ダングリングポインタ)になってしまいます。
しかし、安心してください。Rustのコンパイラは非常に賢く、こうしたメモリの生存期間を常にチェックしています。初心者のうちは、コンパイルエラーが出たときに「参照先のデータが先に消えようとしていないか?」を確認する習慣をつけるだけで十分です。文字列スライスの仕組みを理解することは、Rust最大の難所と言われる「所有権と借用」を攻略するための第一歩となります。一歩ずつ、この強力で安全な武器を自分のものにしていきましょう。