Rustの文字列に関する総合ガイド:&strとStringの秘密
Rustには、文字列型が1つではなく2つあります。これは、システムプログラミングではメモリを精密に制御する必要があるためです。
Rustにおける文字列の扱いは、多くの初心者にとって混乱の種となっています。「なぜStringと&strという2つの型があるのか?」「これらはC言語のchar*とどう違うのか?」このレッスンでは、その点を完全に理解できるよう解説します。
1. 学習内容
- 文字列リテラル
&strとヒープ上に割り当てられた文字列Stringの違い - 作成、追加、連結、および書式設定
String - 文字列のスライシングとインデックスによるアクセス
- 一般的な文字列メソッド(len、contains、replace、trim)
- UTF-8 エンコーディングが文字列操作に与える影響
2. ある司書の物語
(1) 問題:JSONデータが長すぎて収まらない
マリアは図書館員で、5,000冊の書籍を管理するための小規模な図書検索システムを開発しています。
- 各書籍のタイトルの長さはさまざまで、最も長いものは200文字です
- 当初、彼女は書籍のタイトルを格納するために固定長の文字列を使用していた(C言語の
char[50]のようなもの)。 - 結局のところ、「百年の孤独」は収まるが、「無限の宇宙の完全な地図の作り方」は切り詰められてしまう。
char*に切り替えた後、また別の メモリ解放 の問題に遭遇しました。ある箇所でfreeを呼び出すのを忘れてしまい、システムがクラッシュしてしまったのです。
「メモリを自動的に管理してくれるストリングがあれば、本当に素晴らしいのに……」彼女はその後、Rustでその答えを見つけた。
(2) 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) メモリモデル
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) 作成方法
// &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) ▶ サンプル:文字列の作成と変更(難易度 ⭐)
// ============================================
// 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);
}
出力:
After the addition: Hello, Rust!
After concatenation: Hello, World!
format Result: tic-tac-toe
+演算子は、その左側にあるStringを消費(移動)するため、連結後はs1を使用できなくなります。format!は変数を一切消費せず、すべての変数の所有権は保持されます。
(2) ▶ サンプル:文字列のスライシングとインデックス操作 (難易度 ⭐⭐)
// ============================================
// 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!();
}
出力:
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) ▶ サンプル:一般的な文字列メソッド (難易度 ⭐⭐)
// ============================================
// 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);
}
出力:
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 文字列の反復処理と境界チェック (難易度 ⭐⭐⭐)
// ============================================
// 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);
}
出力:
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) ▶ サンプル:実用的な文字列の解析とフォーマット(難易度:⭐⭐)
// ============================================
// 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);
}
出力:
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()は、区切り文字で区切られた配列を連結するのに適しています。
❓ よくある質問
&strは読み取り専用の借用(オーバーヘッドなし)であるのに対し、Stringは所有権を持つ可変文字列(ヒープ割り当てのオーバーヘッドあり)です。関数の引数として&strを使用すると、Stringが自動的に&strに変換されるため、最も柔軟性が高くなります。.to_string() または String::from() を使用し、String を &str に変換するには &s または s.as_str() を使用します。いずれの変換もコストはかかりません(これらは単に、異なる観点からメモリを表しているに過ぎないからです)。s[0] は最初のバイトにアクセスしますが、そのバイトが必ずしも完全な文字であるとは限りません。Rust は安全性を重視して設計されており、実行時に予期しない動作が発生するよりは、コンパイル時にエラーを発生させることを優先します。+ は左側の String を消費してそのバッファを再利用しますが(高性能化のため)、format! は変数を消費せず、まったく新しい String を作成します。2つまたは3つのセグメントの場合は + を、3つ以上の場合は format! を使用してください。String のみです(mut を宣言した後、.push_str()、.push()、.insert() などのメソッドを使用できます)。&strは読み取り専用の参照であり、それが指すデータは変更できません。関数内で文字列を変更する必要がある場合は、パラメータの型を&mut Stringにする必要があります。📖 まとめ
- Rustには2種類の文字列があります:
&str(読み取り専用借用)とString(所有権)です。 Stringは、ヒープ上に割り当てられた、サイズ変更可能な UTF-8 文字列であり、スコープ外になると自動的に解放されます。&strは、有効な UTF-8 バイトの連続したシーケンスを指す文字列のスライスです- UTF-8 エンコーディングのため、文字列は インデックスによる直接アクセスに対応していません。
.chars()またはバイトスライスを使用する必要があります。 - 一般的な手法:
.len()、.trim()、.contains()、.replace()、.split() - 関数の引数が文字列を受け付ける場合は、柔軟性を最大限に確保するため、可能な限り
&strを使用してください
📝 練習問題
- 難易度 ⭐:
Stringを作成し、.push_str()、.push()、+、format!の各演算子を順番に呼び出し、4つの手法の結果を比較してください。 - 難易度 ⭐⭐: 文字列(スペースで区切られたもの)の最初の単語を返す関数
fn first_word(s: &str) -> &strを作成してください。それぞれ&strとStringを使用して呼び出してください。 - 難易度 ⭐⭐⭐:UTF-8の境界に関する問題を調査しましょう。絵文字 🦀
"Rust🦀"を含む文字列を作成し、&s[0..5]と&s[0..6]を使ってその文字列をスライスしてみて、どちらが正常に動作し、どちらがクラッシュするかを観察してください。



