文字列で UTF-8 エンコードされたテキストを格納する
第4章で文字列について触れましたが、ここではさらに深く見ていきます。 Rust を学び始めた人が文字列でつまずくことはよくありますが、その理由は主に3つ あります。Rust には起こりうるエラーを表面化させる傾向があること、文字列が多くの プログラマが思っている以上に複雑なデータ構造であること、そして UTF-8 です。 他のプログラミング言語から来ると、これらの要因が組み合わさって難しく感じられる ことがあります。
文字列をコレクションの文脈で扱うのは、文字列がバイトのコレクションとして実装
されており、それに加えてそのバイト列をテキストとして解釈したときに有用な機能を
提供するいくつかのメソッドを備えているからです。この節では、作成、更新、読み取り
といった、すべてのコレクション型が持つ String の操作について話します。また、
String が他のコレクションと異なる点、すなわち、人間とコンピューターで String
データの解釈が異なるために String へのインデックスアクセスが複雑になることに
ついても説明します。
文字列を定義する
まず、string という用語で何を意味するのかを定義します。Rust のコア言語には
文字列型が1つしかなく、それは通常借用された形である &str として見かける
文字列スライス str です。第4章では、文字列スライスについて説明しました。これは、
別の場所に格納されている UTF-8 エンコードされた文字列データへの参照です。たとえば、
文字列リテラルはプログラムのバイナリに格納されるため、文字列スライスです。
コア言語に組み込まれているのではなく Rust の標準ライブラリによって提供される
String 型は、拡張可能で、可変で、所有権を持つ、UTF-8 エンコードされた
文字列型です。Rustacean が Rust における「文字列」について言うとき、それは
String 型または文字列スライス &str 型のどちらかを指している可能性があり、
そのどちらか一方だけを指しているわけではありません。この節の大部分は String
についてですが、Rust の標準ライブラリでは両方の型が多用されており、String も
文字列スライスも UTF-8 エンコードされています。
新しい文字列を作成する
String は実際には、追加の保証、制約、機能を備えたバイトベクタのラッパーとして
実装されているため、Vec<T> で使えるものと同じ操作の多くは String でも使えます。
Vec<T> と String の両方で同じように動作する関数の一例が、インスタンスを
生成する new 関数で、リスト8-11に示しています。
fn main() {
let mut s = String::new();
}
この行は s という新しい空の文字列を作成し、そこに後からデータを入れられます。
多くの場合、文字列を初期化するときに最初から入れておきたいデータがあります。その
ために、文字列リテラルのように Display トレイトを実装している任意の型で利用
できる to_string メソッドを使います。リスト8-12はその2つの例を示しています。
fn main() {
let data = "initial contents";
let s = data.to_string();
// The method also works on a literal directly:
let s = "initial contents".to_string();
}
このコードは initial contents を含む文字列を作成します。
文字列リテラルから String を作成するには、関数 String::from も使えます。
リスト8-13のコードは、to_string を使うリスト8-12のコードと等価です。
fn main() {
let s = String::from("initial contents");
}
文字列は非常に多くの用途で使われるため、文字列に対して多くの異なる汎用 API を
使うことができ、その分多くの選択肢があります。冗長に見えるものもありますが、
どれにもそれぞれの役割があります! この場合、String::from と to_string は
同じことを行うので、どちらを選ぶかはスタイルと可読性の問題です。
文字列は UTF-8 でエンコードされていることを思い出してください。したがって、 リスト8-14に示すように、適切にエンコードされた任意のデータを含めることが できます。
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
これらはすべて有効な String 値です。
文字列を更新する
String は、さらにデータをプッシュすれば、Vec<T> の内容と同じようにサイズを
大きくでき、内容も変更できます。さらに、String 値を連結するために +
演算子や format! マクロを便利に使えます。
push_str または push による追加
push_str メソッドを使って文字列スライスを追加することで、String を大きく
できます。これはリスト8-15に示しています。
fn main() {
let mut s = String::from("foo");
s.push_str("bar");
}
この2行の後、s には foobar が入ります。push_str メソッドが文字列スライスを
受け取るのは、必ずしも引数の所有権を取りたいわけではないからです。たとえば、
リスト8-16のコードでは、s2 の内容を s1 に追加したあとでも s2 を使える
ようにしたいのです。
fn main() {
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {s2}");
}
push_str メソッドが s2 の所有権を受け取っていたなら、最後の行でその値を出力
することはできないでしょう。しかし、このコードは期待どおりに動きます!
push メソッドは単一の文字を引数に取り、それを String に追加します。
リスト8-17では、push メソッドを使って文字 l を String に追加しています。
fn main() {
let mut s = String::from("lo");
s.push('l');
}
その結果、s には lol が入ります。
+ または format! による連結
既存の2つの文字列を結合したいことはよくあります。その1つの方法は、リスト8-18に
示すように + 演算子を使うことです。
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
}
文字列 s3 には Hello, world! が入ります。s1 が加算後に無効になる理由と、
s2 への参照を使った理由は、+ 演算子を使ったときに呼び出されるメソッドの
シグネチャにあります。+ 演算子は add メソッドを使っており、そのシグネチャは
次のようなものです。
fn add(self, s: &str) -> String {
標準ライブラリでは、add がジェネリクスと関連型を使って定義されているのが
わかります。ここでは具体的な型を当てはめていますが、これはこのメソッドを
String 値で呼び出したときに起こることです。ジェネリクスについては第10章で説明します。
このシグネチャは、+ 演算子のわかりにくい部分を理解するために必要な手がかりを与えてくれます。
まず、s2 には & が付いています。これは、2番目の文字列への参照を
1番目の文字列に追加していることを意味します。これは add 関数の s パラメータが
あるためです。String に追加できるのは文字列スライスだけであり、2つの
String 値をそのまま一緒に加算することはできません。ですが、待ってください。add の
2番目のパラメータで指定されている型は &str なのに、&s2 の型は &String です。
では、なぜリスト8-18はコンパイルできるのでしょうか。
add の呼び出しで &s2 を使える理由は、コンパイラが
&String 引数を &str に型強制できるからです。add メソッドを呼び出すと、
Rust は deref coercion を使い、ここでは &s2 を &s2[..] に変えます。deref
coercion については第15章でさらに詳しく説明します。add は s パラメータの所有権を受け取らないため、
この操作の後でも s2 は引き続き有効な String です。
次に、シグネチャを見ると、self には & が 付いていない ため、add は self
の所有権を受け取ることがわかります。これは、リスト8-18の s1 が add
呼び出しにムーブされ、その後はもう有効ではなくなることを意味します。したがって、
let s3 = s1 + &s2; は両方の文字列をコピーして新しい文字列を作るように見えますが、
実際にはこの文は s1 の所有権を受け取り、s2 の内容のコピーを末尾に追加し、
その後で結果の所有権を返します。つまり、多くのコピーを行っているように見えても、
実際にはそうではなく、この実装は単純にコピーするよりも効率的です。
複数の文字列を連結する必要がある場合、+ 演算子の振る舞いは
扱いにくくなります。
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
}
この時点で、s は tic-tac-toe になります。+ と " の文字がたくさんあるため、
何が起きているのかを見通しにくくなっています。より複雑な方法で文字列を
結合するには、代わりに format! マクロを使えます。
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
}
このコードでも、s は tic-tac-toe になります。format! マクロは
println! と同様に動作しますが、出力を画面に表示する代わりに、その内容を持つ
String を返します。format! を使うこの版のコードははるかに読みやすく、
また format! マクロが生成するコードは参照を使うため、
この呼び出しはそのどのパラメータの所有権も受け取りません。
文字列へのインデックスアクセス
他の多くのプログラミング言語では、文字列内の個々の文字に
インデックスで参照してアクセスすることは、有効で一般的な操作です。しかし、
Rust でインデックス構文を使って String の一部にアクセスしようとすると、
エラーになります。リスト8-19の無効なコードを見てください。
fn main() {
let s1 = String::from("hi");
let h = s1[0];
}
このコードは次のエラーになります:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> src/main.rs:3:16
|
3 | let h = s1[0];
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
= help: the following other types implement trait `SliceIndex<T>`:
`usize` implements `SliceIndex<ByteStr>`
`usize` implements `SliceIndex<[T]>`
= note: required for `String` to implement `Index<{integer}>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error
このエラーが物語っています。Rust の文字列はインデックスアクセスをサポートしていません。ですが、なぜでしょうか。 その疑問に答えるには、Rust が文字列をメモリにどのように格納しているかを説明する必要があります。
内部表現
String は Vec<u8> のラッパーです。リスト8-14にある、適切に
UTF-8 でエンコードされた文字列の例をいくつか見てみましょう。まずはこれです:
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
この場合、len は 4 になります。これは、文字列
"Hola" を格納しているベクタの長さが 4 バイトであることを意味します。これらの文字は
UTF-8 でエンコードすると、それぞれ 1 バイトを占めます。しかし、次の行は
驚くかもしれません(この文字列は数字の 3 ではなく、キリル文字の大文字 Ze で始まることに注意してください):
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
この文字列の長さはいくつかと聞かれたら、12 と答えるかもしれません。実際には、Rust の 答えは 24 です。これは、“Здравствуйте” を UTF-8 でエンコードするのに必要な バイト数です。なぜなら、その文字列内の各 Unicode スカラ値は 2 バイトの 領域を使うからです。したがって、文字列のバイト列へのインデックスは、常に有効な Unicode スカラ値に対応するとは限りません。これを示すために、次の無効な Rust コードを考えてみましょう:
let hello = "Здравствуйте";
let answer = &hello[0];
すでにわかっているとおり、answer は最初の文字である З にはなりません。UTF-8 で
エンコードすると、З の 1 バイト目は 208、2 バイト目は 151 なので、
answer は実際には 208 になるようにも思えます。しかし、208 はそれ単独では
有効な文字ではありません。この文字列の最初の文字を求めたときに 208 が返るのは、
おそらくユーザーの望む動作ではないでしょう。しかし、それが Rust が
バイトインデックス 0 に持っている唯一のデータです。文字列がラテン文字だけを含む場合でも、
一般にユーザーはバイト値が返されることを望みません。&"hi"[0] がバイト値を返す
有効なコードだとしたら、返るのは h ではなく 104 です。
したがって、その答えは、予期しない値を返して すぐには見つからないかもしれないバグを引き起こすことを避けるために、Rust はこのコードを そもそもコンパイルせず、開発プロセスの早い段階で誤解を防いでいるということです。
バイト、スカラ値、書記素クラスタ
UTF-8 に関するもうひとつのポイントは、Rust の観点から文字 列を見る方法には、実は重要なものが 3 つあるということです。すなわち、バイト列、 スカラ値、そして書記素クラスタ(私たちがいうところの 文字 に最も近いもの)です。
デーヴァナーガリー文字で書かれたヒンディー語の “नमस्ते” を見ると、それは
次のような u8 値のベクタとして格納されています:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
これは 18 バイトであり、コンピュータは最終的にこのようにデータを格納します。これらを
Unicode スカラ値として見ると、つまり Rust の char 型が表すものとして見ると、
そのバイト列は次のようになります:
['न', 'म', 'स', '्', 'त', 'े']
ここには 6 つの char 値がありますが、4 番目と 6 番目は文字ではありません。
それらは単独では意味をなさない発音区別符号です。最後に、これらを
書記素クラスタとして見ると、人がヒンディー語のその単語を構成する 4 つの文字と
呼ぶであろうものが得られます:
["न", "म", "स्", "ते"]
Rust は、コンピュータが格納する生の文字列データを解釈するためのさまざまな方法を提供しており、
そのデータがどの人間の言語で書かれているかにかかわらず、各プログラムが
必要とする解釈を選べるようになっています。
Rust で String にインデックスを使って文字を取得できない最後の理由は、
インデックス操作には常に定数時間
(O(1))が期待されるからです。しかし、String ではその性能を保証できません。
というのも、Rust は有効な文字がいくつあるかを判定するために、先頭から
指定されたインデックスまで内容をたどる必要があるからです。
文字列のスライス
文字列へのインデックス指定は、文字列インデックス操作の戻り値の型が何であるべきか、 つまりバイト値なのか、文字なのか、書記素クラスタなのか、それとも文字列スライスなのかが 明確ではないため、多くの場合あまりよい考えではありません。したがって、文字列スライスを 作るためにどうしてもインデックスを使う必要がある場合、Rust はより具体的に指定することを 求めます。
単一の数値で [] を使ってインデックス指定する代わりに、範囲を指定して [] を使うことで、
特定のバイトを含む文字列スライスを作成できます。
#![allow(unused)]
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
}
ここで、s は文字列の先頭 4 バイトを含む &str になります。
前に述べたように、これらの文字はそれぞれ 2 バイトなので、
s は Зд になります。
&hello[0..1] のように、文字を構成するバイトの一部だけをスライスしようとすると、
Rust は、ベクタで無効なインデックスにアクセスした場合と同じように、実行時に
パニックします。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/collections`
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
そのため、範囲を使って文字列スライスを作成するときは注意が必要です。 そうすると、プログラムがクラッシュする可能性があるからです。
文字列の反復処理
文字列の一部分を操作する最善の方法は、文字が欲しいのかバイトが欲しいのかを
明示することです。個々の Unicode スカラー値については、chars メソッドを使います。
“Зд” に対して chars を呼び出すと、型 char の 2 つの値に分けて返し、
結果を反復処理して各要素にアクセスできます。
#![allow(unused)]
fn main() {
for c in "Зд".chars() {
println!("{c}");
}
}
このコードは次のように出力します。
З
д
一方、bytes メソッドは各生バイトを返します。これは、ドメインによっては
適切かもしれません。
#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
println!("{b}");
}
}
このコードは、この文字列を構成する 4 バイトを出力します。
208
151
208
180
ただし、有効な Unicode スカラー値は 1 バイトより多くのバイトで構成されることがある、 という点を忘れないでください。
デーヴァナーガリー文字のように、文字列から書記素クラスタを取得するのは複雑なので、 この機能は標準ライブラリでは提供されていません。この機能が必要な場合は、 crates.io で利用できるクレートがあります。
文字列の複雑さへの対処
要約すると、文字列は複雑です。異なるプログラミング言語は、この複雑さを
プログラマにどう見せるかについて異なる選択をしています。Rust は、String
データを正しく扱うことをすべての Rust プログラムのデフォルトの動作にする、
という選択をしました。つまり、プログラマは UTF-8 データの扱いについて
あらかじめより多く考える必要があります。このトレードオフによって、文字列の
複雑さが他のプログラミング言語よりも表に出てきますが、その一方で、開発
ライフサイクルの後半で非 ASCII 文字に関するエラーに対処しなくて済むように
なります。
よい知らせは、標準ライブラリが、こうした複雑な状況を正しく扱うのに役立つ、
String 型と &str 型を基盤とした多くの機能を提供していることです。
文字列内を検索する contains や、文字列の一部を別の文字列で置き換える
replace のような便利なメソッドについて、ぜひドキュメントを確認してください。
では、もう少し複雑でないもの、ハッシュマップに移りましょう!