はじめに
スライス型へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、空白で区切られた単語の文字列を受け取り、その文字列で見つけた最初の単語を返す関数を書いてプログラミング問題を解きます。その後、インデックスを使って部分文字列を表す制限と、Rust での文字列スライスを使ったこの問題の解決策について説明します。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
スライス型へようこそ。この実験は、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 でさらに実験を行って練習することができます。