Rustの所有権システム、参照(ポインタ)、関数への渡し方について、String型と&str型の両方を含めて整理します。
1. 2つの文字列型
String型(ヒープ、所有権あり)
let s1 = String::from("Hello");
- ヒープに確保
- 所有権を持つ
- 可変にできる
- サイズが動的
&str型(静的領域、参照)
let s2 = "Hello";
- プログラムバイナリに埋め込まれる
- 参照(借用)
- 不変
- サイズが固定
2. 関数への渡し方(完全版)
パターン1: Stringを値渡し(ムーブ)
fn main() {
let s1 = String::from("Hello");
take_string(s1); // 所有権がムーブ
// println!("{}", s1); // ❌ エラー
}
fn take_string(s: String) {
println!("{}", s);
} // メモリ解放
パターン2: Stringの参照を渡す
fn main() {
let s1 = String::from("Hello");
borrow_string(&s1); // 参照を渡す
println!("{}", s1); // ✓ OK
}
fn borrow_string(s: &String) {
println!("{}", s);
}
パターン3: &strを値渡し(コピー)
fn main() {
let s2 = "Hello";
take_str(s2); // コピー(参照のコピー)
println!("{}", s2); // ✓ OK
}
fn take_str(s: &str) {
println!("{}", s);
}
パターン4: Stringを&strとして渡す(推奨)
fn main() {
let s1 = String::from("Hello");
print_str(&s1); // &String → &str(自動変換)
println!("{}", s1); // ✓ OK
}
fn print_str(s: &str) {
println!("{}", s);
}
3. 完全な比較表
| 変数の型 | 宣言 | メモリ | 所有権 | 関数の引数 | 呼び出し | 元の変数 |
|---|---|---|---|---|---|---|
| String | String::from(“Hello”) | ヒープ | あり | s: String | func(s) | ❌ 使えない |
| String | String::from(“Hello”) | ヒープ | あり | s: &String | func(&s) | ✓ 使える |
| String | String::from(“Hello”) | ヒープ | あり | s: &mut String | func(&mut s) | ✓ 使える |
| &str | “Hello” | 静的 | なし | s: &str | func(s) | ✓ 使える |
| String | String::from(“Hello”) | ヒープ | あり | s: &str | func(&s) | ✓ 使える |
4. メモリ配置の比較
String型
let s1 = String::from("Hello");
スタック ヒープ ┌──────────┐ ┌─────────────┐ │ s1 │─────→│ H e l l o │ │ ptr/len │ └─────────────┘ └──────────┘
&str型
let s2 = "Hello";
スタック プログラムバイナリ(静的領域) ┌──────────┐ ┌─────────────┐ │ s2 │─────→│ H e l l o │ │ ptr/len │ └─────────────┘ └──────────┘
&String型
let s1 = String::from("Hello");
let r = &s1;
スタック スタック ヒープ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │ r │─────→│ s1 │─────→│ H e l l o │ │ ptr │ │ ptr/len │ └─────────────┘ └──────────┘ └──────────┘
5. 関数の引数の推奨パターン
パターン1: &strを受け取る(最も汎用的・推奨)
fn print_text(s: &str) {
println!("{}", s);
}
fn main() {
let s1 = String::from("Hello");
let s2 = "World";
print_text(&s1); // String → &str
print_text(s2); // &str → &str
}
メリット: Stringも&strも受け取れる
パターン2: &Stringを受け取る(限定的)
fn print_string(s: &String) {
println!("{}", s);
}
fn main() {
let s1 = String::from("Hello");
let s2 = "World";
print_string(&s1); // ✓ OK
// print_string(s2); // ❌ エラー
print_string(&s2.to_string()); // 変換が必要
}
デメリット: &strを直接受け取れない
パターン3: Stringを受け取る(消費する)
fn consume(s: String) {
println!("{}", s);
}
fn main() {
let s1 = String::from("Hello");
let s2 = "World";
consume(s1); // ✓ OK(ムーブ)
consume(s2.to_string()); // 変換が必要
}
6. 実践的なガイドライン
関数の引数として推奨される型
| 用途 | 推奨型 | 理由 |
|---|---|---|
| 読み取りのみ | &str | Stringも&strも受け取れる |
| 変更する | &mut String | 元の変数を変更できる |
| 消費する | String | 所有権を取得 |
| 返す | String | 新しい値を返す |
変数宣言の使い分け
| 用途 | 推奨型 | 宣言 |
|---|---|---|
| 固定文字列 | &str | let s = “Hello”; |
| 動的文字列 | String | let s = String::from(“Hello”); |
| 変更する文字列 | String | let mut s = String::from(“Hello”); |
7. 複数の参照のルール
ルール1: 不変参照は複数OK
fn main() {
let s = String::from("Hello");
let r1 = &s; // ✓ OK
let r2 = &s; // ✓ OK
let r3 = &s; // ✓ OK
println!("{}, {}, {}", r1, r2, r3);
}
ルール2: 可変参照は1つだけ
fn main() {
let mut s = String::from("Hello");
let r1 = &mut s; // ✓ OK
// let r2 = &mut s; // ❌ エラー!
println!("{}", r1);
}
ルール3: 不変と可変は同時に持てない
fn main() {
let mut s = String::from("Hello");
let r1 = &s; // ✓ OK(不変参照)
let r2 = &s; // ✓ OK(不変参照)
// let r3 = &mut s; // ❌ エラー!
println!("{}, {}", r1, r2);
}
8. 完全な実践例
fn main() {
// String型
let s1 = String::from("Hello");
// &str型
let s2 = "World";
// パターン1: &strを受け取る(推奨)
print_any(&s1); // String → &str
print_any(s2); // &str → &str
// パターン2: &Stringを受け取る
print_string_ref(&s1); // ✓ OK
// print_string_ref(s2); // ❌ エラー
// パターン3: Stringを受け取る(消費)
let s3 = String::from("Rust");
consume_string(s3);
// println!("{}", s3); // ❌ エラー
// パターン4: 可変参照
let mut s4 = String::from("Hello");
append_world(&mut s4);
println!("{}", s4); // "Hello World"
}
// 最も汎用的(推奨)
fn print_any(s: &str) {
println!("{}", s);
}
// String専用
fn print_string_ref(s: &String) {
println!("{}", s);
}
// 所有権を取得
fn consume_string(s: String) {
println!("{}", s);
}
// 変更する
fn append_world(s: &mut String) {
s.push_str(" World");
}
9. Copyトレイトを持つ型との違い
String(Copyではない)
fn main() {
let s = String::from("Hello");
func(s); // ムーブ
// println!("{}", s); // ❌ エラー
}
fn func(s: String) {
println!("{}", s);
}
i32(Copyトレイト)
fn main() {
let x = 42;
func(x); // コピー
println!("{}", x); // ✓ OK!
}
fn func(n: i32) {
println!("{}", n);
}
&str(Copyトレイト)
fn main() {
let s = "Hello";
func(s); // コピー
println!("{}", s); // ✓ OK!
}
fn func(s: &str) {
println!("{}", s);
}
理由: &strは参照なので、ポインタと長さ(16バイト)だけをコピーする
10. まとめ図
┌─────────────────────────────────────────────────────┐
│ 文字列型と関数への渡し方 │
└─────────────────────────────────────────────────────┘
String型(所有権あり)
let s1 = String::from("Hello");
├─ func(s1) → 所有権ムーブ、元の変数❌
├─ func(&s1) → 参照、元の変数✓
├─ func(&mut s1) → 可変参照、元の変数✓(変更される)
└─ func(&s1) (&str) → &strとして渡す、元の変数✓
&str型(参照、所有権なし)
let s2 = "Hello";
└─ func(s2) → コピー、元の変数✓
推奨パターン
関数の引数: fn func(s: &str)
理由: StringもStrも受け取れる
11. 重要なポイント
- 関数の引数には
&strを使うのが最も汎用的Stringも&strも受け取れるため &strはCopyトレイトを持つ
関数に渡しても元の変数が使えるStringはCopyトレイトを持たない
関数に渡すと所有権がムーブする- 参照を使えばメモリ効率が良い
データのコピーが発生しない - 不変参照は複数持てるが、可変参照は1つだけ
データ競合を防ぐため
12. 結論
基本的には不変参照(&T)を使い、変更が必要なら可変参照(&mut T)、値を消費する場合のみ所有権を渡す。
文字列を扱う関数では、&strを引数にするのが最も推奨される。
No responses yet