404 Not Found

404 Not Found


nginx

Rustの文字列に関する総合ガイド:&strとStringの秘密

Rustには、文字列型が1つではなく2つあります。これは、システムプログラミングではメモリを精密に制御する必要があるためです。

Rustにおける文字列の扱いは、多くの初心者にとって混乱の種となっています。「なぜString&strという2つの型があるのか?」「これらはC言語のchar*とどう違うのか?」このレッスンでは、その点を完全に理解できるよう解説します。


1. 学習内容


2. ある司書の物語

(1) 問題:JSONデータが長すぎて収まらない

マリアは図書館員で、5,000冊の書籍を管理するための小規模な図書検索システムを開発しています。

「メモリを自動的に管理してくれるストリングがあれば、本当に素晴らしいのに……」彼女はその後、Rustでその答えを見つけた。

(2) Rustにおける文字列の処理方法

RUST
fn main() {
    // &str: String Literals, Present in the binary at compile time
    let title_book1: &str = "One Hundred Years of Solitude";

    // String: Allocated on the heap, Variable length, Automatic Memory Management
    let mut title_book2 = String::from("How to Make a Complete Map of an Infinite Universe");
    title_book2.push_str(" (Second Edition)");  // Additional Content

    println!("Book 1: {} (Length: {} Bytes)", title_book1, title_book1.len());
    println!("Book 2: {} (Length: {} Bytes)", title_book2, title_book2.len());
    // String Automatically released when leaving the scope, No manual free required
}

RustのStringはJavaの文字列のようなもので、メモリを自動的に管理します。一方、&strはCのconst char*のようなもので、読み取り専用の参照です。これらを組み合わせることで、パフォーマンスと柔軟性のバランスが取れています。


3. &str と String の違い

(1) メモリモデル

100%
graph LR
    subgraph "&str (String Literals)"
        S1_ptr[ptr ──→ ...rust...]
        S1_len[len: 4]
    end
    subgraph "String (Heap Allocation)"
        S2_ptr[ptr ──→ Rust is...]
        S2_len[len: 10]
        S2_cap[cap: 16]
    end
    subgraph "Storage Location"
        STACK[Stack: &str<br>Read-Only Borrow]
        HEAP[Heap: String<br>Ownership]
    end
    S1_ptr -.->|Determined at compile time| STACK
    S2_ptr -.->|Runtime Allocation| HEAP
特徴 &str String
所有者 読み取り専用アクセス 完全な所有権
可変性 不変 追加・変更が可能
格納場所 読み取り専用データセグメント(コンパイル時) ヒープ(実行時に割り当て)
作成方法 リテラル "hello" String::from() または .to_string()
ユースケース 読み取り専用の文字列パラメータ 文字列の変更または所有権の取得が必要

(2) よく使われる文字列メソッドのクイックリファレンス

メソッド 戻り値の型 説明
push_str(&str) () 追加の文字列スライス s.push_str("abc")
push(char) () 1文字を追加 s.push('!')
len() usize 戻りバイト数 s.len()
is_empty() bool 空ですか? s.is_empty()
trim() &str 先頭と末尾のスペースを削除 s.trim()
contains(&str) bool 部分文字列が含まれているかどうか s.contains("rust")
replace(&str, &str) String すべて置換 s.replace("a", "b")
区切り文字で分割 split(char) Split s.split(',')
小文字に変換する
大文字 to_uppercase() String 大文字に切り替える
chars() Chars 文字単位で反復 s.chars()
bytes() Bytes バイト単位で反復処理 s.bytes()
文字列をクリアする
capacity() usize バッファ容量に戻る s.capacity()

(3) 作成方法

RUST
// &str Create
let s1: &str = "hello, world";       // String Literals
let s2: &str = r"raw string\n";      // Original string(Do not escape)

// String Create
let s3 = String::from("hello");
let s4 = "world".to_string();
let s5 = format!("{}-{}", s3, s4);   // Format and Create
let s6: String = "hi".into();         // Through Type Conversion

(4) 文字列変換メソッドのクイックリファレンス

ソースタイプ ターゲットタイプ 変換方法 オーバーヘッドなし
ヒープメモリを割り当てる
ヒープメモリを割り当てる
ヒープメモリを割り当てる
String &str &s または s.as_str() ゼロコスト(借入)
String &str &s[..] ゼロコスト(スライス)
&str &[u8] "hello".as_bytes() ゼロコスト(借入)
String Vec<u8> s.into_bytes() ゼロオーバーヘッド(所有権の移転)
Vec<u8> String String::from_utf8(v) オーバーヘッドゼロ(所有権の移転;UTF-8の検証が必要)

4. 一般的な文字列操作

(1) ▶ サンプル:文字列の作成と変更(難易度 ⭐)

RUST
// ============================================
// String Creation, Append and Concatenation
// ============================================

fn main() {
    // Create
    let mut s = String::from("Hello");

    // Add
    s.push_str(", ");       // Append a string slice
    s.push('R');            // Add a single character
    s.push_str("ust!");
    println!("After the addition: {}", s);

    // Concatenation (+ operator consumes the left-hand side String)
    let s1 = String::from("Hello, ");
    let s2 = String::from("World!");
    let s3 = s1 + &s2;      // s1 Moved, It can't be used anymore.
    println!("After concatenation: {}", s3);

    // format! macro (Does not consume any variables)
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    let result = format!("{}-{}-{}", a, b, c);
    println!("format Results: {}", result);
}

出力:

TEXT
After the addition: Hello, Rust!
After concatenation: Hello, World!
format Result: tic-tac-toe

+ 演算子は、その左側にある String を消費(移動)するため、連結後は s1 を使用できなくなります。format! は変数を一切消費せず、すべての変数の所有権は保持されます。


(2) ▶ サンプル:文字列のスライシングとインデックス操作 (難易度 ⭐⭐)

RUST
// ============================================
// String Slicing -- Note UTF-8 Boundary!
// ============================================

fn main() {
    let s = String::from("Rust🦀Code");

    // Get the length of a string (Number of bytes, Not the number of characters)
    println!("Byte Length: {}", s.len());         // 11 (4 Byte ASCII + 4 Byte UTF-8 emoji + 4 Byte ASCII)

    // String Slicing (Index by Byte, Must be at a character boundary)
    let slice1 = &s[0..4];                     // "Rust" (first 4 bytes fall exactly on a character boundary)
    println!("First 4 Byte Slice: {}", slice1);

    // &s[0..5] will panic! (Because bytes 4-5 are in the middle of the emoji)

    // Iterate by character count
    for c in s.chars() {
        print!("{} ", c);
    }
    println!();

    // Iterate by byte
    for b in s.bytes() {
        print!("{:02x} ", b);
    }
    println!();
}

出力:

TEXT
Byte Length: 11
First 4 Byte Slice: Rust
R u s t 🦀 C o d e
52 75 73 74 f0 9f a6 80 43 6f 64 65

Rustの文字列は、UTF-8文字が可変長であるため、インデックスによる直接アクセス(例:s[0])をサポートしていません。.chars() を使用して文字を順に処理するか、バイトスライスを使用する必要があります。スライスは文字の境界に揃えて指定する必要があります。そうしないと、実行時にクラッシュが発生します。


(3) ▶ サンプル:一般的な文字列メソッド (難易度 ⭐⭐)

RUST
// ============================================
// Common String Queries, Replace, Cutting Methods
// ============================================

fn main() {
    let s = "  Hello, Rust World!  ";

    // Remove leading and trailing spaces
    println!("trim: '{}'", s.trim());

    // Includes a check
    println!("contains 'Rust': {}", s.contains("Rust"));

    // Replace
    let replaced = s.replace("Rust", "Go");
    println!("replace: '{}'", replaced);

    // Split
    let parts: Vec<&str> = "apple,banana,cherry".split(',').collect();
    println!("split: {:?}", parts);

    // Case Conversion
    let lower = "HELLO".to_lowercase();
    let upper = "world".to_uppercase();
    println!("lower: {}, upper: {}", lower, upper);
}

出力:

TEXT
trim: 'Hello, Rust World!'
contains 'Rust': true
replace: '  Hello, Go World!  '
split: ["apple", "banana", "cherry"]
lower: hello, upper: WORLD

trim() および split() は、新しい文字列を作成することなく &str を返します。replace() は、まったく新しい String を返します。変更を行う際のパフォーマンス上のオーバーヘッドを考慮してください。


(4) ▶ サンプル:UTF-8 文字列の反復処理と境界チェック (難易度 ⭐⭐⭐)

RUST
// ============================================
// UTF-8 Bytes in a string, Character and Grapheme Clusters
// ============================================

fn main() {
    let s = "Rust🦀Code";

    println!("String: {}", s);
    println!("Byte Length: {}", s.len());
    println!("Number of characters: {}", s.chars().count());

    println!("\n--- Character-by-character traversal ---");
    for (i, c) in s.chars().enumerate() {
        println!("Character {}: '{}' (Unicode: U+{:04X})", i, c, c as u32);
    }

    println!("\n--- Traverse byte by byte ---");
    for (i, b) in s.bytes().enumerate() {
        print!("{:02x} ", b);
        if (i + 1) % 8 == 0 { println!(); }
    }
    println!();

    println!("\n--- Safe Slices ---");
    let ascii_part = &s[0..4];
    println!("ASCII Part &s[0..4]: '{}'", ascii_part);

    let emoji_start = 4;
    let emoji_end = emoji_start + 4;
    let emoji_part = &s[emoji_start..emoji_end];
    println!("Emoji Part &s[{}..{}]: '{}'", emoji_start, emoji_end, emoji_part);

    let code_start = emoji_end;
    let code_end = code_start + 4;
    let code_part = &s[code_start..code_end];
    println!("Code Part &s[{}..{}]: '{}'", code_start, code_end, code_part);

    let combined = format!("{}{}{}", ascii_part, emoji_part, code_part);
    println!("Concatenated Reconstruction: {}", combined);
}

出力:

TEXT
String: Rust🦀Code
Byte Length: 12
Number of characters: 8

--- Character-by-character traversal ---
Character 0: 'R' (Unicode: U+0052)
Character 1: 'u' (Unicode: U+0075)
Character 2: 's' (Unicode: U+0073)
Character 3: 't' (Unicode: U+0074)
Character 4: '🦀' (Unicode: U+1F980)
Character 5: 'C' (Unicode: U+0043)
Character 6: 'o' (Unicode: U+006F)
Character 7: 'd' (Unicode: U+0064)
Character 8: 'e' (Unicode: U+0065)

--- Traverse byte by byte ---
52 75 73 74 f0 9f a6 80 43 6f 64 65

--- Safe Slices ---
ASCII Part &s[0..4]: 'Rust'
Emoji Part &s[4..8]: '🦀'
Code Part &s[8..12]: 'Code'
Concatenated Reconstruction: Rust🦀Code

UTF-8では、ASCII文字は1バイト、絵文字は4バイトを占めます。文字の分割は文字単位で行わなければなりません。そうしないと、実行時にパニックが発生します。.chars().enumerate() を使用すると、文字を1つずつ安全に処理できます。


(5) ▶ サンプル:実用的な文字列の解析とフォーマット(難易度:⭐⭐)

RUST
// ============================================
// String Parsing, Combined Use of Formatting and Linking
// ============================================

fn main() {
    let raw_data = "name=Alice;age=30;city=Beijing";

    println!("Raw Data: {}", raw_data);
    println!("\n--- Parsing Key-Value Pairs ---");

    let pairs: Vec<&str> = raw_data.split(';').collect();
    for pair in pairs {
        let parts: Vec<&str> = pair.split('=').collect();
        if parts.len() == 2 {
            let key = parts[0].trim();
            let value = parts[1].trim();
            println!("{} => {}", key, value);
        }
    }

    println!("\n--- Construct a New String ---");
    let name = "Alice";
    let age = 30;
    let city = "Beijing";
    let summary = format!("{}, {} years old, From {}", name, age, city);
    println!("Abstract: {}", summary);

    let mut report = String::from("Personnel Report\n");
    report.push_str(&format!("Name: {}\n", name));
    report.push_str(&format!("Age: {}\n", age));
    report.push_str(&format!("City: {}\n", city));
    report.push_str("--- End ---");
    println!("\n{}", report);

    let csv_line = ["Alice", "30", "Beijing"].join(",");
    println!("\nCSV Format: {}", csv_line);
}

出力:

TEXT
Raw Data: name=Alice;age=30;city=Beijing

--- Parsing Key-Value Pairs ---
name => Alice
age => 30
city => Beijing

--- Construct a New String ---
Abstract: Alice, 30 years old, From Beijing

Personnel Report
Name: Alice
Age: 30
City: Beijing
--- End ---

CSV Format: Alice,30,Beijing

split() および collect() は、構造化されたテキストを解析するための基本的なパターンです。format! は、1回限りの連結に適しています。push_str() は、長いテキストを段階的に構築するのに適しています。join() は、区切り文字で区切られた配列を連結するのに適しています。


❓ よくある質問

Q なぜRustには2種類の文字列があるのですか?
A 所有権とパフォーマンスのバランスをとるためです。 &strは読み取り専用の借用(オーバーヘッドなし)であるのに対し、Stringは所有権を持つ可変文字列(ヒープ割り当てのオーバーヘッドあり)です。関数の引数として&strを使用すると、Stringが自動的に&strに変換されるため、最も柔軟性が高くなります。
Q String と &str の相互変換はどのように行いますか?
A &str を String に変換するには .to_string() または String::from() を使用し、String を &str に変換するには &s または s.as_str() を使用します。いずれの変換もコストはかかりません(これらは単に、異なる観点からメモリを表しているに過ぎないからです)。
Q なぜ s[0] では最初の文字にアクセスできないのですか?
A UTF-8 は可変長エンコーディングだからです。s[0] は最初のバイトにアクセスしますが、そのバイトが必ずしも完全な文字であるとは限りません。Rust は安全性を重視して設計されており、実行時に予期しない動作が発生するよりは、コンパイル時にエラーを発生させることを優先します。
Q 文字列の連結には「+」と「format!」のどちらを使うべきですか?
A 文字列の数が少ない場合は「+」を、複数のセグメントがある場合は「format!」を使用してください。+ は左側の String を消費してそのバッファを再利用しますが(高性能化のため)、format! は変数を消費せず、まったく新しい String を作成します。2つまたは3つのセグメントの場合は + を、3つ以上の場合は format! を使用してください。
Q 文字列は変更できますか? &str と String のどちらが変更可能ですか?
A 変更できるのは String のみです(mut を宣言した後、.push_str().push().insert() などのメソッドを使用できます)。&strは読み取り専用の参照であり、それが指すデータは変更できません。関数内で文字列を変更する必要がある場合は、パラメータの型を&mut Stringにする必要があります。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐String を作成し、.push_str().push()+format! の各演算子を順番に呼び出し、4つの手法の結果を比較してください。
  2. 難易度 ⭐⭐: 文字列(スペースで区切られたもの)の最初の単語を返す関数 fn first_word(s: &str) -> &str を作成してください。それぞれ &strString を使用して呼び出してください。
  3. 難易度 ⭐⭐⭐:UTF-8の境界に関する問題を調査しましょう。絵文字 🦀 "Rust🦀" を含む文字列を作成し、&s[0..5]&s[0..6] を使ってその文字列をスライスしてみて、どちらが正常に動作し、どちらがクラッシュするかを観察してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%