文字列

文字列は、プログラマがマスタすべき重要な概念です。 Rustの文字列の扱いは、Rust言語がシステムプログラミングにフォーカスしているため、少し他の言語と異なります。 動的なサイズを持つデータ構造があるといつも、物事は複雑性を孕みます。 そして文字列もまたサイズを変更することができるデータ構造です。 これはつまり、Rustの文字列もまた、Cのような他のシステム言語とは少し異なる振る舞いをするということです。

詳しく見ていきましょう。「文字列」は、UTF-8のバイトストリームとしてエンコードされたユニコードのスカラ値のシーケンスです。 すべての文字列は、妥当なUTF-8のシーケンスであることが保証されています。 また、他のシステム言語とは異なり、文字列はnull終端でなく、nullバイトを保持することもできます。

Rustには主要な文字列型が二種類あります。&strStringです。 まず &str について説明しましょう。 &str は「文字列スライス」と呼ばれます。 文字列スライスは固定サイズで変更不可能です。文字列スライスはUTF-8のバイトシーケンスへの参照です。

fn main() { let greeting = "Hello there."; // greeting: &'static str }
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);

もう一つは \ を使って空白と改行を削る形式です:

fn main() { let s = "foo\ bar"; assert_eq!("foobar", s); }
let s = "foo\
    bar";

assert_eq!("foobar", s);

Rustには &str だけでなく、 String というヒープアロケートされる文字列もあります。 この文字列は伸張可能であり、またUTF-8であることも保証されています。 String は一般的に文字列スライスを to_string メソッドで変換することで作成されます。

fn main() { let mut s = "Hello".to_string(); // mut s: String println!("{}", s); s.push_str(", world."); println!("{}", s); }
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); }
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&* を用いて明示的に変換しなければなりません。

fn main() { use std::net::TcpStream; // TcpStream::connect("192.168.0.1:3000"); // &str parameter TcpStream::connect("192.168.0.1:3000"); // 引数として &str を渡す let addr_string = "192.168.0.1:3000".to_string(); // TcpStream::connect(&*addr_string); // convert addr_string to &str TcpStream::connect(&*addr_string); // addr_string を &str に変換して渡す }
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 として見るコストは低いのですが、&strString に変換するとメモリアロケーションが発生します。 必要がなければ、やるべきではないでしょう!

インデクシング

文字列は妥当な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での定義と厳密には一致しません。 文字列をバイト列として見るかコードポイント列として見るか選ぶことができます。

fn main() { let hachiko = "忠犬ハチ公"; for b in hachiko.as_bytes() { print!("{}, ", b); } println!(""); for c in hachiko.chars() { print!("{}, ", c); } println!(""); }
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 を末尾に連結することができます:

fn main() { let hello = "Hello ".to_string(); let world = "world!"; let hello_world = hello + world; }
let hello = "Hello ".to_string();
let world = "world!";

let hello_world = hello + world;

しかし、2つの String を連結するには、 & が必要になります:

fn main() { let hello = "Hello ".to_string(); let world = "world!".to_string(); let hello_world = hello + &world; }
let hello = "Hello ".to_string();
let world = "world!".to_string();

let hello_world = hello + &world;

これは、 &String が自動的に &str に型強制されるためです。 このフィーチャは 「 Deref による型強制 」と呼ばれています。