404 Not Found

404 Not Found


nginx

Rustのスライス:効率的なデータビューと文字列スライス

スライスとは、Rustにおける特殊な参照の一種です。スライスはデータセット全体を指すのではなく、その中の連続したセグメントを指します。

スライシングを使用すると、データをコピーすることなく、処理のためにデータの一部を安全に「抽出」することができます。


1. 学習内容


2. 概念図

100%
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アドレスを抽出する必要があります:

「『見るだけで、書き写さない』という形でデータを抽出する方法が必要なんだ――まるで新聞の文章の行に指をなぞるように、書き留める必要がないようにね。」

(2) Rustのスライスに関する解決策

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) メモリモデル

RUST
let s = String::from("Hello, Rust!");
let slice = &s[0..5];  // "Hello"
TEXT
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) ▶ サンプル:文字列のスライシング (難易度 ⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
hello: 'Hello'
rust: 'Rust'
world: 'World!'
full: 'Hello, Rust World!'
Literal Slicing: 'Directly'

文字列リテラル("hello" など)は、それ自体が &str 型、つまりバイナリファイルを指すスライスです。すでに参照であるため、& という接頭辞は必要ありません。


(2) ▶ サンプル:文字列のスライシングにおける境界の落とし穴(難易度 ⭐⭐)

RUST
// ============================================
// 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);
    }
}

出力:

TEXT
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) ▶ サンプル:配列のスライシングとベクトルのスライシングの比較(難易度 ⭐⭐)

RUST
// ============================================
// 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]>());
}

出力:

TEXT
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) ▶ サンプル:関数の引数としてのスライス (難易度 ⭐⭐)

RUST
// ============================================
// &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
}

出力:

TEXT
The first word in the literal string: 'hello'
String The First Word: 'Rust'
Array Slicing and: 15

ベストプラクティス:関数のパラメータが文字列を受け取る場合は、&String ではなく &str を使用してください。&String は(deref キャストによって)自動的に &str に変換されるため、&str パラメータの方が汎用性が高く、リテラルと文字列参照の両方を受け入れることができます。


(5) ▶ サンプル:総合演習—ログパーサー(難易度 ⭐⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
=== 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] のスライスを受け入れるため、非常に汎用性が高いです。数値スライスに対する統計関数も、データを借用するのみです。


❓ よくある質問

Q スライスと参照の違いは何ですか?
A スライスは「ファットポインタ」(ポインタ+長さ)ですが、通常の参照は単なるポインタです。&strはメモリ内に「指す先」と「長さ」という2つの情報を格納するのに対し、&Stringは「指す先」のみを格納します。スライスを使用すると、データの境界がわかるため、範囲外のアクセスを防ぐことができます。
Q 文字列のスライス &str と String の関係はどのようなものですか?
A &str は、String の一部として捉えることができます。データそのものは String が所有しており、&str はその一部を借用している形になります。String を一冊の本全体と見なし、&str をその本の中の 1 ページと見なすと理解しやすいでしょう。
Q なぜ &s[0..5] を中国語の文字で使用するとパニックが発生するのですか?
A スライスはバイト単位でインデックス付けされるためです。UTF-8 エンコーディングでは、中国語の文字 1 文字が 3 バイトを占めます。 &s[0..4] ASCII文字列の場合、これはまさに「hello」に相当しますが、文字列「RustBianCheng」の場合、それは「Rust」(4バイト)になります。&s[0..5] これは文字「Bian」の中央のバイトに当たり、コンパイラはこれが有効な文字であるかどうかを判断できないため、パニックが発生します。
Q パラメータには &str と &String のどちらを使うべきですか?
A 常に &str を使用してください。これは、&String は(間接参照によって)自動的に &str に変換されますが、その逆は成り立たないためです。&str を使用したパラメータは、文字列リテラルと String 参照の両方を受け入れることができるため、より汎用性が高くなります。
Q vec![1,2,3]&vec[..] でスライスすると、新しいデータが生成されますか?
A いいえ、オーバーヘッドはゼロです。スライス処理では、元のベクトルのデータへの新しいポインタと範囲情報が作成されるだけです。メモリの割り当てやデータのコピーは行われません。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐String を作成し、[..] を使用して文字列全体を切り出し、その切り出し結果の型が &str であることを確認してください。
  2. 難易度 ⭐⭐: 最後の単語(スペースで区切られたもの)を返す関数 fn last_word(s: &str) -> &str を作成してください。この関数を、文字列リテラルと String の両方に呼び出してください。
  3. 難易度 ⭐⭐⭐: 中国語の文字と絵文字を含む文字列 "Rust🦀BianCheng" を作成し、.char_indices() を使って安全なスライスの境界を見つけ、「Rust🦀」の部分のみを抽出してください(ヒント:🦀 は 4 バイトを占めます)。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%