注意: 最新版のドキュメントをご覧ください。この第1版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。
文字列は、プログラマがマスタすべき重要な概念です。 Rustの文字列の扱いは、Rust言語がシステムプログラミングにフォーカスしているため、少し他の言語と異なります。 動的なサイズを持つデータ構造があるといつも、物事は複雑性を孕みます。 そして文字列もまたサイズを変更することができるデータ構造です。 これはつまり、Rustの文字列もまた、Cのような他のシステム言語とは少し異なる振る舞いをするということです。
詳しく見ていきましょう。「文字列」は、UTF-8のバイトストリームとしてエンコードされたユニコードのスカラ値のシーケンスです。 すべての文字列は、妥当なUTF-8のシーケンスであることが保証されています。 また、他のシステム言語とは異なり、文字列はnull終端でなく、nullバイトを保持することもできます。
Rustには主要な文字列型が二種類あります。&str
と String
です。
まず &str
について説明しましょう。 &str
は「文字列スライス」と呼ばれます。
文字列スライスは固定サイズで変更不可能です。文字列スライスはUTF-8のバイトシーケンスへの参照です。
let greeting = "Hello there."; // greeting: &'static str
"Hello there."
は文字列リテラルで、 &'static str
型を持ちます。
文字列リテラルは、静的にアロケートされた文字列スライスです。これはつまりコンパイルされたプログラム内に保存されていて、
プログラムの実行中全てにわたって存在しているということです。
greeting
の束縛はこのように静的にアロケートされた文字列を参照しています。
文字列スライスを引数として期待している関数はすべて文字列リテラルを引数に取ることができます。
文字列リテラルは複数行にわたることができます。 複数行文字列リテラルには2つの形式があります。 一つ目の形式は、改行と行頭の空白を含む形式です:
fn main() { let s = "foo bar"; assert_eq!("foo\n bar", s); }let s = "foo bar"; assert_eq!("foo\n bar", s);
もう一つは \
を使って空白と改行を削る形式です:
let s = "foo\ bar"; assert_eq!("foobar", s);
通常、str
には直接アクセスできず、 &str
経由でのみアクセス出来ることに注意して下さい。
これは str
がサイズ不定型であり追加の実行時情報がないと使用できないからです。
詳しくはサイズ不定型の章を読んで下さい。
Rustには &str
だけでなく、 String
というヒープアロケートされる文字列もあります。
この文字列は伸張可能であり、またUTF-8であることも保証されています。
String
は一般的に文字列スライスを to_string
メソッドで変換することで作成されます。
let mut s = "Hello".to_string(); // mut s: String println!("{}", s); s.push_str(", world."); println!("{}", s);
String
は &
によって &str
に型強制されます。
fn takes_slice(slice: &str) { println!("Got: {}", slice); } fn main() { let s = "Hello".to_string(); takes_slice(&s); }
このような変換は &str
ではなく &str
の実装するトレイトを引数として取る関数に対しては自動的には行われません。
たとえば、 TcpStream::connect
は引数として型 ToSocketAddrs
を要求しています。
このような関数には &str
は渡せますが、 String
は &*
を用いて明示的に変換しなければなりません。
use std::net::TcpStream; TcpStream::connect("192.168.0.1:3000"); // 引数として &str を渡す let addr_string = "192.168.0.1:3000".to_string(); TcpStream::connect(&*addr_string); // addr_string を &str に変換して渡す
String
を &str
として見るコストは低いのですが、&str
を String
に変換するとメモリアロケーションが発生します。
必要がなければ、やるべきではないでしょう!
文字列は妥当なUTF-8であるため、文字列はインデクシングをサポートしていません:
fn main() { let s = "hello"; // println!("The first letter of s is {}", s[0]); // ERROR!!! println!("The first letter of s is {}", s[0]); // エラー!!! }let s = "hello"; println!("The first letter of s is {}", s[0]); // エラー!!!
普通、ベクタへの []
を用いたアクセスはとても高速です。
しかし、UTF-8でエンコードされた文字列内の文字は複数のバイト対応することがあるため、
文字列のn番目の文字を探すには文字列上を走査していく必要があります。
そのような処理はベクタのアクセスに比べると非常に高コストな演算であり、誤解を招きたくなかったのです。
さらに言えば、上の「文字 (letter)」というのはUnicodeでの定義と厳密には一致しません。
文字列をバイト列として見るかコードポイント列として見るか選ぶことができます。
let hachiko = "忠犬ハチ公"; for b in hachiko.as_bytes() { print!("{}, ", b); } println!(""); for c in hachiko.chars() { print!("{}, ", c); } println!("");
これは、以下の様な出力をします:
229, 191, 160, 231, 138, 172, 227, 131, 143, 227, 131, 129, 229, 133, 172,
忠, 犬, ハ, チ, 公,
ご覧のように、 char
の数よりも多くのバイトが含まれています。
インデクシングするのと近い結果を以下の様にして得ることができます:
fn main() { let hachiko = "忠犬ハチ公"; let dog = hachiko.chars().nth(1); // hachiko[1]のような感じで }let dog = hachiko.chars().nth(1); // hachiko[1]のような感じで
このコードは、chars
のリストの上を先頭から走査しなければならないことを強調しています。
文字列スライスは以下のようにスライス構文を使って取得することができます:
fn main() { let dog = "hachiko"; let hachi = &dog[0..5]; }let dog = "hachiko"; let hachi = &dog[0..5];
しかし、注意しなくてはならない点はこれらのオフセットは バイト であって 文字 のオフセットではないという点です。 そのため、以下のコードは実行時に失敗します:
fn main() { let dog = "忠犬ハチ公"; let hachi = &dog[0..2]; }let dog = "忠犬ハチ公"; let hachi = &dog[0..2];
そして、次のようなエラーが発生します:
thread '<main>' panicked at 'index 0 and/or 2 in `忠犬ハチ公` do not lie on
character boundary'
String
が存在するとき、 &str
を末尾に連結することができます:
let hello = "Hello ".to_string(); let world = "world!"; let hello_world = hello + world;
しかし、2つの String
を連結するには、 &
が必要になります:
let hello = "Hello ".to_string(); let world = "world!".to_string(); let hello_world = hello + &world;
これは、 &String
が自動的に &str
に型強制されるためです。
このフィーチャは 「 Deref
による型強制 」と呼ばれています。