スライス

Essential
最終更新: タグ: Rust

前提知識

配列 [T; N] はコンパイル時に長さがわかる。しかし多くのアルゴリズムは、実行時にしか長さがわからない要素の並びを扱う必要がある — 検索結果、ファイルのチャンク、Vec の内容などだ。スライス(slice)はそれを Rust でモデル化する方法だ。

[T] — スライス型

[T](読み方: “T のスライス”)は長さ不明の T 値の連続した並びだ。コンパイル時にサイズが不明なため、[T]動的サイズ型(DST)(dynamically sized type)だ:固定サイズのラッパーなしに、変数・構造体フィールド・スタックへ [T] を直接格納することはできない。

// これはコンパイルエラー — [i32] はサイズ不明:
// let s: [i32] = …;

実際にはほぼ常にスライス参照(slice reference)、すなわち &[T]&mut [T] を使う。

&[T] — スライス参照(ファットポインタ)

スライス参照はファットポインタ(fat pointer)だ:(先頭要素のアドレス、要素数)という2値のペアを保持する。通常の参照の2倍のサイズ、つまり usize 2個分を占める。

use std::mem::size_of;

assert_eq!(size_of::<&[u8]>(), 2 * size_of::<usize>());

アドレスは既存の連続した確保領域 — 配列、Vec、静的バッファ — を指す。スライスはデータを所有しない。参照がスカラー値を借用するのと同じように、データのビューを借用する。

&mut [T] — ミュータブルなスライス参照

&mut [T] は排他的な読み書き可能版だ。スライスのソート、要素の上書き、データのコピーを行える。&mut T と同じ排他性ルールが適用される:重複するデータへの他の参照が同時に存在してはならない。

let mut data = [3, 1, 4, 1, 5];
let s: &mut [i32] = &mut data;
s.sort();
// data は [1, 1, 3, 4, 5] になる

スライスの取得

範囲インデックス(range index)を使って、任意の連続したコレクションのスライスを取得できる:

let arr = [10, 20, 30, 40, 50];
let all:   &[i32] = &arr[..];    // 配列全体
let first: &[i32] = &arr[..3];   // [10, 20, 30]
let mid:   &[i32] = &arr[1..4];  // [20, 30, 40]

let v = vec![1, 2, 3, 4];
let chunk: &[i32] = &v[1..3];    // [2, 3]

範囲構文:..(全体)、a..b(末尾を含まない)、a..=b(末尾を含む)、..ba..

インデックスアクセス

s[i] でスライスにインデックスアクセスすると、要素を参照として返し、i が範囲外の場合は実行時パニックする:

let s = &[10, 20, 30][..];
let x = s[1]; // 20、i32 は Copy なので

失敗するかもしれないインデックスアクセスには .get(i) を使う。これは Option<&T> を返す:

match s.get(5) {
    Some(v) => println!("got {v}"),
    None    => println!("out of bounds"),
}

インデックスが信頼できない入力から来る場合は .get() を優先して使う。

イテレーション

よく使う3つのイテレーションパターン:

let s: &[i32] = &[1, 2, 3];

for x in s.iter() {         // &i32 を yield する
    println!("{x}");
}

let mut data = [4, 5, 6];
for x in data.iter_mut() {  // &mut i32 を yield する
    *x *= 2;
}
// data は [8, 10, 12] になる

for x in [7, 8, 9] {        // 配列を消費する;i32 を yield する(Copy)
    println!("{x}");
}

&[T] 自体への for x in s ループは for x in s.iter() に脱糖される — 値ではなく参照を yield する。

よく使うスライスメソッド

let s: &[i32] = &[3, 1, 4, 1, 5];

s.len();          // 5
s.is_empty();     // false
s.first();        // Some(&3)
s.last();         // Some(&5)
s.contains(&4);   // true

let mut m = [3, 1, 4, 1, 5];
m.sort();         // [1, 1, 3, 4, 5] — インプレースソート、&mut [T] が必要

これらのメソッドは [T] に定義されており、何もインポートせずに任意のスライス参照から利用できる。

&str は UTF-8 バイトのスライス

文字列スライス(string slice)&str は、バイトが有効な UTF-8 であるという追加保証を持つ &[u8] そのものだ。&[T] について言えることはすべて当てはまる:ファットポインタ(アドレス+長さ)であり、どこか別の場所のデータを借用し、文字列を所有しない。

let greeting: &str = "hello";
let bytes: &[u8] = greeting.as_bytes(); // 同じデータを生バイトとして参照

&str が単純に &[u8] でない理由は、UTF-8 の文字が複数バイトにまたがる場合があるからだ。s[i] によるバイト位置でのインデックスアクセスは &str では使えない — Unicode スカラー値をイテレートするには .chars() を、生バイトには .bytes() を使う。

配列からスライスへのコアーション

配列 [T; N] は、スライス参照が期待される場所では自動的に &[T] に型強制(coerce)される。これをサイズなしコアーション(unsized coercion)と呼ぶ:

fn sum(s: &[i32]) -> i32 {
    s.iter().sum()
}

let arr = [1, 2, 3, 4];
let total = sum(&arr); // &[i32; 4] が &[i32] に型強制される

&[T] を受け取る関数は、配列・Vec・その他任意の連続したバッファに対しても等しく機能する — オーバーロードを別に用意する必要はない。

まとめ

  • [T] はDSTだ;常に &[T] または &mut [T] を通じて扱う。
  • &[T] はファットポインタ:アドレス+長さ、サイズは 2 * usize
  • 範囲インデックスでスライスを取得する:&arr[1..4]&v[..]
  • s[i] は範囲外でパニックする;.get(i)Option<&T> を返す。
  • .iter()&T を yield)や .iter_mut()&mut T を yield)でイテレートする。
  • 便利なメソッド:.len().is_empty().first().last().contains().sort()
  • &str は UTF-8 バイトのスライス — 文字列バッファへのファットポインタだ。
  • 配列 [T; N] は自動的に &[T] に型強制されるため、スライスを受け取る関数は配列にも対応する。