Rustのスライス:効率的なデータビューと文字列スライス
スライスとは、Rustにおける特殊な参照の一種です。スライスはデータセット全体を指すのではなく、その中の連続したセグメントを指します。
スライシングを使用すると、データをコピーすることなく、処理のためにデータの一部を安全に「抽出」することができます。
1. 学習内容
- ストリングスライスの概念と作成方法
&str [start..end]範囲構文を使用してスライスを作成する- 文字列のスライスは、UTF-8の文字境界に一致しなければならない
- 配列のスライシング
&[T]およびベクトルのスライシング&[Vec] - 関数の引数としてのスライス — 文字列入力の中で最も柔軟な形式
- スライスの内部表現:ポインタ + 長さ
2. 概念図
flowchart LR
subgraph "Heap memory"
HEAP["'H','e','l','l','o','','R','u','s','t','!'"]
end
subgraph "String s"
S_PTR["ptr"] --> HEAP
S_LEN["len: 12"]
S_CAP["cap: 12"]
end
subgraph "&str slice"
SL_PTR["ptr"] -->|"Pointing Forward5Byte"| HEAP
SL_LEN["len: 5"]
end
3. データアナリストの物語
(1) 問題:ログからの情報抽出に時間がかかりすぎる
マリアは、あるEコマース企業のデータアナリストです。彼女は、膨大な量のサーバーログからIPアドレスを抽出する必要があります:
- 各ログ行には数万文字が含まれている
substringを使用してコピーする場合、ログエントリ 1 件ごとに数千バイトの追加メモリが必要になります。- 1日あたり1,000,000件のログを処理し、数GBの追加メモリが割り当てられている
- OOM(メモリ不足)エラーにより、サーバーが1回クラッシュした
「『見るだけで、書き写さない』という形でデータを抽出する方法が必要なんだ――まるで新聞の文章の行に指をなぞるように、書き留める必要がないようにね。」
(2) Rustのスライスに関する解決策
fn main() {
let log_line = "192.168.1.1 - - [01/Jul/2026:12:00:00] \"GET /index.html\"";
// Slices -- do not copy, they just "point to" a portion of the original string
let ip = &log_line[0..13]; // "192.168.1.1"
let date = &log_line[20..38]; // "01/Jul/2026:12:00:00"
let path = &log_line[50..62]; // "/index.html"
println!("IP: {}", ip);
println!("Date: {}", date);
println!("Path: {}", path);
// Key Points: No strings were copied! All slices point to different areas of log_line
println!("The original logs are still available: {}", log_line);
}
スライスは、元のデータの別のビューを指し示す「窓」のようなものです。データのコピーもメモリの割り当ても行われないため、オーバーヘッドはゼロです。これは、大規模なデータを扱う際に極めて重要です。
4. スライシングの原則
(1) メモリモデル
let s = String::from("Hello, Rust!");
let slice = &s[0..5]; // "Hello"
graph TB
subgraph "String s"
S_ptr[ptr ──→ H e l l o , R u s t !]
S_len[len: 12]
S_cap[cap: 12]
end
subgraph "&str slice"
SL_ptr[ptr ──→ H e l l o]
SL_len[len: 5]
end
S_ptr -.-> heap[Load the data]
SL_ptr -.-> heap
| 特集 | String 全体 |
&s[0..5] の一部 |
|---|---|---|
| メモリ | ptr + len + cap (3バイト) | ptr + len (2バイト) |
| 所有状況 | 所有 | 借用(引用) |
| データをコピーするかどうか | — | コピーしない |
| アクセス範囲 | 文字列全体 | "Hello" (5バイト) |
(2) スコープの構文
| 構文 | 意味 | 例 |
|---|---|---|
[0..5] |
0~5(5を除く) | "Hello" |
[..5] |
最初から5まで | "Hello" |
[5..] |
5から最後まで | ", Rust!" |
[..] |
文字列全体 | "Hello, Rust!" |
(3) スライスタイプのクイックリファレンス
| スライスタイプ | 構文 | サイズ | 説明 |
|---|---|---|---|
| 文字列のスライス | &str |
16 バイト | ptr + len (FATポインタ) |
| 配列のスライシング | &[T] |
16 バイト | ptr + len (ファットポインタ) |
| 配列参照 | &[T; N] |
8 バイト | ポインタのみ(長さはコンパイル時に判明) |
| 可変スライス | &mut [T] |
16 バイト | ptr + len (変更可能な要素) |
5. スライシングの例
(1) ▶ サンプル:文字列のスライシング (難易度 ⭐)
// ============================================
// Basic Usage of String Slicing
// ============================================
fn main() {
let s = String::from("Hello, Rust World!");
// Various Slicing Methods
let hello = &s[..5]; // "Hello"
let rust = &s[7..11]; // "Rust"
let world = &s[12..]; // "World!"
let full = &s[..]; // All
println!("hello: '{}'", hello);
println!("rust: '{}'", rust);
println!("world: '{}'", world);
println!("full: '{}'", full);
// String literals are, by their very nature, &str
let literal: &str = "Create a slice directly";
let first_word = &literal[..2];
println!("Literal Slicing: '{}'", first_word);
}
出力:
hello: 'Hello'
rust: 'Rust'
world: 'World!'
full: 'Hello, Rust World!'
Literal Slicing: 'Directly'
文字列リテラル(
"hello"など)は、それ自体が&str型、つまりバイナリファイルを指すスライスです。すでに参照であるため、&という接頭辞は必要ありません。
(2) ▶ サンプル:文字列のスライシングにおける境界の落とし穴(難易度 ⭐⭐)
// ============================================
// UTF-8 Boundary: Slices must be at character boundaries.
// ============================================
fn main() {
let s = "RustProgramming"; // Number of bytes: R(1) u(1) s(1) t(1) Bian(3) Cheng(3) = 10 bytes
// Security Slices
let rust = &s[..4]; // "Rust" (first 4 bytes are exactly ASCII)
println!("Rust Part: {}", rust);
// The following are examples of errors (uncomment to see panic!)
// let bad = &s[0..5]; // ❌ 5 is between 4 and 6, landing in the middle of "Bian"'s first byte
// A Safe Approach: use chars() and char_indices() to iterate
for (i, c) in s.char_indices() {
println!("Byte Index {}: Character '{}'", i, c);
}
// Use char_indices to find safe slice boundaries
if let Some((pos, _)) = s.char_indices().nth(4) {
let safe_slice = &s[..pos];
println!("First 4 character slice: {}", safe_slice);
}
}
出力:
Rust Part: Rust
Byte Index 0: Character 'R'
Byte Index 1: Character 'u'
Byte Index 2: Character 's'
Byte Index 3: Character 't'
Byte Index 4: Character 'Bian'
Byte Index 7: Character 'Cheng'
First 4 character slice: RustProgramming
スライスのインデックスは、文字単位ではなくバイト単位で指定されます。スライスの境界がマルチバイト文字の途中に位置する場合、プログラムはパニックを起こしてクラッシュします。安全な文字単位のインデックスを取得するには、
.char_indices()を使用してください。
(3) ▶ サンプル:配列のスライシングとベクトルのスライシングの比較(難易度 ⭐⭐)
// ============================================
// Slicing Arrays and Vectors
// ============================================
fn main() {
// Array Slicing
let arr = [1, 2, 3, 4, 5];
let slice_arr = &arr[1..4]; // [2, 3, 4]
println!("Array Slicing: {:?}", slice_arr);
// Vector Slicing
let vec = vec![10, 20, 30, 40, 50];
let slice_vec = &vec[..3]; // [10, 20, 30]
println!("Vector Slicing(first 3): {:?}", slice_vec);
// Edit Slice Content (requires &mut)
let mut numbers = vec![1, 2, 3, 4, 5];
let mut_slice = &mut numbers[1..4]; // [2, 3, 4]
mut_slice[0] = 99; // Modifying a slice affects the original vector.
println!("Modified original vector: {:?}", numbers);
// Slice Type Size
println!("&[i32] Occupancy: {} Byte", std::mem::size_of::<&[i32]>());
println!("&[i32;5] Occupancy: {} Byte", std::mem::size_of::<&[i32; 5]>());
}
出力:
Array Slicing: [2, 3, 4]
Vector Slicing(first 3): [10, 20, 30]
Modified original vector: [1, 99, 3, 4, 5]
&[i32] Occupancy: 16 Byte
&[i32;5] Occupancy: 8 Byte
スライスタイプ (
&[i32]) は 16 バイト(ポインタ用 8 バイト + 長さ用 8 バイト)を占有するのに対し、通常の参照 (&[i32;5]) は 8 バイト(ポインタのみ)しか占有しません。これは「ファットポインタ」として知られているもので、スライスとは長さ情報を含むポインタのことです。
(4) ▶ サンプル:関数の引数としてのスライス (難易度 ⭐⭐)
// ============================================
// &str As a function argument -- the most flexible approach
// ============================================
// ✅ Best Practices: Receive &str -- regardless of whether the input is &str or &String
fn first_word(s: &str) -> &str {
for (i, &b) in s.as_bytes().iter().enumerate() {
if b == b' ' {
return &s[..i];
}
}
&s[..] // No spaces, return the entire string
}
fn main() {
// Incoming &str Literal
let result1 = first_word("hello world");
println!("The first word in the literal string: '{}'", result1);
// Incoming &String (automatically converts to &str)
let s = String::from("Rust is awesome");
let result2 = first_word(&s); // &String Automatically convert to &str
println!("String The First Word: '{}'", result2);
// Passing an array slice
let arr = [1, 2, 3, 4, 5];
let sum: i32 = sum_slice(&arr[..]); // Use [..] to convert array to slice
println!("Array Slicing and: {}", sum);
}
fn sum_slice(slice: &[i32]) -> i32 {
let mut total = 0;
for x in slice {
total += *x;
}
total
}
出力:
The first word in the literal string: 'hello'
String The First Word: 'Rust'
Array Slicing and: 15
ベストプラクティス:関数のパラメータが文字列を受け取る場合は、
&Stringではなく&strを使用してください。&Stringは(deref キャストによって)自動的に&strに変換されるため、&strパラメータの方が汎用性が高く、リテラルと文字列参照の両方を受け入れることができます。
(5) ▶ サンプル:総合演習—ログパーサー(難易度 ⭐⭐⭐)
// ============================================
// Practical Guide to Slicing: Zero-Copy Log Parsing
// ============================================
fn parse_log_line(line: &str) -> (&str, &str, &str) {
let ip_end = line.find(' ').unwrap_or(line.len());
let ip = &line[..ip_end];
let rest = &line[ip_end..].trim_start();
let method_end = rest.find(' ').unwrap_or(rest.len());
let method = &rest[..method_end];
let path_part = &rest[method_end..].trim_start();
let path_end = path_part.find(' ').unwrap_or(path_part.len());
let path = &path_part[..path_end];
(ip, method, path)
}
fn find_longest<'a>(strings: &[&'a str]) -> &'a str {
strings.iter().max_by_key(|s| s.len()).unwrap_or(&"")
}
fn summarize(data: &[i32]) -> (f64, i32, i32) {
if data.is_empty() {
return (0.0, 0, 0);
}
let sum: i32 = data.iter().sum();
let avg = sum as f64 / data.len() as f64;
let min = *data.iter().min().unwrap();
let max = *data.iter().max().unwrap();
(avg, min, max)
}
fn main() {
let logs = [
"192.168.1.1 GET /index.html HTTP/1.1",
"10.0.0.5 POST /api/login HTTP/1.1",
"172.16.0.1 DELETE /api/user/42 HTTP/1.1",
];
println!("=== Log Analysis (Zero-Copy) ===");
for log in &logs {
let (ip, method, path) = parse_log_line(log);
println!("IP: {:<15} Methods: {:<6} Path: {}", ip, method, path);
}
println!("\n=== Longest Path ===");
let paths: Vec<&str> = logs.iter().map(|l| {
let (_, _, p) = parse_log_line(l);
p
}).collect();
println!("Longest Path: '{}'", find_longest(&paths));
println!("\n=== Statistics on Numerical Slices ===");
let scores = [85, 92, 78, 95, 88, 70, 96];
let (avg, min, max) = summarize(&scores);
println!("Grade Slices: {:?}", scores);
println!("Average: {:.1}, Lowest: {}, Highest: {}", avg, min, max);
}
出力:
=== Log Analysis (Zero-Copy) ===
IP: 192.168.1.1 Methods: GET Path: /index.html
IP: 10.0.0.5 Methods: POST Path: /api/login
IP: 172.16.0.1 Methods: DELETE Path: /api/user/42
=== Longest Path ===
Longest Path: '/api/user/42'
=== Statistics on Numerical Slices ===
Grade Slices: [85, 92, 78, 95, 88, 70, 96]
Average: 86.3, Lowest: 70, Highest: 96
ログの解析中、
parse_log_lineによって返される 3 つの&str値は、すべて元の文字列の異なる領域を指しています。これにより、ゼロコピーかつゼロアロケーションが実現されます。find_longestは&[&str]のスライスを受け入れるため、非常に汎用性が高いです。数値スライスに対する統計関数も、データを借用するのみです。
❓ よくある質問
&strはメモリ内に「指す先」と「長さ」という2つの情報を格納するのに対し、&Stringは「指す先」のみを格納します。スライスを使用すると、データの境界がわかるため、範囲外のアクセスを防ぐことができます。&s[0..4] ASCII文字列の場合、これはまさに「hello」に相当しますが、文字列「RustBianCheng」の場合、それは「Rust」(4バイト)になります。&s[0..5] これは文字「Bian」の中央のバイトに当たり、コンパイラはこれが有効な文字であるかどうかを判断できないため、パニックが発生します。vec![1,2,3] を &vec[..] でスライスすると、新しいデータが生成されますか?📖 まとめ
- スライスとは、データの「ビュー」であり、データを所有したり、そのコピーを作成したりするものではありません。
- 文字列のスライシング
&strとは、文字列またはリテラルの一部を指します - 範囲の表記法:
[start..end]、[..end]、[start..]、[..] - スライスは、16バイトを占めるファットポインタ(ポインタ+長さ)です
- 文字列のスライスは、UTF-8の文字境界に一致する必要があります
- 文字列を受け付ける関数のパラメータには
&strを使用してください(&Stringよりも汎用性が高いです) - 配列のスライシング
&[T]は、ベクトルのスライシングと同じように機能します
📝 練習問題
- 難易度 ⭐:
Stringを作成し、[..]を使用して文字列全体を切り出し、その切り出し結果の型が&strであることを確認してください。 - 難易度 ⭐⭐: 最後の単語(スペースで区切られたもの)を返す関数
fn last_word(s: &str) -> &strを作成してください。この関数を、文字列リテラルとStringの両方に呼び出してください。 - 難易度 ⭐⭐⭐: 中国語の文字と絵文字を含む文字列
"Rust🦀BianCheng"を作成し、.char_indices()を使って安全なスライスの境界を見つけ、「Rust🦀」の部分のみを抽出してください(ヒント:🦀 は 4 バイトを占めます)。



