スライス
Essential前提知識
配列 [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(末尾を含む)、..b、a..。
インデックスアクセス
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]に型強制されるため、スライスを受け取る関数は配列にも対応する。