はじめに
スライス型へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、空白で区切られた単語の文字列を受け取り、その文字列で見つけた最初の単語を返す関数を書いてプログラミング問題を解きます。その後、インデックスを使って部分文字列を表す制限と、Rust での文字列スライスを使ったこの問題の解決策について説明します。
スライス型
スライスを使うと、コレクション全体ではなく、コレクション内の連続した要素のシーケンスを参照できます。スライスは一種の参照なので、所有権を持っていません。
以下は、小さなプログラミング問題です。空白で区切られた単語の文字列を受け取り、その文字列で見つけた最初の単語を返す関数を書きましょう。関数が文字列内に空白を見つけない場合、文字列全体が 1 つの単語なので、文字列全体を返す必要があります。
スライスを使わずにこの関数のシグネチャを書く方法を考えてみましょう。これにより、スライスが解決する問題を理解できます。
fn first_word(s: &String) ->?
first_word関数は、パラメータとして&Stringを持っています。所有権を必要としないので、これは問題ありません。しかし、何を返すべきでしょうか?文字列の一部を表す方法がありません。ただし、空白で示される単語の末尾のインデックスを返すことができます。これを試してみましょう。リスト 4-7 を参照してください。
ファイル名:src/main.rs
fn first_word(s: &String) -> usize {
1 let bytes = s.as_bytes();
for (2 i, &item) in 3 bytes.iter().enumerate() {
4 if item == b' ' {
return i;
}
}
5 s.len()
}
リスト 4-7: Stringパラメータに対してバイトインデックス値を返すfirst_word関数
Stringの要素を 1 つずつ調べ、値が空白かどうかを確認する必要があるため、as_bytesメソッドを使ってStringをバイト配列に変換します[1]。
次に、iterメソッドを使ってバイト配列のイテレータを作成します[3]。第 13 章でイテレータについて詳しく説明します。今のところ、iterはコレクション内の各要素を返すメソッドであり、enumerateはiterの結果をラップし、各要素をタプルの一部として返すことを知っておいてください。enumerateから返されるタプルの最初の要素はインデックスで、2 番目の要素は要素への参照です。これは、自分でインデックスを計算するよりも便利です。
enumerateメソッドはタプルを返すので、パターンを使ってそのタプルを分解できます。第 6 章でパターンについてもっと説明します。forループでは、タプル内のインデックス用のiと、タプル内の 1 バイト用の&itemを持つパターンを指定します[2]。.iter().enumerate()から要素への参照を取得するので、パターンで&を使います。
forループの中では、バイトリテラル構文を使って空白を表すバイトを探します[4]。空白を見つけた場合は、その位置を返します。それ以外の場合は、s.len()を使って文字列の長さを返します[5]。
これで、文字列内の最初の単語の末尾のインデックスを取得する方法ができましたが、問題があります。usizeを単独で返していますが、&Stringのコンテキストでのみ意味のある数値です。つまり、Stringとは別の値なので、将来も有効であることが保証されていません。リスト 4-7 のfirst_word関数を使ったリスト 4-8 のプログラムを見てみましょう。
// src/main.rs
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // wordには5が代入されます
s.clear(); // これでStringが空になり、""に等しくなります
// ここでもwordにはまだ5が入っていますが、5を意味のある形で使える文字列はもうありません。wordはもはや完全に無効です!
}
リスト 4-8: first_word関数を呼び出して結果を格納し、その後Stringの内容を変更する
このプログラムはエラーなくコンパイルされます。s.clear()を呼び出した後にwordを使っても同じことが起こります。wordはsの状態にまったく関連付けられていないので、wordにはまだ5の値が入っています。wordに保存された5を使って変数sから最初の単語を抽出しようとすると、バグになります。なぜなら、wordに5を保存してからsの内容が変更されているからです。
wordのインデックスがsのデータと同期しなくなることを心配するのは面倒くさく、エラーが発生しやすいです!second_word関数を書く場合、インデックスの管理はさらに脆弱になります。そのシグネチャはこのようになります。
fn second_word(s: &String) -> (usize, usize) {
これで、開始インデックスと終了インデックスの両方を追跡しています。そして、特定の状態のデータから計算された値がさらに増えましたが、その状態にまったく関連付けられていません。同期を保つ必要がある、無関係な 3 つの変数があります。
幸い、Rust にはこの問題の解決策があります。文字列スライスです。
文字列スライス
文字列スライスは、Stringの一部への参照で、次のようになります。
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
helloは、String全体への参照ではなく、Stringの一部への参照で、追加の[0..5]部分で指定されます。スライスは、角括弧内の範囲を使って[開始インデックス..終了インデックス]を指定することで作成されます。ここで、開始インデックスはスライスの最初の位置で、終了インデックスはスライスの最後の位置より 1 つ多い位置です。内部的には、スライスデータ構造は開始位置とスライスの長さを保存します。これは、終了インデックスから開始インデックスを引いた値に相当します。したがって、let world = &s[6..11];の場合、worldは、sのインデックス 6 のバイトへのポインタと長さ値 5 を持つスライスになります。
図 4-6 は、これを図示したものです。
図 4-6: Stringの一部を参照する文字列スライス
Rust の..範囲構文を使うと、インデックス 0 から始める場合、2 つのピリオドの前の値を省略できます。つまり、次の 2 つは等価です。
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
同様に、スライスがStringの最後のバイトを含む場合、末尾の数字を省略できます。つまり、次の 2 つは等価です。
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
また、両方の値を省略して、文字列全体のスライスを取得することもできます。したがって、次の 2 つは等価です。
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
注:文字列スライスの範囲インデックスは、有効な UTF-8 文字境界で発生する必要があります。マルチバイト文字の途中で文字列スライスを作成しようとすると、プログラムはエラーで終了します。文字列スライスを紹介する目的で、このセクションでは ASCII のみを想定しています。UTF-8 の処理に関するより詳細な議論は、「文字列で UTF-8 エンコードされた文字列を保存する」で行われます。
これらのことを心に留めて、first_wordを書き直してスライスを返すようにしましょう。「文字列スライス」を表す型は&strと書かれます。
ファイル名:src/main.rs
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[..]
}
最初の空白の最初の出現箇所を探すことで、リスト 4-7 と同じ方法で単語の末尾のインデックスを取得します。空白を見つけたときは、文字列の先頭と空白のインデックスを開始インデックスと終了インデックスとして使って文字列スライスを返します。
これで、first_wordを呼び出すと、基礎データに関連付けられた単一の値が返されます。この値は、スライスの開始点への参照とスライス内の要素数で構成されています。
スライスを返すことは、second_word関数にも機能します。
fn second_word(s: &String) -> &str {
これで、コンパイラがStringへの参照が有効なままであることを保証するため、混乱しにくいシンプルな API ができました。リスト 4-8 のプログラムのバグを思い出してください。最初の単語の末尾のインデックスを取得した後、文字列をクリアしてインデックスが無効になってしまったときです。そのコードは論理的に誤っていましたが、即座にエラーを表示しませんでした。空になった文字列を使って最初の単語のインデックスを使い続けると、後で問題が発生します。スライスを使うことで、このバグを防ぐことができ、コードに問題があることをはるかに早く知ることができます。first_wordのスライスバージョンを使うと、コンパイル時エラーが発生します。
ファイル名:src/main.rs
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // エラー!
println!("the first word is: {word}");
}
コンパイラエラーは次のとおりです。
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
借用規則を思い出してください。何かに対する不変参照を持っている場合、可変参照も取得できません。clearはStringをトリムする必要があるため、可変参照を取得する必要があります。clearの呼び出しの後のprintln!は、wordの参照を使っています。したがって、不変参照はその時点でまだ有効でなければなりません。Rust は、clearの可変参照とwordの不変参照が同時に存在することを許可せず、コンパイルが失敗します。Rust は、API を使いやすくするだけでなく、コンパイル時に完全なエラークラスを排除しました!
文字列リテラルをスライスとして
先ほど、文字列リテラルがバイナリ内に格納されていることについて説明しました。スライスについて学んだ今、文字列リテラルを適切に理解することができます。
let s = "Hello, world!";
ここでのsの型は&strです。これは、バイナリの特定の箇所を指すスライスです。これが、文字列リテラルが不変である理由でもあります。&strは不変参照です。
パラメータとしての文字列スライス
リテラルとString値のスライスを取得できることがわかったので、first_wordに対するさらなる改善点があります。それは、そのシグネチャです。
fn first_word(s: &String) -> &str {
もっと経験豊富な Rust プログラマは、代わりにリスト 4-9 に示すシグネチャを書くでしょう。なぜなら、これにより、&String値と&str値の両方で同じ関数を使用できるようになるからです。
fn first_word(s: &str) -> &str {
リスト 4-9: sパラメータの型として文字列スライスを使用することでfirst_word関数を改善する
文字列スライスがあれば、それを直接渡すことができます。Stringがあれば、StringのスライスまたはStringへの参照を渡すことができます。この柔軟性は、「関数とメソッドによる暗黙的な参照強制」で説明する機能である参照強制を利用しています。
Stringへの参照ではなく文字列スライスを受け取るように関数を定義することで、API をより汎用的で便利にすることができます。機能を失うことなくです。
ファイル名:src/main.rs
fn main() {
let my_string = String::from("hello world");
// `first_word` は、部分的または全体的な `String` のスライスに対して機能します
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` は、`String` への参照にも機能します。これは、`String` の全体のスライスと同等です
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` は、部分的または全体的な文字列リテラルのスライスに対して機能します
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// 文字列リテラルはすでに文字列スライスなので、スライス構文なしでもこれが機能します!
let word = first_word(my_string_literal);
}
その他のスライス
おそらく想像できるように、文字列スライスは文字列に固有です。しかし、より汎用的なスライス型もあります。この配列を考えてみましょう。
let a = [1, 2, 3, 4, 5];
文字列の一部を参照したい場合と同じように、配列の一部を参照したい場合があります。その場合は次のようにします。
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
このスライスの型は&[i32]です。文字列スライスと同じように、最初の要素への参照と長さを保存することで機能します。この種のスライスは、他のさまざまなコレクションで使用します。第 8 章でベクトルについて説明する際に、これらのコレクションについて詳細に説明します。
まとめ
おめでとうございます!あなたは「スライス型」の実験を完了しました。あなたの技術を向上させるために、LabEx でさらに実験を行って練習することができます。