文字列でUTF-8でエンコードされたテキストを保持する
第4章で文字列について語りましたが、今度はより掘り下げていきましょう。新参者のRustaceanは、 3つの概念の組み合わせにより、文字列でよく行き詰まります: Rustのありうるエラーを晒す性質、 多くのプログラマが思っている以上に文字列が複雑なデータ構造であること、そしてUTF-8です。 これらの要因が、他のプログラミング言語から移ってきた場合、一見困難に見えるように絡み合うわけです。
コレクションの文脈で文字列を議論することは、有用なことです。なぜなら、文字列はテキストとして解釈された時に有用になる機能を提供するメソッドと、
バイトのコレクションで実装されているからです。この節では、生成、更新、読み込みのような全コレクションが持つString
の処理について語ります。
また、String
が他のコレクションと異なる点についても議論します。具体的には、人間とコンピュータがString
データを解釈する方法の差異により、
String
に添え字アクセスする方法がどう複雑なのかということです。
文字列とは?
まずは、文字列という用語の意味を定義しましょう。Rustには、言語の核として1種類しか文字列型が存在しません。
文字列スライスのstr
で、通常借用された形態&str
で見かけます。第4章で、文字列スライスについて語りました。
これは、別の場所に格納されたUTF-8エンコードされた文字列データへの参照です。例えば、文字列リテラルは、
プログラムのバイナリ出力に格納されるので、文字列スライスになります。
String
型は、言語の核として組み込まれるのではなく、Rustの標準ライブラリで提供されますが、伸長可能、
可変、所有権のあるUTF-8エンコードされた文字列型です。RustaceanがRustにおいて「文字列」を指したら、
どちらかではなく、String
と文字列スライスの&str
のことを通常意味します。この節は、大方、
String
についてですが、どちらの型もRustの標準ライブラリで重宝されており、
どちらもUTF-8エンコードされています。
また、Rustの標準ライブラリには、他の文字列型も含まれています。OsString
、OsStr
、CString
、CStr
などです。
ライブラリクレートにより、文字列データを格納する選択肢はさらに増えます。
それらの名前が全てString
かStr
で終わっているのがわかりますか?所有権ありと借用されたバージョンを指しているのです。
ちょうど以前見かけたString
と&str
のようですね。例えば、これらの文字列型は、異なるエンコード方法でテキストを格納していたり、
メモリ上の表現が異なったりします。この章では、これらの他の種類の文字列については議論しません;
使用方法やどれが最適かについては、APIドキュメントを参照してください。
新規文字列を生成する
Vec<T>
で使用可能な処理の多くがString
でも使用できます。文字列を生成するnew
関数から始めましょうか。
リスト8-11に示したようにですね。
#![allow(unused)] fn main() { let mut s = String::new(); }
この行は、新しい空のs
という文字列を生成しています。それからここにデータを読み込むことができるわけです。
だいたい、文字列の初期値を決めるデータがあるでしょう。そのために、to_string
メソッドを使用します。
このメソッドは、文字列リテラルのように、Display
トレイトを実装する型ならなんでも使用できます。
リスト8-12に2例、示しています。
#![allow(unused)] 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::from
関数を使っても、文字列リテラルからString
を生成することができます。
リスト8-13のコードは、to_string
を使用するリスト8-12のコードと等価です。
#![allow(unused)] fn main() { let s = String::from("initial contents"); }
文字列は、非常に多くのものに使用されるので、多くの異なる一般的なAPIを使用でき、たくさんの選択肢があるわけです。
冗長に思われるものもありますが、適材適所です!今回の場合、String::from
とto_string
は全く同じことをします。
従って、どちらを選ぶかは、スタイル次第です。
文字列はUTF-8エンコードされていることを覚えていますか?要するに文字列には、適切にエンコードされていればどんなものでも含めます。 リスト8-14に示したように。
#![allow(unused)] 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の通りです。
#![allow(unused)] fn main() { let mut s = String::from("foo"); s.push_str("bar"); }
この2行の後、s
はfoobar
を含むことになります。push_str
メソッドは、必ずしも引数の所有権を得なくていいので、
文字列スライスを取ります。例えば、リスト8-16のコードは、中身をs1
に追加した後、
s2
を使えなかったら残念だということを示しています。
#![allow(unused)] fn main() { let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2); }
もし、push_str
メソッドがs2
の所有権を奪っていたら、最後の行でその値を出力することは不可能でしょう。
ところが、このコードは予想通りに動きます!
push
メソッドは、1文字を引数として取り、String
に追加します。リスト8-15は、
push
メソッドでlをString
に追加するコードを呈示しています。
#![allow(unused)] fn main() { let mut s = String::from("lo"); s.push('l'); }
このコードの結果、s
はlol
を含むことになるでしょう。
編者注:
lol
はlaughing out loud
(大笑いする)の頭文字からできたスラングです。 日本語のwww
みたいなものですね。
+
演算子、またはformat!
マクロで連結
2つのすでにある文字列を組み合わせたくなることがよくあります。リスト8-18に示したように、
一つ目の方法は、+
演算子を使用することです。
#![allow(unused)] fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // s1はムーブされ、もう使用できないことに注意 }
このコードの結果、s3
という文字列は、Hello, world!
を含むことになるでしょう。
追記の後、s1
がもう有効でなくなった理由と、s2
への参照を使用した理由は、
+
演算子を使用した時に呼ばれるメソッドのシグニチャと関係があります。+
演算子は、add
メソッドを使用し、
そのシグニチャは以下のような感じです:
fn add(self, s: &str) -> String {
これは、標準ライブラリにあるシグニチャそのものではありません: 標準ライブラリでは、add
はジェネリクスで定義されています。
ここでは、ジェネリックな型を具体的な型に置き換えたadd
のシグニチャを見ており、これは、
このメソッドをString
値とともに呼び出した時に起こることです。ジェネリクスについては、第10章で議論します。
このシグニチャが、+
演算子の巧妙な部分を理解するのに必要な手がかりになるのです。
まず、s2
には&
がついてます。つまり、add
関数のs
引数のために最初の文字列に2番目の文字列の参照を追加するということです:
String
には&str
を追加することしかできません。要するに2つのString
値を追加することはできないのです。
でも待ってください。add
の第2引数で指定されているように、&s2
の型は、&str
ではなく、
&String
ではないですか。では、なぜ、リスト8-18は、コンパイルできるのでしょうか?
add
呼び出しで&s2
を使える理由は、コンパイラが&String
引数を&str
に型強制してくれるためです。
add
メソッド呼び出しの際、コンパイラは、参照外し型強制というものを使用し、ここでは、
&s2
を&s2[..]
に変えるものと考えることができます。参照外し型強制について詳しくは、第15章で議論します。
add
がs
引数の所有権を奪わないので、この処理後もs2
が有効なString
になるわけです。
2番目に、シグニチャからadd
はself
の所有権をもらうことがわかります。self
には&
がついていないからです。
これはつまり、リスト8-18においてs1
はadd
呼び出しにムーブされ、その後は有効ではなくなるということです。
故に、s3 = s1 + &s2;
は両文字列をコピーして新しいものを作るように見えますが、
この文は実際にはs1
の所有権を奪い、s2
の中身のコピーを追記し、結果の所有権を返すのです。言い換えると、
たくさんのコピーをしているように見えますが、違います; 実装は、コピーよりも効率的です。
複数の文字列を連結する必要が出ると、+
演算子の振る舞いは扱いにくくなります:
#![allow(unused)] 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!
マクロを使用することができます:
#![allow(unused)] 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!
を使用したコードの方がはるかに読みやすく、
引数の所有権を奪いません。
文字列に添え字アクセスする
他の多くのプログラミング言語では、文字列中の文字に、添え字で参照してアクセスすることは、有効なコードであり、
一般的な処理です。しかしながら、Rustにおいて、添え字記法でString
の一部にアクセスしようとすると、
エラーが発生するでしょう。リスト8-19の非合法なコードを考えてください。
let s1 = String::from("hello");
let h = s1[0];
このコードは、以下のようなエラーに落ち着きます:
error[E0277]: the trait bound `std::string::String: std::ops::Index<{Integer}>` is not satisfied
(エラー: トレイト境界`std::string::String: std::ops::Index<{Integer}>`が満たされていません)
|>
3 |> let h = s1[0];
|> ^^^^^ the type `std::string::String` cannot be indexed by `{Integer}`
|> (型`std::string::String`は`{Integer}`で添え字アクセスできません)
= help: the trait `std::ops::Index<{Integer}>` is not implemented for `std::string::String`
(ヘルプ: `std::ops::Index<{Integer}>`というトレイトが`std::string::String`に対して実装されていません)
エラーと注釈が全てを物語っています: Rustの文字列は、添え字アクセスをサポートしていないのです。 でも、なぜでしょうか?その疑問に答えるには、Rustがメモリにどのように文字列を保持しているかについて議論する必要があります。
内部表現
String
はVec<u8>
のラッパです。リスト8-14から適切にUTF-8でエンコードされた文字列の例をご覧ください。
まずは、これ:
#![allow(unused)] fn main() { let len = String::from("Hola").len(); }
この場合、len
は4になり、これは、文字列"Hola"を保持するベクタの長さが4バイトであることを意味します。
これらの各文字は、UTF-8でエンコードすると、1バイトになるのです。しかし、以下の行ではどうでしょうか?
(この文字列は大文字のキリル文字Zeで始まり、アラビア数字の3では始まっていないことに注意してください)
#![allow(unused)] fn main() { let len = String::from("Здравствуйте").len(); }
文字列の長さはと問われたら、あなたは12と答えるかもしれません。ところが、Rustの答えは、24です: “Здравствуйте”をUTF-8でエンコードすると、この長さになります。各Unicodeスカラー値は、2バイトの領域を取るからです。 それ故に、文字列のバイトの添え字は、必ずしも有効なUnicodeのスカラー値とは相互に関係しないのです。 デモ用に、こんな非合法なRustコードを考えてください:
let hello = "Здравствуйте";
let answer = &hello[0];
answer
の値は何になるべきでしょうか?最初の文字のЗ
になるべきでしょうか?UTF-8エンコードされた時、
З
の最初のバイトは208
、2番目は151
になるので、answer
は実際、208
になるべきですが、
208
は単独では有効な文字ではありません。この文字列の最初の文字を求めている場合、208
を返すことは、
ユーザの望んでいるものではないでしょう; しかしながら、Rustには、バイト添え字0の位置には、そのデータしかないのです。
文字列がラテン文字のみを含む場合でも、ユーザは一般的にバイト値が返ることを望みません:
&"hello"[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
でそのパフォーマンスを保証することはできません。というのも、
合法な文字がいくつあるか決定するのに、最初から添え字まで中身を走査する必要があるからです。
文字列をスライスする
文字列に添え字アクセスするのは、しばしば悪い考えです。文字列添え字処理の戻り値の型が明瞭ではないからです:
バイト値、文字、書記素クラスタ、あるいは文字列スライスにもなります。故に、文字列スライスを生成するのに、
添え字を使う必要が本当に出た場合にコンパイラは、もっと特定するよう求めてきます。添え字アクセスを特定し、
文字列スライスが欲しいと示唆するためには、[]
で1つの数値により添え字アクセスするのではなく、
範囲とともに[]
を使って、特定のバイトを含む文字列スライスを作ることができます:
#![allow(unused)] fn main() { let hello = "Здравствуйте"; let s = &hello[0..4]; }
ここで、s
は文字列の最初の4バイトを含む&str
になります。先ほど、これらの文字は各々2バイトになると指摘しましたから、
s
はЗд
になります。
&hello[0..1]
と使用したら、何が起きるでしょうか?答え: Rustはベクタの非合法な添え字にアクセスしたかのように、
実行時にパニックするでしょう:
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4
('main'スレッドは「バイト添え字1は文字の境界ではありません; `Здравствуйте`の'З'(バイト番号0から2)の中です」でパニックしました)
範囲を使用して文字列スライスを作る際にはプログラムをクラッシュさせることがあるので、気をつけるべきです。
文字列を走査するメソッド群
幸いなことに、他の方法でも文字列の要素にアクセスすることができます。
もし、個々のUnicodeスカラー値に対して処理を行う必要があったら、最適な方法はchars
メソッドを使用するものです。
“नमस्ते”に対してchars
を呼び出したら、分解して6つのchar
型の値を返すので、各要素にアクセスするには、
その結果を走査すればいいわけです:
#![allow(unused)] fn main() { for c in "नमस्ते".chars() { println!("{}", c); } }
このコードは、以下のように出力します:
न
म
स
्
त
े
bytes
メソッドは、各バイトをそのまま返すので、最適になることもあるかもしれません:
#![allow(unused)] fn main() { for b in "नमस्ते".bytes() { println!("{}", b); } }
このコードは、String
をなす18バイトを出力します:
224
164
// --snip--
165
135
ですが、合法なUnicodeスカラー値は、2バイト以上からなる場合もあることは心得ておいてください。
書記素クラスタを文字列から得る方法は複雑なので、この機能は標準ライブラリでは提供されていません。 この機能が必要なら、crates.ioでクレートを入手可能です。
文字列はそう単純じゃない
まとめると、文字列は込み入っています。プログラミング言語ごとにこの複雑性をプログラマに提示する方法は違います。
Rustでは、String
データを正しく扱うことが、全てのRustプログラムにとっての既定動作になっているわけであり、
これは、プログラマがUTF-8データを素直に扱う際に、よりしっかり考えないといけないことを意味します。
このトレードオフにより、他のプログラミング言語で見えるよりも文字列の複雑性がより露出していますが、
ASCII以外の文字に関するエラーを開発の後半で扱わなければならない可能性が排除されているのです。
もう少し複雑でないものに切り替えていきましょう: ハッシュマップです!