スライス型
スライス を使うと、コレクション 内の要素の連続した並びを参照できます。スライスは参照の一種なので、所有権を持ちません。
ここで、小さなプログラミング上の問題を考えてみましょう。空白で区切られた単語からなる文字列を受け取り、その文字列の中で最初に見つかった単語を返す関数を書いてください。関数が文字列内に空白を見つけられない場合、その文字列全体が 1 つの単語でなければならないので、文字列全体を返すべきです。
注: スライスを導入する目的上、この節では ASCII のみを前提にしています。UTF-8 の扱いについてのより詳しい説明は、第 8 章の 「文字列で UTF-8 エンコードされたテキストを保持する」 節にあります。
スライスが解決する問題を理解するために、まずはスライスを使わずにこの関数のシグネチャをどのように書くかを順に見ていきましょう。
fn first_word(s: &String) -> ?
first_word 関数は、型 &String の引数を持っています。所有権は必要ないので、これは問題ありません。(Rust の慣用的な書き方では、必要でない限り関数は引数の所有権を受け取りません。その理由は、この先を読み進めるにつれて明らかになります。)しかし、何を返せばよいのでしょうか。文字列の 一部 について語る方法は、実のところありません。しかし、空白によって示される単語の終端のインデックスを返すことはできます。リスト 4-7 に示すように、それを試してみましょう。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
String を要素ごとにたどって、その値が空白かどうかを調べる必要があるため、as_bytes メソッドを使って String をバイト配列に変換します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
次に、iter メソッドを使ってそのバイト配列に対するイテレータを作成します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
イテレータについては 第 13 章 でより詳しく説明します。今のところは、iter はコレクション内の各要素を返すメソッドであり、enumerate は iter の結果をラップして、各要素をタプルの一部として返すものだと知っておいてください。enumerate から返されるタプルの最初の要素はインデックスで、2 番目の要素はその要素への参照です。これは、自分でインデックスを計算するより少し便利です。
enumerate メソッドはタプルを返すので、そのタプルを分解するためにパターンを使えます。パターンについては 第 6 章 でさらに説明します。for ループでは、タプル中のインデックスに i、単一バイトに &item を対応させるパターンを指定しています。.iter().enumerate() から得られるのは要素への参照なので、パターン内で & を使います。
for ループの中では、バイトリテラル構文を使って空白を表すバイトを探します。空白が見つかったら、その位置を返します。そうでなければ、s.len() を使って文字列の長さを返します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
これで、文字列内の最初の単語の終端のインデックスを見つける方法は手に入りました。しかし、問題があります。usize を単独で返していますが、それは &String という文脈でのみ意味を持つ数値です。言い換えると、String とは別の値であるため、将来にわたってそれが有効であり続ける保証はありません。リスト 4-7 の first_word 関数を使う、リスト 4-8 のプログラムを考えてみてください。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties the String, making it equal to ""
// word still has the value 5 here, but s no longer has any content that we
// could meaningfully use with the value 5, so word is now totally invalid!
}
このプログラムはエラーなしでコンパイルされ、s.clear() を呼び出した後に word を使ったとしても同様です。word は s の状態とまったく結び付いていないため、word には依然として値 5 が入っています。その値 5 を変数 s と組み合わせて最初の単語を取り出そうとすることもできてしまいますが、word に 5 を保存してから s の内容が変わっているので、これはバグになります。
word のインデックスが s のデータとずれてしまわないように気を配らなければならないのは、面倒でエラーが起きやすいことです。second_word 関数を書くなら、このようなインデックスの管理はさらに脆くなります。そのシグネチャは次のようになるでしょう。
fn second_word(s: &String) -> (usize, usize) {
今度は開始インデックス と 終了インデックスを追跡することになり、特定の状態のデータから計算されたにもかかわらず、その状態とはまったく結び付いていない値がさらに増えます。同期を保つ必要がある、互いに無関係な 3 つの変数がばらばらに存在することになります。
幸いなことに、Rust にはこの問題に対する解決策があります。文字列スライスです。
文字列スライス
文字列スライス は String の要素の連続した並びへの参照で、次のようになります。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
String 全体への参照ではなく、hello は String の一部への参照であり、その範囲は追加の [0..5] の部分で指定されています。スライスは、角括弧内に範囲を指定することで作成します。[starting_index..ending_index] のように書きます。ここで、starting_index はスライス内の最初の位置であり、ending_index はスライス内の最後の位置の 1 つ後です。内部的には、スライスのデータ構造は開始位置とスライスの長さを保持しており、その長さは ending_index から starting_index を引いた値に対応します。したがって、let world = &s[6..11]; の場合、world は s のインデックス 6 のバイトへのポインタと、長さの値 5 を持つスライスになります。
図 4-7 はこれを図で示しています。
図 4-7: String の一部を参照する文字列スライス
Rust の .. 範囲構文では、インデックス 0 から始めたい場合、2 つのピリオドの前の値を省略できます。言い換えると、これらは同じです。
#![allow(unused)]
fn main() {
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
}
同様に、スライスに String の最後のバイトが含まれている場合は、末尾の数字を省略できます。つまり、これらは同じです。
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
}
文字列全体のスライスを取得するために、両方の値を省略することもできます。したがって、これらは同じです。
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
}
注: 文字列スライスの範囲インデックスは、有効な UTF-8 文字境界上になければなりません。 マルチバイト文字の途中で文字列スライスを作成しようとすると、 プログラムはエラーで終了します。
ここまでの情報を踏まえて、first_word を書き直し、スライスを返すようにしましょう。「文字列スライス」を表す型は &str と書きます:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {}
単語の終端のインデックスは、リスト 4-7 と同じように、最初に現れる空白を探すことで取得します。空白が見つかったら、文字列の先頭とその空白のインデックスを開始・終了インデックスとして使い、文字列スライスを返します。
これで first_word を呼び出すと、基になるデータに結び付いた単一の値が返ってきます。その値は、スライスの開始位置への参照と、スライス内の要素数で構成されます。
スライスを返す方法は、second_word 関数にも同様に使えます:
fn second_word(s: &String) -> &str {
これで、String 内への参照が有効なままであることをコンパイラが保証してくれるため、ずっと扱いやすく、間違えにくい API になりました。リスト 4-8 のプログラムで、最初の単語の終端インデックスを取得したあとに文字列を空にしてしまい、その結果インデックスが無効になったバグを覚えていますか? あのコードは論理的には誤っていましたが、すぐには何のエラーも示しませんでした。問題が表面化するのは、空にした文字列に対して最初の単語のインデックスを使い続けようとした、もっと後になってからです。スライスを使えばこのバグは起こりえず、コードに問題があることもずっと早い段階で分かります。first_word のスライス版を使うと、コンパイル時エラーが発生します:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {word}");
}
コンパイラエラーは次のとおりです:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {word}");
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
借用の規則から、何かへの不変参照がある場合、その対象への可変参照を同時に取得することはできない、と説明しました。clear は String を切り詰める必要があるため、可変参照を取得する必要があります。clear の呼び出し後にある println! は word に入っている参照を使うので、その時点では不変参照はまだ有効でなければなりません。Rust は、clear 内の可変参照と word 内の不変参照が同時に存在することを許さないため、コンパイルは失敗します。Rust は API を使いやすくしただけでなく、エラーの一群全体をコンパイル時に排除してくれたのです!
スライスとしての文字列リテラル
文字列リテラルがバイナリ内部に格納されることについてはすでに説明しました。スライスが分かった今なら、文字列リテラルを正しく理解できます:
#![allow(unused)]
fn main() {
let s = "Hello, world!";
}
ここでの s の型は &str です。これは、バイナリ内のその特定の位置を指すスライスです。文字列リテラルが不変である理由もこれです。&str は不変参照だからです。
引数としての文字列スライス
リテラルと String 値のどちらからもスライスを取れると分かったので、first_word にはもう 1 つ改善できる点があります。それがシグネチャです:
fn first_word(s: &String) -> &str {
経験を積んだ Rustacean なら、代わりにリスト 4-9 に示したシグネチャを書くでしょう。そうすれば、同じ関数を &String 値にも &str 値にも使えるからです。
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
文字列スライスがあるなら、それをそのまま渡せます。String があるなら、String のスライスか String への参照を渡せます。この柔軟性は Deref 型強制を活用したものです。これは第 15 章の 「関数とメソッドで Deref 型強制を使う」 節で扱います。
関数が String への参照ではなく文字列スライスを受け取るように定義すると、機能を何も失うことなく、API をより汎用的で便利なものにできます:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
その他のスライス
想像どおり、文字列スライスは文字列に特化したものです。しかし、もっと一般的なスライス型もあります。次の配列を考えてみましょう:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}
文字列の一部を参照したいのと同じように、配列の一部を参照したくなることもあります。その場合は次のようにします:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}
このスライスの型は &[i32] です。これは文字列スライスと同じように、最初の要素への参照と長さを保持することで機能します。この種のスライスは、ほかのさまざまなコレクションでも使います。こうしたコレクションについては、第 8 章でベクタを扱うときに詳しく説明します。
まとめ
所有権、借用、そしてスライスという概念により、Rust プログラムではコンパイル時にメモリ安全性が保証されます。Rust 言語は、ほかのシステムプログラミング言語と同じように、メモリ使用を自分で制御する力を与えてくれます。しかし、データの所有者がスコープを外れたときに、そのデータが自動的にクリーンアップされるため、この制御を得るための余分なコードを書いたりデバッグしたりする必要はありません。
所有権は Rust のほかの多くの部分の動作にも影響するので、この本の残りを通して、これらの概念についてさらに取り上げていきます。第 5 章に進み、struct で複数のデータをひとまとめにする方法を見ていきましょう。