スライス型

所有権のない別のデータ型は、スライスです。スライスにより、コレクション全体ではなく、 その内の一連の要素を参照することができます。

ちょっとしたプログラミングの問題を考えてみましょう: 文字列を受け取って、その文字列中の最初の単語を返す関数を書いてください。 関数が文字列中に空白を見つけられなかったら、文字列全体が一つの単語に違いないので、文字列全体が返されるべきです。

この関数のシグニチャについて考えてみましょう:

fn first_word(s: &String) -> ?

この関数、first_wordは引数に&Stringをとります。所有権はいらないので、これで十分です。 ですが、何を返すべきでしょうか?文字列の一部について語る方法が全くありません。しかし、 単語の終端の添え字を返すことができますね。リスト4-7に示したように、その方法を試してみましょう。

ファイル名: src/main.rs

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() {}

リスト4-7: String引数へのバイト数で表された添え字を返すfirst_word関数

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は、コレクション内の各要素を返すメソッドであること、 enumerateiterの結果をラップして、(結果をそのまま返す)代わりにタプルの一部として各要素を返すことを知っておいてください。 enumerateから返ってくるタプルの第1要素は、添え字であり、2番目の要素は、(コレクションの)要素への参照になります。 これは、手動で添え字を計算するよりも少しだけ便利です。

enumerateメソッドがタプルを返すので、Rustのあらゆる場所同様、パターンを使って、そのタプルを分配できます。 従って、forループ内で、タプルの添え字に対するiとタプルの1バイトに対応する&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のプログラムを考えてください。

ファイル名: src/main.rs

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
                               // wordの中身は、値5になる

    s.clear(); // this empties the String, making it equal to ""
               // Stringを空にする。つまり、""と等しくする

    // word still has the value 5 here, but there's no more string that
    // we could meaningfully use the value 5 with. word is now totally invalid!
    // wordはまだ値5を保持しているが、もうこの値を正しい意味で使用できる文字列は存在しない。
    // wordは今や完全に無効なのだ!
}

リスト4-8: first_word関数の呼び出し結果を保持し、Stringの中身を変更する

このプログラムは何のエラーもなくコンパイルが通り、words.clear()の呼び出し後に使用しても、 コンパイルが通ります。wordsの状態に全く関連づけられていないので、その中身はまだ値5のままです。 その値5を変数sに使用し、最初の単語を取り出そうとすることはできますが、これはバグでしょう。 というのも、sの中身は、5wordに保存した後変わってしまったからです。

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全体への参照を取ることに似ていますが、余計な[0..5]という部分が付いています。 String全体への参照ではなく、Stringの一部への参照です。

[starting_index..ending_index]と指定することで、角かっこに範囲を使い、スライスを生成できます。 ここで、starting_indexはスライスの最初の位置、ending_indexはスライスの終端位置よりも、 1大きい値です。内部的には、スライスデータ構造は、開始地点とスライスの長さを保持しており、 スライスの長さはending_indexからstarting_indexを引いたものに対応します。以上より、 let world = &s[6..11];の場合には、worldsの添え字6のバイトへのポインタと5という長さを持つスライスになるでしょう。

図4-6は、これを図解しています。

文字列sの6バイト目へのポインタと長さ5を保持するworld

図4-6: Stringオブジェクトの一部を参照する文字列スライス

Rustの..という範囲記法で、最初の番号(ゼロ)から始めたければ、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文字境界に置かなければなりません。 マルチバイト文字の真ん中で文字列スライスを生成しようとしたら、エラーでプログラムは落ちるでしょう。 この節では文字列スライスを導入することが目的なので、ASCIIのみを想定しています; UTF-8に関するより徹底した議論は、 第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[..]
}

fn main() {}

リスト4-7で取った方法と同じように、最初の空白を探すことで単語の終端の添え字を取得しています。 空白を発見したら、文字列の最初を開始地点、空白の添え字を終了地点として使用して文字列スライスを返しています。

これで、first_wordを呼び出すと、元のデータに紐付けられた単独の値を得られるようになりました。 この値は、スライスの開始地点への参照とスライス中の要素数から構成されています。

second_word関数についても、スライスを返すことでうまくいくでしょう:

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

これで、ずっと混乱しにくい素直なAPIになりました。なぜなら、Stringへの参照が有効なままであることをコンパイラが、 保証してくれるからです。最初の単語の終端添え字を得た時に、 文字列を空っぽにして先ほどの添え字が無効になってしまったリスト4-8のプログラムのバグを覚えていますか? そのコードは、論理的に正しくないのですが、即座にエラーにはなりませんでした。問題は後になってから発生し、 それは空の文字列に対して、最初の単語の添え字を使用し続けようとした時でした。スライスならこんなバグはあり得ず、 コードに問題があるなら、もっと迅速に判明します。スライスバージョンのfirst_wordを使用すると、 コンパイルエラーが発生します:

ファイル名: 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[..]
}

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);
   |                           -- 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
                                                (不変借用はその後ここで使われています)

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

借用規則から、何かへの不変な参照がある時、さらに可変な参照を得ることはできないことを思い出してください。 clearStringを切り詰める必要があるので、可変な参照を得る必要があります。Rustはこれを認めないので、コンパイルが失敗します。 RustのおかげでAPIが使いやすくなるだけでなく、ある種のエラー全てを完全にコンパイル時に排除してくれるのです!

文字列リテラルはスライスである

文字列は、バイナリに埋め込まれると話したことを思い出してください。今やスライスのことを知ったので、 文字列リテラルを正しく理解することができます。


#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

ここでのsの型は、&strです: バイナリのその特定の位置を指すスライスです。 これは、文字列が不変である理由にもなっています。要するに、&strは不変な参照なのです。

引数としての文字列スライス

リテラルやString値のスライスを得ることができると知ると、first_wordに対して、もう一つ改善点を見出すことができます。 シグニチャです:

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
    // first_wordは`String`のスライスに対して機能する
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    // first_wordは文字列リテラルのスライスに対して機能する
    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);
}

リスト4-9: s引数の型に文字列スライスを使用してfirst_word関数を改善する

もし、文字列スライスがあるなら、それを直接渡せます。Stringがあるなら、 そのString全体のスライスを渡せます。Stringへの参照の代わりに文字列スライスを取るよう関数を定義すると、 何も機能を失うことなくAPIをより一般的で有益なものにできるのです。

Filename: src/main.rs

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
    // first_wordは`String`のスライスに対して機能する
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    // first_wordは文字列リテラルのスライスに対して機能する
    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];
}

このスライスは、&[i32]という型になります。これも文字列スライスと同じように動作します。 つまり、最初の要素への参照と長さを保持するのです。 この種のスライスは、他のすべての種類のコレクションに対して使用することになるでしょう。 それらのコレクションについて、詳しくは、第8章でベクタについて話すときに議論します。

まとめ

所有権、借用、スライスの概念は、Rustプログラムにおいて、コンパイル時にメモリ安全性を保証します。 Rust言語も他のシステムプログラミング言語と同じように、メモリの使用法について制御させてくれるわけですが、 データの所有者がスコープを抜けたときに、所有者に自動的にデータを片付けさせることは、この制御をするために、 余計なコードを書いたりデバッグしたりする必要がないことを意味します。

所有権は、Rustの他のいろんな部分が動作する方法に影響を与えるので、これ以降もこれらの概念についてさらに語っていく予定です。 第5章に移って、structでデータをグループ化することについて見ていきましょう。

関連キーワード:  スライス, let, 参照, item, bytes, string, 関数, iter, enumerate, 要素