文字列で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の標準ライブラリには、他の文字列型も含まれています。OsStringOsStrCStringCStrなどです。 ライブラリクレートにより、文字列データを格納する選択肢はさらに増えます。 それらの名前が全てStringStrで終わっているのがわかりますか?所有権ありと借用されたバージョンを指しているのです。 ちょうど以前見かけたString&strのようですね。例えば、これらの文字列型は、異なるエンコード方法でテキストを格納していたり、 メモリ上の表現が異なったりします。この章では、これらの他の種類の文字列については議論しません; 使用方法やどれが最適かについては、APIドキュメントを参照してください。

新規文字列を生成する

Vec<T>で使用可能な処理の多くがStringでも使用できます。文字列を生成するnew関数から始めましょうか。 リスト8-11に示したようにですね。


#![allow(unused)]
fn main() {
let mut s = String::new();
}

リスト8-11: 新しい空のStringを生成する

この行は、新しい空の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();
}

リスト8-12: to_stringメソッドを使用して文字列リテラルからStringを生成する

このコードは、initial contents(初期値)を含む文字列を生成します。

さらに、String::from関数を使っても、文字列リテラルからStringを生成することができます。 リスト8-13のコードは、to_stringを使用するリスト8-12のコードと等価です。


#![allow(unused)]
fn main() {
let s = String::from("initial contents");
}

リスト8-13: String::from関数を使って文字列リテラルからStringを作る

文字列は、非常に多くのものに使用されるので、多くの異なる一般的なAPIを使用でき、たくさんの選択肢があるわけです。 冗長に思われるものもありますが、適材適所です!今回の場合、String::fromto_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");
}

リスト8-14: いろんな言語の挨拶を文字列に保持する

これらは全て、有効なStringの値です。

文字列を更新する

Stringは、サイズを伸ばすことができ、Vec<T>の中身のように、追加のデータをプッシュすれば、中身も変化します。 付け加えると、String値を連結する+演算子や、format!マクロを便利に使用することができます。

push_strpushで文字列に追加する

push_strメソッドで文字列スライスを追記することで、Stringを伸ばすことができます。 リスト8-15の通りです。


#![allow(unused)]
fn main() {
let mut s = String::from("foo");
s.push_str("bar");
}

リスト8-15: push_strメソッドでStringに文字列スライスを追記する

この2行の後、sfoobarを含むことになります。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);
}

リスト8-16: 中身をStringに追加した後に、文字列スライスを使用する

もし、push_strメソッドがs2の所有権を奪っていたら、最後の行でその値を出力することは不可能でしょう。 ところが、このコードは予想通りに動きます!

pushメソッドは、1文字を引数として取り、Stringに追加します。リスト8-15は、 pushメソッドでlStringに追加するコードを呈示しています。


#![allow(unused)]
fn main() {
let mut s = String::from("lo");
s.push('l');
}

リスト8-17: pushString値に1文字を追加する

このコードの結果、slolを含むことになるでしょう。

編者注: lollaughing 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はムーブされ、もう使用できないことに注意
}

リスト8-18: +演算子を使用して二つのString値を新しいString値にする

このコードの結果、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章で議論します。 adds引数の所有権を奪わないので、この処理後もs2が有効なStringになるわけです。

2番目に、シグニチャからaddselfの所有権をもらうことがわかります。selfには&がついていないからです。 これはつまり、リスト8-18においてs1add呼び出しにムーブされ、その後は有効ではなくなるということです。 故に、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;
}

ここで、stic-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);
}

このコードでも、stic-tac-toeになります。format!マクロは、println!と同様の動作をしますが、 出力をスクリーンに行う代わりに、中身をStringで返すのです。format!を使用したコードの方がはるかに読みやすく、 引数の所有権を奪いません。

文字列に添え字アクセスする

他の多くのプログラミング言語では、文字列中の文字に、添え字で参照してアクセスすることは、有効なコードであり、 一般的な処理です。しかしながら、Rustにおいて、添え字記法でStringの一部にアクセスしようとすると、 エラーが発生するでしょう。リスト8-19の非合法なコードを考えてください。

let s1 = String::from("hello");
let h = s1[0];

リスト8-19: 文字列に対して添え字記法を試みる

このコードは、以下のようなエラーに落ち着きます:

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がメモリにどのように文字列を保持しているかについて議論する必要があります。

内部表現

StringVec<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以外の文字に関するエラーを開発の後半で扱わなければならない可能性が排除されているのです。

もう少し複雑でないものに切り替えていきましょう: ハッシュマップです!

関連キーワード:  let, from, リスト, バイト, コード, メソッド, 文字, allow, データ, スライス