問題:なぜRustでは s[0] が失敗するのか
Python、JavaScript、C++などの言語を使っていると、文字を取得するためにまず my_string[0] と書きたくなるでしょう。しかしRustでは、この操作は即座にコンパイルエラーを引き起こします。言語仕様として、これを許可していないのです。
error[E0277]: the type `String` cannot be indexed by `usize`
--> src/main.rs:3:13
|
3 | let c = s[0];
| ^^^^ `String` cannot be indexed by `usize`
|
= help: the trait `Index<usize>` is not implemented for `String`
Rustが文字列を異なる方法で扱うのは、文字列が Vec<u8> をラップしたUTF-8エンコード形式だからです。UTF-8では、1つの文字は1バイトから4バイトの間のサイズになります。例えば、英字の 'R' は1バイトですが、カニの絵文字 (🦀) は4バイトです。もしRustが s[i] を許可してしまうと、i番目の「バイト」が欲しいのか、それともi番目の「文字」が欲しいのかが不明確になります。バイトのインデックス指定は高速(O(1))ですが、文字のインデックス指定は文字列を走査する必要があるため低速(O(n))です。Rustは、こうした隠れたパフォーマンスコストを避けるために、明示的な記述を強制します。
デバッグプロセス
単純なユーザー入力をパースしようとしている時に、この問題に遭遇するかもしれません。失敗するコード例を見てみましょう:
fn main() {
let name = String::from("Rust");
let first_letter = name[0]; // この行でビルドが失敗します
println!("First letter is: {}", first_letter);
}
コンパイラは、String 型が usize による Index トレイトを実装していないことを指摘しています。解決するには、速度を優先するかUnicodeの安全性を優先するかに基づいて、特定の戦略を選択する必要があります。
解決策1:.chars().nth() を使用する(安全な方法)
特定の文字が必要で、Unicodeを正しく扱いたい場合は、chars() イテレータを使用します。これは、ほとんどのアプリケーションにおいて最も信頼できる方法です。
fn main() {
let name = String::from("Rust");
// .chars() はUnicode文字をイテレートします
// .nth(0) は安全に最初の文字の取得を試みます
if let Some(first_char) = name.chars().nth(0) {
println!("The first character is: {}", first_char);
}
}
メリット: "Ferris 🦀" のようなマルチバイト文字を壊さずに扱えます。 デメリット: 実行時間がO(n)になります。100番目の文字を見つけるには、Rustはその前にあるすべてのバイトを確認しなければなりません。
解決策2:文字列スライス(高速だが危険)
文字が確実に1バイト(標準的なASCIIなど)であると確信している場合や、正確なバイト境界がわかっている場合は、スライスを使用できます。これは char ではなく &str を返します。
fn main() {
let name = String::from("Rust");
let first_char_slice = &name[0..1];
println!("First char: {}", first_char_slice);
}
警告: マルチバイト文字の途中でスライスを行うと、実行時に panic を引き起こします。プログラムは即座にクラッシュします。
let crab = String::from("🦀");
let fail = &crab[0..1]; // パニックが発生します! カニの絵文字は4バイト長であり、1は境界ではありません。
解決策3:生のバイトにアクセスする
文字を気にせず、生の u8 値だけが必要な場合があります。その場合は as_bytes() を使用します。
fn main() {
let name = String::from("Rust");
let first_byte = name.as_bytes()[0];
println!("First byte value: {}", first_byte); // 'R' のASCII値である 82 が出力されます
}
書記素クラスタ(Grapheme Clusters)の扱い
見た目上の1文字が複数のUnicode値で構成されている場合があります。肌の色を指定した絵文字や、アクセント記号付きの文字(y̆ など)がその例です。これらに対しては .chars() だけでは不十分です。「人間が認識する」文字を扱うには、unicode-segmentation クレートが必要になります。
use unicode_segmentation::UnicodeSegmentation;
fn main() {
let complex_emoji = "y̆"; // これは 'y' と結合用の記号(ブレーヴェ)の組み合わせです
let first_grapheme = complex_emoji.graphemes(true).next().unwrap();
println!("Real first char: {}", first_grapheme);
}
検証:修正を確認する方法
- `cargo check` を実行し、E0277エラーが解消されていることを確認します。
- `cargo run` を実行して出力を確認します。
- `.nth()` を選択した場合は、空の文字列でテストして `Option` の処理が機能することを確認します。
- スライスを選択した場合は、`é` のような非ASCII文字を渡してみて、境界によってパニックが発生しないことを確認します。
学んだこと
- **文字列は配列ではない:** Rustは利便性よりもデータの整合性を優先します。明示的に指示しない限り、UTF-8文字列を単純なバイトのリストとして扱うことはできません。
- **計算量に注意:** 500番目の文字へのアクセスはO(n)の操作です。頻繁にランダムアクセスが必要な場合は、代わりにデータを `Vec<char>` に格納することを検討してください。
- **速度よりも安全性:** デフォルトでは `.chars().nth()` を使用しましょう。スライスやバイトアクセスへの変更は、パフォーマンスのボトルネックが測定され、かつバイト境界が保証できる場合にのみ行ってください。

