Rustの文字列操作、なんだか難しそう…って思っていませんか?
他のプログラミング言語とは少し違う振る舞いに、戸惑うこともあるかもしれませんね。特に `String` と `&str` という二つの型、どう使い分ければいいのか迷う方も多いはず。
この記事では、Rustの文字列の基本から、ちょっとした応用、そして初心者がハマりがちな落とし穴まで解説していきます。
この記事で学べること
- Stringと&str、二つの文字列型の違いがわかる
- 文字列の作り方、つなげ方、変更の仕方がわかる
- 文字列の一部分だけを扱う方法(スライス)がわかる
- 文字列の中身を一つずつ処理する方法がわかる
- Rustならではの注意点(所有権とか)がわかる
Rustの文字列とは? ~ 基本的な考え方 ~
Rustの文字列は、他の言語、例えばPythonやJavaScriptの文字列とは少し考え方が異なります。一番大きな違いは、Rustのメモリ管理の仕組み(所有権システム)が深く関わっている点にあります。
難しく考えなくて大丈夫! 要は、データ(この場合は文字列)を「誰が所有しているか」をRustが厳しくチェックしているのです。この仕組みのおかげで、メモリに関するやっかいなバグを防ぎやすくなっています。
そして、Rustには主に二種類の文字列型が登場します。それが `String` と `&str` です。なぜ二つも? それは、それぞれ役割が違うから。
ざっくり言うと、自分で中身を自由に変えられる文字列 (`String`) と、他の場所にある文字列の一部を指し示しているだけのもの (`&str`) がある、というイメージです。
例えるなら、`String` は自分で持っているノート、`&str` は図書館の本の一部分をコピーしたメモ、みたいな感じでしょうか。ノートは書き込めますが、メモは読むだけですよね。
2種類のRustの文字列: `String` と `&str`
では、`String` と `&str` の違いをもう少し詳しく見ていきましょう。ここを理解するのがRust文字列攻略の第一歩です!
String - 変更可能な文字列
`String` は、プログラムの実行中に内容を変更できる文字列です。テキストデータをプログラム自身が「所有」しているイメージですね。
- 特徴1: 中身を自由に追加したり、変更したりできます (可変)。
- 特徴2: 文字列データをメモリ上に確保し、そのデータの所有権を持ちます。
- 特徴3: 文字列の長さが変わるような操作も可能です。
データを自分で確保して管理する必要があるので、`&str` に比べると少しだけ動作が遅くなることもありますが、柔軟性はピカイチです。
ユーザーからの入力や、ファイルから読み込んだ内容など、実行時に中身が決まる文字列を扱う際によく使われます。
作り方はいろいろありますが、代表的なのは以下の方法です。
// 空のStringを作る let mut s1 = String::new(); // 文字列リテラル (&str) からStringを作る let s2 = String::from("こんにちは"); let s3 = "世界".to_string(); // .to_string() メソッドでも作れる
`String::from("...")` や `.to_string()` がよく使われますので、覚えておくと便利ですよ。
&str - 不変の文字列スライス
`&str` (文字列スライスと読みます) は、すでにある文字列データの一部分、または全体を「参照」している型です。参照、つまり「指し示しているだけ」なので、`&str` 自体がデータを持っているわけではありません。
- 特徴1: 中身を変更することはできません (不変)。
- 特徴2: 他の場所にある文字列データ(例えば `String` や、プログラム内に直接書かれた文字列リテラル)を指し示しています。所有権は持ちません。
- 特徴3: 効率的に文字列の一部を扱えます。
プログラムの中に直接書く `let message = "これは&str型";` のような文字列リテラルは、実はこの `&str` 型なのです。また、`String` の一部だけを使いたい時にも `&str` が活躍します。
// 文字列リテラルは &str 型 let message: &str = "This is immutable"; // 文字列リテラルは基本的に &str // String から &str を作る (スライス) let s = String::from("A piece of text"); // 英語のString // 最初の7バイト分 ("A piece") を &str として参照 let slice: &str = &s[0..7]; println!("Slice: {}", slice); // &str は元の String (s) を「借用」しているだけ // s がスコープを抜けると slice も無効になる (ライフタイム)
表示結果:
Slice: A piece
このように、`&str` はデータの「一部分」を効率よく、安全に扱うための仕組みなのです。`&str` はあくまで参照なので、元のデータ(この例では `s`)が存在し続ける必要がある点に注意しましょう。
Rustの文字列の基本的な使い方
理屈がわかったところで、いよいよ実践です!
Rustで文字列をどうやって作ったり、くっつけたり、変更したりするのか、具体的なコードを見ながら学んでいきましょう。`String` と `&str` のどちらを使うべきかも意識してみてくださいね。
文字列の作成 (String::new, String::from, to_string)
まずは `String` 型の文字列を作る方法のおさらいです。
書き方
fn main() { // 1. 空っぽの String を作るなら let mut empty_string = String::new(); println!("空の文字列: '{}'", empty_string); // 2. 最初から文字を入れるなら String::from let hello = String::from("こんにちは"); println!("挨拶: {}", hello); // 3. &str から作るなら .to_string() も便利 let world = "世界"; // これは &str let world_string = world.to_string(); // String に変換 println!("場所: {}", world_string); // 文字列を追加してみる (mut が必要) empty_string.push_str("何か書く"); println!("追記後: '{}'", empty_string); }
表示結果
空の文字列: '' 挨拶: こんにちは 場所: 世界 追記後: '何か書く'
空の `String` を作るときは `String::new()` が基本です。後から中身を追加する予定があるなら、`mut` を付けて変数を可変にしておくのを忘れずに。
文字列の結合 (+演算子, format!マクロ)
文字列同士をくっつけたい場面はよくありますよね。Rustにはいくつかの方法があります。
書き方1: `+` 演算子 (ちょっと注意が必要)
fn main() { let s1 = String::from("Rustの"); let s2 = String::from("文字列"); // s1 に s2 を結合 → s1 の所有権が移動し、s1は使えなくなる! // s2 は &str 型である必要があるので、&s2 のように参照にする let s3 = s1 + &s2; println!("{}", s3); // println!("{}", s1); // これはエラーになる! s1はもう使えない }
表示結果
Rustの文字列
`+` 演算子は直感的ですが、左側の `String` の所有権が移動してしまうという特徴があります。つまり、`s1 + &s2` を実行した後、`s1` はもう使えなくなってしまうのです。これは少し扱いにくい場面もあります。
書き方2 - `format!` マクロ (おすすめ!)
fn main() { let s1 = String::from("もっと"); let s2 = String::from("安全に"); let s3 = String::from("結合!"); // format! マクロは所有権を奪わない! let combined = format!("{} {} {}", s1, s2, s3); println!("{}", combined); // s1, s2, s3 はまだ使える! println!("元の文字列: {}, {}, {}", s1, s2, s3); }
表示結果
もっと 安全に 結合! 元の文字列: もっと, 安全に, 結合!
`format!` マクロは、元の文字列の所有権を移動させずに、新しい `String` を作ってくれます。複数の値を組み合わせたり、数値などを文字列に含めたりするのも簡単なので、文字列結合には `format!` マクロを使うのが安全で便利ですよ。
文字列の変更・更新 (push_str, push, replace)
`String` 型の文字列は中身を変更できます。いくつか代表的なメソッドを見てみましょう。
書き方
fn main() { // 変更するには `mut` が必要 let mut my_string = String::from("最初のテキスト"); println!("変更前: {}", my_string); // 1. 文字列 (&str) を末尾に追加: push_str my_string.push_str("を追加しました。"); println!("push_str後: {}", my_string); // 2. 一文字 (char) を末尾に追加: push my_string.push('!'); println!("push後: {}", my_string); // 3. 文字列の一部を置換: replace (これは新しい String を返す) let replaced_string = my_string.replace("追加", "変更"); println!("replace後: {}", replaced_string); // 元の my_string は変わっていないことに注意! println!("元の文字列: {}", my_string); }
表示結果
変更前: 最初のテキスト push_str後: 最初のテキストを追加しました。 push後: 最初のテキストを追加しました。! replace後: 最初のテキストを変更しました。! 元の文字列: 最初のテキストを追加しました。!
`push_str` や `push` は元の `String` を直接変更します。そのため、`let mut` で変数を可変 (`mut`) にしておく必要があります。一方、`replace` は元の文字列を変更せず、置換後の新しい `String` を返す点に注意しましょう。
文字列スライス (部分的な参照)
文字列全体ではなく、一部分だけを使いたいときに便利なのが「スライス」です。これは元の文字列データの一部を指し示す「参照」であり、`&str` 型として扱われます。効率的に文字列の一部にアクセスできますよ。
書き方
fn main() { // シングルバイト文字中心の文字列で試してみましょう let long_text = String::from("This is a sample text."); // [開始位置..終了位置] で範囲を指定 // 開始位置のバイトは含む、終了位置のバイトは含まない // 最初の4バイト ("This") をスライス let head = &long_text[0..4]; println!("Head: {}", head); // 5バイト目から7バイト目まで ("is") let middle = &long_text[5..7]; println!("Middle: {}", middle); // 省略記法も便利 let head_short = &long_text[..4]; // 最初から4バイト目までと同じ let tail = &long_text[15..]; // 15バイト目から最後まで ("text.") println!("Head (short): {}", head_short); println!("Tail: {}", tail); }
表示結果
Head: This Middle: is Head (short): This Tail: text.
スライスは `&text[開始バイト位置..終了バイト位置]` のように、バイト単位での指定となります。上の例のようにASCII文字(アルファベットや数字など、基本的に1文字1バイト)だけで構成されている文字列なら、バイト位置と文字数が一致するので直感的に扱えます。
しかし、ここで非常に重要な注意点があります。
日本語のようなマルチバイト文字(1文字が2バイト以上で表現される文字)が含まれる文字列に対して、バイト単位でスライスを行うと、文字の途中でデータが切れてしまう可能性があります。
そうなると、文字化けしたり、エラー(パニック)を起こして停止してしまいます。
例えば、"こんにちは" のような文字列を不用意にバイトスライスするのは危険です。もし文字単位で安全にスライスしたい場合は、専用のライブラリ(クレート)を使うなどの工夫が必要になります。
最初のうちは、マルチバイト文字をスライスする際は特に慎重になるか、文字境界を意識しなくて済む `.chars()` などを使うのが安全かもしれません。
また、指定した範囲が元の文字列のバイト範囲を超えている場合もパニックが発生しますので、インデックスの指定は慎重に行いましょう。
文字列のイテレーション (chars, bytes)
文字列の中の文字を一つずつ順番に処理したい、という場面もありますね。`for` ループと組み合わせるのが一般的です。
書き方
fn main() { let message = String::from("Hello, 世界!"); // 1. 文字 (char) ごとに処理: chars() println!("--- chars() ---"); for c in message.chars() { print!("'{}' ", c); } println!(); // 改行 // 2. バイト (u8) ごとに処理: bytes() println!("--- bytes() ---"); for b in message.bytes() { print!("{} ", b); } println!(); // 改行 }
表示結果
--- chars() --- 'H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界' '!' --- bytes() --- 72 101 108 108 111 44 32 228 184 150 231 149 140 33
`chars()` メソッドを使うと、Unicodeの文字単位で処理できます。日本語のようなマルチバイト文字もちゃんと1文字として扱ってくれます。
一方、`bytes()` メソッドは、文字列を構成するバイトデータ(数値)を一つずつ取り出します。見ての通り、「世界」は複数のバイトで表現されていますね。通常、日本語などを扱う場合は `chars()` を使うのが自然でしょう。
Rustの文字列操作における注意点 (所有権とライフタイム)
さて、基本的な使い方がわかったところで、Rustならではの注意点、特に「所有権」と「ライフタイム」について触れておきましょう。
これがRustの文字列(に限らず、多くの型)を扱う上で、最初のうちは少し難しく感じるところかもしれません。
例えば、関数に `String` を渡すと、その関数のものになってしまい(所有権が移動し)、元の場所では使えなくなることがあります。
fn process_string(s: String) { println!("関数内で表示: {}", s); // s はこの関数が終わると破棄される } fn main() { let my_string = String::from("データ"); process_string(my_string); // 下の行はコンパイルエラー! my_stringの所有権は process_string 関数に移ったため // println!("main関数で表示: {}", my_string); }
これを避けるには、`String` そのものではなく、その参照 `&str` を渡す方法があります。参照(借用とも言います)なら、所有権は移動しません。
// &str を受け取るように変更 fn process_slice(s: &str) { println!("関数内で表示: {}", s); // s (&str) は参照なので、元のデータは破棄されない } fn main() { let my_string = String::from("データ"); // String (&my_string) から &str を渡す process_slice(&my_string); // 今度はOK! my_string はまだ使える println!("main関数で表示: {}", my_string); }
表示結果
関数内で表示: データ main関数で表示: データ
また、「ライフタイム」は、参照 (`&str` など) が、参照先のデータよりも長生きしないようにするための仕組みです。例えば、関数が終わった後も存在するはずのないデータへの参照を作ろうとすると、コンパイラがエラーを出してくれます。
最初はこれらのルールに戸惑うかもしれませんが、これはプログラムが予期せぬ動作をするのを防ぐための、Rustの安全機能なのです。
コンパイルエラーが出たら、エラーメッセージをよく読んでみましょう。Rustのコンパイラは非常に親切で、どう直せばいいかのヒントをくれることが多いです。エラーメッセージを読む練習も、Rust上達の近道ですよ!
【まとめ】Rustの文字列を使いこなそう
Rustの文字列 `String` と `&str` について、基本的なところから注意点まで見てきました。最後に要点をまとめておきましょう。
- Rustには主に2つの文字列型がある。`String` は可変で所有権を持つ文字列、`&str` は不変の文字列スライス(参照)。
- `String` は `String::new()`, `String::from()`, `.to_string()` などで作れる。
- 文字列の結合は `+` 演算子も使えるけれど、所有権が移動するので注意。`format!` マクロが安全で推奨される。
- `String` の変更には `push_str` (文字列追加), `push` (一文字追加), `replace` (置換、新しいStringを返す) などがある。変更するなら `mut` が必要。
- 文字列の一部を参照するにはスライス `&text[start..end]` を使う。バイト単位なので注意。
- 文字ごとの処理は `.chars()`、バイトごとの処理は `.bytes()` を使う。
- 関数の引数などで `String` を渡すと所有権が移動する。参照 `&str` を使うと所有権は移動しない。
- 所有権やライフタイムのルールは、プログラムの安全性を高めるためのもの。エラーメッセージをヒントに解決していこう!
どちらを使うか迷ったら、基本的には
- 文字列の中身を変更する必要がある、またはゼロから文字列を組み立てるなら `String`
- 文字列の中身を変更する必要がない、または関数に文字列を渡すだけなら `&str`
という使い分けを意識すると良いでしょう。
Rustの文字列操作は、慣れるまで少し練習が必要かもしれませんが、その仕組みを理解すれば、非常に安全で効率的なコードを書くことができます。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。