Skip to content

Latest commit

 

History

History
321 lines (208 loc) · 19.5 KB

04.3_SliceType.md

File metadata and controls

321 lines (208 loc) · 19.5 KB
第4章 所有権を理解する ③



4.3. スライス型


スライス型(slice type)を使用すると、コレクション全体ではなく、コレクションの中の一連の連続した要素を参照することができます。 スライスは一種の参照であるため、所有権はありません

ちょっとしたプログラミングの問題です。スペース(空白)で区切られた単語の文字列を受け取り、その文字列で最初に見つかった単語を返す関数を書きます。関数が文字列内にスペースを見つけられない場合は、文字列全体が 1 つの単語であるはずなので、文字列全体が返されるはずです。

スライス型によって解決される問題を理解するために、まずスライスを使用せずにこの関数のシグネチャー(関数の構成)をどのようにするのか考えて見てみましょう。

fn first_word(s: &String) -> ?

first_word 関数〔「最初の単語」という関数名〕には引数として &String があります。所有権を必要としないので、この形で大丈夫です。しかし、戻り値はどうしましょうか? 文字列の一部分だけを探る方法は実際にはありません。 しかし、スペースで示される単語の末尾を添え字インデックスとしてを返すことはできます。リスト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()
}
リスト4-7: 引数 String にバイト数で示された添え字を返す first_word 関数

Stringの値を一文字ごとに調べ、その値がスペース(空白)かどうかを確認する必要があるため、as_bytes メソッドを使用して Stringバイト配列に変換します。

    let bytes = s.as_bytes();

《訳注》 バイト (byte): データの基本単位で、「1 バイト」=「8 ビット(bit)」。コンピュータの内部では情報が 2 進法で処理されていますが、1 ビットがその 2 進法の 1 桁を示しますので、8 ビット= 1 バイトでは 2 進数の 8 桁分、すなわち 256 通り( 28 )の半角英数文字(アルファベットの大文字・小文字、数字、各種記号)が表示できます。1 バイトあれば英語圏の人々にとって必要な文字や記号をすべて表現することができる訳です。一方、日本語の文字のように、265種類以上あり 1 バイトで表現できない文字は、「マルチバイト文字」と呼ばれています。

ここでは、文字列を一文字毎の「バイト文字列」に変換して、その中に「空白」を示す文字コードがあるかどうかを検査します。

次に、 iter メソッドを使用して、バイト配列に対すてイテレータ(反復子)を生成します。

    for (i, &item) in bytes.iter().enumerate() {

「イテレータ」については、第13章で議論します。今のところは、iter はコレクション内の各要素を返すメソッドであり、enumerateiter の結果を包括して、文字列の各要素をタプル型のデータとして返すことを知っておけば十分です。enumerate から返される**タプル型データの最初の要素は「添え字」で、2 番目の要素は「要素への参照」**です。 この方法は、添え字を自分で勘定するよりも少し便利です。

enumerate メソッドはタプル型データを返すので、パターンを用いてそのタプルを分解できます。「パターン」については第 6 章で詳しく説明します。for ループ構文では、タプルの「添え字」に i を、タプル内の「各 1 バイトのデータ」に &item を対応させるパターンを指定します。.iter().enumerate() から要素への「参照」を行なうので、パターン指定で「借用」を示す & 記号を使用します。

for ループの中では、「バイト・リテラル」表記を用いて「スペース」を表すバイト文字を検索します。「スペース」が見つかった場合は、その位置を返します。 それ以外の場合は、s.len() を使用して文字列の長さを返します。

        if item == b' ' {
            return i;
        }
    }

    s.len()

これで、文字列中の最初の単語 Hello の末尾(5文字目)を示す添え字を見つける方法が判りましたが、問題があります。 単独で usize を返していますが、これは &String が有効である状況でのみ意味のある数値にすぎません。 つまり、文字列とは別の値であるため、将来も有効であるという保証がないのです。 リスト 4-7 での first_word 関数を使用するプログラム(リスト 4-8 )を考えてみましょう。

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);
    ※ 変数 `word` は値 `5` です

    s.clear();
    ※ ここで `String`は解除され、""(空データ)と同一になります。

    ※ 変数 `word` は依然として値 `5` を保持していますが、`5`という値の根拠となる文字列はもうありませんので、変数 `word` は完全に無効です。
}
リスト 4-7: first_word 関数を呼び出し結果を保存した後、String の内容を変更する

このプログラムはエラーなくコンパイルされますし、s.clear() の呼び出し後に word を使用した場合も同様です。これは、words の状態変化には全く関与していないためで、word は値 5 を保ち続けているのです。変数 s の値 5 を用いて最初の単語を抽出することはできますが、これはバグです。なぜなら、word5 を保存したあとに s の内容はクリアされているからです。

word の添え字が s の内容と同期しなくなることを心配するのは面倒、かつ、エラーの素です。second_word 関数〔「2番目の単語」という関数名〕を書くと、添え字の管理はさらに難しくなります。この関数のシグニチャは次のようになるはずです。

fn second_word(s: &String) -> (usize, usize) {

この関数では、2語目の始まる場所を示す添え字と終わる場所の添え字をカウントしており、実際のデータから得られたけれども、元の変数の状態にはまったく関連付けられていない値がさらに増えるのです。本来変数の状態に同期しているはずのものが、結び付きを失なった状態で三つ(一語目の添え字ひとつと二語目の添え字ふたつ)になるのです。

幸いなことに、Rust ではこの問題に対する解決策が用意されています。それが「文字列スライス」です。


文字列スライス String Slice

文字列スライスは、String の一部分に対する参照のことで、次のようなものです。

    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];

hello は、String 全体への参照を行なうのではなく、String の一部分を参照する [0..5]という少し余分な指定を行なっています。このように、スライスは「角括弧([])」で参照範囲を指定するもので、[開始添え字..終了添え字]の形で行ないます。「開始添え字」がスライスの先頭位置で、「終了添え字」はスライス最後尾のひとつ後ろになります。内部的には、スライスのデータ構造は、開始位置とスライスの長さを保持しており、スライスの長さは「終了添え字」から「開始添え字」を引いた値に相当します。それ故、let world = &s[6..11]の場合、world は変数 s の添え字(index) 6の位置にあるバイト(文字コード)への「ポインタ」情報(ptr = pointer、アドレス情報)とスライスの「長さ」情報(len = length) 5 を持つスライスを意味します。

図4-6 にて、これを図示します。

図4-6:String の一部分だけを参照する「文字列スライス」

Rust の「..式範囲記法」では、添え字が「ゼロ」から始まる場合、ふたつのピリオド(..)の前にある添え字番号は省略ができます。したがって、次の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..];

さらにまた、文字列全体のスライスを行なう場合、前後双方の添え字番号を省略できます。したがって、以下も同じ事を意味します。

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

【原注】 文字列スライスの範囲指定は有効な UTF-8 文字コードの範囲内で行なわれなければなりません。(日本語のような)マルチバイト文字で「文字列スライス」を行なおうとすると、プログラム・エラーとなります。この節では、「文字列スライス」の紹介が目的であるため、用いている文字コードは「1 バイト」(ASCII文字コード)だけとの想定で話を進めます。UTF-8 文字コードのより詳細な説明は、第 8.2 章の「文字列で 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[..]
}

単語の終わりを示す添え字は、リスト 4-7 で行なったのと同じ方法、すなわち「最初のスペース(空白部分)を探す」ことで見つけています。スペースが見つかると、文字列の先頭を「添え字番号の最初」に、スペースの位置を「添え字番号の最後」に指定して文字列スライスを返します。

これで、first_word 関数を呼び出すと、実際のデータに結び付いた単一の値が得られるようになりました。この値は、スライスの開始地点への参照とスライス中の要素の数から成り立っています。

スライスを返すこのやり方は second_word 関数でも同じように機能します。

fn second_word(s: &String) -> &str {

コンパイラーが String への参照が有効であることを保証してくれるため、遥かに混乱しにくい判り易い API 〔プログラムから利用できる外部の共用プログラム〕になりました。リスト 4-8 のプログラムのバグを思い出してください。最初の単語の末尾の添え字を取得した後、文字列をクリアしたため、取得した添え字が無効になりました。プログラムが論理的に正しくないためですが、すぐにはエラーは表示されませんでした。最初の単語の添え字をクリアされた文字列で使用しようとすると、問題が発生します。スライスを用いるとこのバグの発生を不可能にし、コードに問題があることをより早く知らせてくれます。スライスを返すやり方の first_word 関数であれば、コンパイル時にエラーが判ります。


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
※ 変数 `s` は不変として借用されているので、可変借用できません。
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           ※ ここで「不変借用」発生
17 | 
18 |     s.clear(); // error!
   |     ^^^^^^^^^ ※ ここで「可変借用」発生
19 | 
20 |     println!("the first word is: {}", word);
   |                                       ※ ここで「不変借用」として利用あり。

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

不変参照が行なわれている場合、同時に可変参照を行なうことはできないという借用規則を思い出してください。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 への参照を渡すことになります。この柔軟性は、第 15.2 章「Derefトレイトでスマートポインタを普通の参照のように扱う」の「関数やメソッドで暗黙的な参照外し型強制」の項で説明する参照外し型強制Deref coercion)を利用しています。これは、関数を定義する際に、String への参照をする代わりに文字列スライスを取得することで、少しも関数の機能を失なうことなく、その API をより一般的かつ有用にするものです。

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] 型です。最初の添え字と長さへの参照を格納することで、文字列スライスと同じように機能します。Rust では、この種のスライスを他のあらゆる種類のコレクションに使用します。コレクションの詳細については、第8.1章のベクタ」のところで学習します。


まとめ

Rust プログラムでは、「所有権」「借用」「スライス」の考え方によって、コンパイル時にメモリの安全性を担保しています。Rust プログラミング言語は、他のシステム・プログラミング言語と同じようにメモリ使用量の管理をユーザーに委ねていますが、データの所有者がスコープ外に出たときにそのデータを自動的に解除するので、メモリ管理のための余分なコードを自分で記述しデバッグする必要がないのです。

「所有権」の考え方は、Rust の様々な部分がどのように機能するのかに関与しているので、本書の残りの部分でもこれらの概念についてさらに説明します。では第 5 章に進み、個々のデータ要素を struct構造体)を用いてグループ化する方法を見ていきましょう。