文字列

文字列は、プログラマがマスタすべき重要なコンセプトです。 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 型を持ちます。 文字列リテラルは、静的にアロケートされた文字列のスライスであり、これはつまりコンパイルされたプログラム中に保存されており、 プログラムの実行中全てにわたって存在していることを意味しています。 どの文字列のスライスを引数として期待している関数も、文字列リテラルを引数に取ることができます。

文字列リテラルは複数行に渡ることができます。 複数行の文字列リテラルを記述する方法は、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番の文字を探すためには文字列上を移動していく必要があります。 そのような作業はとても高コストな演算であり、誤解してはならない点です。 さらに言えば、「文字」というものは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 による型強制 」と呼ばれています。