byte index is not a char boundary パニックの原因と直し方
Rustの文字列はUTF-8のバイト列として保持されており、日本語のようなマルチバイト文字の途中の位置でスライスしようとするとパニックします。文字単位で扱いたい場合はcharsなどのイテレータを使う必要があります。
エラーメッセージの読み方
thread 'main' panicked at main.rs:3:19:
main.rs- ファイル名
3- 行番号 — 実際にクラッシュした行
19- 列番号
panicked- パニック — 実行時に回復不能な状態になったことを示します。tryやResultで捕まえない限り、そのままプロセスが終了します
byte index 1 is not a char boundary; it is inside 'こ' (bytes 0..3) of `こんにちは`- 詳細メッセージ — どの値が問題だったか
このエラーが出る典型パターン
パターン1
1 fn main() { 2 let s = String::from("こんにちは"); 3 let slice = &s[0..1]; ^ 4 println!("{}", slice); 5 }
日本語の1文字はUTF-8で3バイトです。バイト境界の途中である1や2を指定するとパニックします。
直し方: 1 を 3 にします。
パターン2
1 fn main() { 2 let s = String::from("ありがとう"); 3 let slice = &s[0..2]; ^ 4 println!("{}", slice); 5 }
英語の感覚で1文字ずつバイト数を数えると、マルチバイト文字の途中を指してしまいます。
直し方: 2 を 3 にします。
パターン3
1 fn main() { 2 let s = String::from("すし"); 3 let slice = &s[0..1]; ^ 4 println!("{}", slice); 5 }
文字数で数えたい場合は、バイトのスライスではなくcharsイテレータを使う必要があります。
直し方: 1 を 3 にします。
パターン4
1 fn main() { 2 let s = String::from("ねこ"); 3 let slice = &s[0..2]; ^ 4 println!("{}", slice); 5 }
バイト境界でない位置を指定した瞬間に、Rustはその場でパニックして誤ったスライスを防ぎます。
直し方: 2 を 3 にします。
パターン5
1 fn main() { 2 let s = String::from("さくら"); 3 let slice = &s[0..1]; ^ 4 println!("{}", slice); 5 }
文字列のバイト長とその文字列が何文字かは、マルチバイト文字を含む限り一致しません。
直し方: 1 を 3 にします。
よくある誤解
「文字列のスライスは英語と同じように1文字=1バイトとして数えられるはず」という思い込みは誤りです。UTF-8ではマルチバイト文字の内部にバイト境界があり、その途中を指定するとパニックします。
実務での勘所
RustのStringとstrは、常に有効なUTF-8であることを型システムが保証する、という強い前提の上に成り立っています。UTF-8は文字によって1〜4バイトの可変長でエンコードされるため、マルチバイト文字の途中でスライスを許してしまうと、結果のバイト列が有効なUTF-8として成立しなくなり、この保証が壊れてしまいます。Rustはこの保証を絶対に破らないという方針のため、境界がずれたスライスを黙って受け入れるのではなく、その場でパニックさせて不正な状態が生まれることを防いでいます。