Skip to content

Instantly share code, notes, and snippets.

@kenmori
Last active February 15, 2025 11:01
Show Gist options
  • Save kenmori/73a6fdd11500321838cf8492ba51fcdc to your computer and use it in GitHub Desktop.
Save kenmori/73a6fdd11500321838cf8492ba51fcdc to your computer and use it in GitHub Desktop.
【WIP】Rust練習問題集(Rust practice questions)

Rust練習問題集(Rust practice questions)

Author

Rust Playground

Rust練習問題

INDEX

  • 所有権のムーブ
  • 所有権のクローン
  • 不変借用
  • 可変借用
  • 二重可変借用
  • 参照のスコープ
  • ダングリングポインタ
  • 明示的なライフタイム
  • 構造体とライフタイム
  • ライフタイムとスコープ
  • 可変参照の制約
  • 借用後の変更
  • 参照を返す関数
  • Optionとライフタイム

1. 所有権のムーブ

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // ここで所有権が移動する
    
    // TODO: s1 を println! で出力しよう
}

修正後の結果: コンパイルエラーになる(s1 の所有権が s2 に移動しているため)

答え
  • s2 を使う(s1 は使わない)
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 の所有権が s2 に移動

    println!("{}", s2); // s2 を出力する
}

s1 は s2 に所有権が移動して無効になるため、s1 を使わず s2 を出力すればエラーにならない。

  • clone() を使う
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // s1 のクローンを作成

    println!("{}", s1); // s1 もそのまま使える
}

clone() を使うとヒープデータのコピーが作成され、s1 と s2 の両方を使用できる。

  • 借用(参照)を使う
fn main() {
    let s1 = String::from("hello");
    let s2 = &s1; // s1 を参照する

    println!("{}", s1); // s1 も使える
    println!("{}", s2); // s2 も使える
}

s2 は s1 の所有権を奪わずに 参照(不変借用) するため、両方とも有効。

  • format! を使う
fn main() {
    let s1 = String::from("hello");
    let s2 = format!("{}", s1); // s1 の内容をコピーして新しい String を作成

    println!("{}", s1); // s1 も使える
}

format! は s1 の内容をコピーして新しい String を作るため、s1 の所有権は保持される。

2. 所有権のクローン

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // クローンを作成
    
    println!("{}", s1); // TODO: s1 を使ってもエラーが出ないようにする
}

修正後の結果: "hello" が 2 回表示される

答え
fn main() {
    let s1 = String::from("Hello");
    let s2 = s1.clone();
    println!("s1 is {}", s1);
    println!("s2 is {}", s2);
}

clone() を使うことで、s1 のヒープデータが新しくコピーされ、 s1 と s2 の両方が String のデータを持つようになります。 そのため、s1 を println! で出力してもエラーになりません。

3. 不変借用

fn print_length(s: &String) {
    println!("Length: {}", s.len());
}

fn main() {
    let s = String::from("hello");
    
    // TODO: s を print_length に渡してコンパイルが通るようにする
}

修正後の結果: "Length: 5" が表示される

答え

このコードがエラーになるのは、print_length 関数の引数 s が &String(不変参照)を期待しているのに、main の s をそのまま渡すと所有権が移動しようとするから

  • 不変借用で渡す
fn print_length(s: &String) {
    println!("Length: {}", s.len());
}

fn main() {
    let s = String::from("hello");

    print_length(&s); // ✅ 不変借用で渡す
}

&s を渡すことで、print_length は s の参照を受け取るだけになり、所有権は移動しません。 s は print_length 内で変更されることなく、安全に使えます

  • 型を変える
fn print_length(s: &str) { // ✅ &str を受け取る
    println!("Length: {}", s.len());
}

fn main() {
    let s = String::from("hello");

    print_length(&s); // ✅ &String は &str に自動変換される
}

String は &str に 自動的に変換(Deref Coercion) されるため、print_length の引数を &str にすると、&String でも &str でも受け取れるようになります。 より柔軟な設計になるため、String だけでなくリテラル("hello")も渡せます。

所有権を移動させる

fn print_length(s: String) { // ✅ String をそのまま受け取る
    println!("Length: {}", s.len());
}

fn main() {
    let s = String::from("hello");

    print_length(s); // ❌ s の所有権が移動し、以降 `s` は使えない
}

s を print_length に渡すと所有権が移動するため、main 内で s を再利用できなくなります。 一度しか使わない場合は問題ありませんが、再利用する場合は適していません。

まとめ ① &String(不変借用) s をそのまま使い続けられる &str に比べて少し柔軟性が低い ② &str を受け取る String も &str も受け取れる 特になし(&str が推奨される場面が多い) ③ String を移動 s を完全に渡せる s を後で使えない

基本的には ① または ② を使うのがベスト

4. 可変借用

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut s = String::from("hello");
    
    // TODO: s を可変借用して add_exclamation を呼び出す
    println!("{}", s);
}

修正後の結果: "hello!" が表示される

答え

このコードがエラーになるのは、add_exclamation 関数が &mut String(可変参照)を受け取るのに、main の s をそのまま渡すと所有権が移動しようとするから

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut s = String::from("hello");
    
    add_exclamation(&mut s); // ✅ 可変借用で渡す
    
    println!("{}", s); // 出力: hello!
}
  • add_exclamation(s: &mut String) は 可変な参照 (&mut String) を受け取る。
  • add_exclamation(&mut s); で s を可変借用 (&mut s) し、関数に渡す。
  • push('!') で s に ! が追加される。
  • println!("{}", s); の結果は "hello!" になる。

② 戻り値で String を返す

fn add_exclamation(s: String) -> String { // ✅ String を受け取って返す
    let mut s = s;
    s.push('!');
    s
}

fn main() {
    let s = String::from("hello");
    
    let s = add_exclamation(s); // ✅ 所有権を移動して受け取る
    
    println!("{}", s); // 出力: hello!
}

String の所有権を add_exclamation に渡し、関数内で push してから String を返す。 s の所有権を再び main に戻せるので、関数を抜けた後も s を利用可能。 関数の中で s を完全に処理し終える場合はこの方法もあり。

③ &str を受け取って新しい String を作る

fn add_exclamation(s: &str) -> String { // ✅ &str を受け取り新しい String を返す
    let mut new_s = String::from(s);
    new_s.push('!');
    new_s
}

fn main() {
    let s = String::from("hello");
    
    let s = add_exclamation(&s); // ✅ &String は &str に自動変換される
    
    println!("{}", s); // 出力: hello!
}

&str を受け取り、新しい String を作成して返す。 元の s を変更しないので、不変参照のまま使える。 String を変更したくない場合に有効。

まとめ

  • &mut String(可変借用)・・・メモリ割り当てが少なく、効率的。だが、可変借用の制約を受ける
  • String を受け取って返す・・・s を完全に処理できる。だが、関数を抜けるたびに所有権を受け渡す必要がある
  • &str を受け取って新しい・・・String を作る。元の s を変更せずに使える。だが、String を新しく作るためメモリ消費が増える

基本的には ① 可変借用 を使うのがベストですが、s を後で使う必要がないなら ②、元の s を変更したくないなら ③ が良い選択肢になります。

5. 二重可変借用(エラー)

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &mut s;
    let r2 = &mut s; // TODO: ここを修正してコンパイルエラーを回避する
    
    println!("{}", r1);
}

修正後の結果: 二重可変借用を回避して s を適切に扱う

答え

このコードがコンパイルエラーになる理由は Rust では同時に 2 つの可変参照を持つことができない ためです。

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
        println!("{}", r1); // ✅ r1 のスコープを明示的に終了
    } // `r1` はここでドロップされる

    let r2 = &mut s; // ✅ r1 のスコープが終わったのでOK
    println!("{}", r2);
}

r1 のスコープを {} で囲み、println!("{}", r1); の実行後に r1 を解放。 r1 が使われなくなった後に r2 を作ることで、二重可変借用を回避。

  1. 可変参照を使わずに String を直接変更する
fn main() {
    let mut s = String::from("hello");

    s.push_str(", world!"); // ✅ 直接 `s` を変更
    println!("{}", s);
}

s.push_str(", world!"); のように、可変参照なしで String を変更。 可変参照を使わなくても済む場合はこの方法がシンプル。

3.可変参照を使わずに String を受け取る関数を作る

fn modify_string(mut s: String) -> String { // ✅ 所有権を移動して変更
    s.push_str(", world!");
    s
}

fn main() {
    let s = String::from("hello");
    
    let s = modify_string(s); // ✅ 新しい `s` を受け取る
    
    println!("{}", s);
}

modify_string が String の所有権を受け取り、変更後に返す。 s の所有権を移動するので、二重借用の問題が起こらない。

まとめ

① スコープを明示的に管理(推奨) メモリ効率が良く、Rust の所有権ルールに沿った設計 スコープの管理が必要 ② 可変参照なしで String を直接変更 シンプルで安全 String に直接変更できる場合に限る ③ 所有権を移動して関数で変更 String を安全に変更できる String を毎回受け渡す必要がある

基本的には ① スコープを明示的に管理する方法がベスト ですが、場合によっては ② や ③ の方法も有効 です。

6. 参照のスコープ

fn main() {
    let mut s = String::from("hello");

    {
        let r = &mut s;
        r.push_str(", world");
    } // ここで r はスコープを抜ける
    
    // TODO: s を println! で出力しよう
}

修正後の結果: "hello, world" が表示される

答え

この問題は 参照のスコープを適切に管理する ことがポイントです。

Rust では 不変参照(&s)と可変参照(&mut s)を同時に持つことはできません。 ただし、スコープを適切に管理すれば安全に扱えます。

fn main() {
    let mut s = String::from("hello");

    {
        let r = &mut s;
        r.push_str(", world"); // ✅ r のスコープ内で変更
    } // ✅ r のスコープ終了

    println!("{}", s); // ✅ ここでは s を安全に使用できる
}

r は s の可変参照を持っているため、そのスコープ {} の中でのみ s を変更可能。 {} のスコープを抜けた後は s の所有権が戻るため、println!("{}", s); で安全に出力できる。

  1. 可変借用を使わずに String を直接変更
fn main() {
    let mut s = String::from("hello");

    s.push_str(", world"); // ✅ 直接 `s` を変更
    println!("{}", s);
}

s.push_str(", world"); のように、可変参照なしで s を直接変更。 可変参照を使う必要がなければ、この方法が最もシンプル。

3.不変参照と可変参照のスコープを分ける

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2); // ✅ ここで r1, r2 のスコープが終了

    let r3 = &mut s; // ✅ r1, r2 のスコープが終わった後なのでOK
    r3.push_str(", world");
    
    println!("{}", r3);
}

r1, r2 の不変参照のスコープが終わった後 に r3 の可変参照を作る。 不変参照と可変参照を同時に持たないように管理 することでコンパイルエラーを防ぐ。

まとめ

① {} でスコープ管理(推奨) スコープを明示的に制御し、安全に s を使用できる {} を適切に配置する必要がある ② String を直接変更 余計な参照を持たずに済む 可変参照を使いたい場合には適用できない ③ 不変参照と可変参照のスコープを分ける 不変参照を使いながら安全に s を変更できる スコープの管理が複雑になる

基本的には ① のスコープ管理を意識するのがベスト ですが、場合によっては ② や ③ の方法も有効 です。

7. ダングリングポインタ(エラー)

fn dangle() -> &String {
    let s = String::from("hello");
    &s // TODO: ここを修正してコンパイルエラーを回避する
}

fn main() {
    let s = dangle();
    println!("{}", s);
}

修正後の結果: dangle の戻り値を所有権付きで返すようにする

  • 関数内で作った値の参照を返すと、その値が破棄された後に参照を使うことになる(ダングリングポインタ)。
  • 所有権を渡す (String を返す) のが正しい解決策。
答え この問題は ダングリングポインタ(参照が無効なメモリを指す)を防ぐ ことがポイントです。

Rust では、関数のスコープ内で作成した値の参照を返すと、スコープ終了時に値が破棄され、参照が無効になる ためコンパイルエラーが発生します。

エラーが発生する理由

fn dangle() -> &String {
    let s = String::from("hello"); // ✅ dangle 関数のスコープ内で `s` を作成
    &s // ❌ `s` の参照を返すが、関数終了時に `s` は破棄される
}

fn main() {
    let s = dangle(); // ❌ dangle の戻り値を使うが、`s` の参照は無効
    println!("{}", s);
}

s は dangle() 関数内で作成された String なので、関数終了と同時に 解放される。 そのため &s を返すと、 参照が無効なメモリを指すことになり、コンパイルエラー になる。

  1. 所有権を返す
fn dangle() -> String {
    let s = String::from("hello");
    s // ✅ 所有権を返す
}

fn main() {
    let s = dangle();
    println!("{}", s); // 出力: hello
}

String の所有権を関数の呼び出し元に渡す ことで、スコープを超えて s を安全に使える。

② &'static str を返す

fn dangle() -> &'static str {
    "hello" // ✅ 文字列リテラルは `'static` ライフタイムを持つため安全
}

fn main() {
    let s = dangle();
    println!("{}", s); // 出力: hello
}

"hello" は 文字列リテラル であり、プログラムが終了するまで メモリから消えない ('static ライフタイム) ので、参照を安全に返せる。

  1. 参照を外部から受け取る
fn dangle(s: &String) -> &String {
    s // ✅ 呼び出し元の `String` を参照するので安全
}

fn main() {
    let s = String::from("hello");
    let s_ref = dangle(&s);
    println!("{}", s_ref); // 出力: hello
}

引数として渡された String の参照をそのまま返す ため、関数終了後も s_ref は有効。 ただし、これは 関数内で String を新規作成しない場合のみ有効。

まとめ

① 所有権を返す(推奨) シンプルで安全 String の所有権が移るため、元の変数は使えなくなる ② 'static を返す 文字列リテラルなら安全 使えるのはリテラルのみ ③ 引数の参照を返す 呼び出し元の String をそのまま使える dangle 内で String を作成できない

基本的には ① の所有権を返す方法が最も柔軟で安全 ですが、状況に応じて 'static や 参照を受け取る方法を使い分ける のがベストです。

8. 明示的なライフタイム

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");

    let result = longest(&s1, &s2);
    println!("{}", result);
}

実行結果: "Longest: world!!!"

  • ライフタイムを 'a のように指定することで、参照が無効にならないようにする。
答え

元のコードでは、関数 longest は 2 つの参照 &str を受け取り、そのうちの長い方を返します。しかし、Rust では参照のライフタイムを明示的に指定しないと、関数のスコープを抜けたときに参照が無効になる可能性があるため、コンパイルエラーになります。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");
    let result = longest(&s1, &s2);
    println!("{}", result);
}

これにより、Rust は x または y のどちらかを安全に返すことができると判断し、コンパイルが通るようになります。

2.String を返すようにする

ライフタイムを考慮せずに値を返す場合、String を返せば所有権が移るため、ライフタイムの問題を回避できます。

fn longest(x: &str, y: &str) -> String {
    if x.len() > y.len() {
        x.to_string()
    } else {
        y.to_string()
    }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");
    let result = longest(&s1, &s2);
    println!("{}", result);
}

メリット

ライフタイムの制約が不要になる。 返り値を所有できるため、元のデータがスコープを抜けても使える。

デメリット

to_string() によるメモリの割り当てが発生し、パフォーマンスのオーバーヘッドがある。

  1. Cow を使う std::borrow::Cow(Copy-On-Write)を利用すると、必要に応じて String に変換しつつ、参照のまま使うことも可能になります。
use std::borrow::Cow;

fn longest<'a>(x: &'a str, y: &'a str) -> Cow<'a, str> {
    if x.len() > y.len() {
        Cow::Borrowed(x) // そのまま借用
    } else {
        Cow::Owned(y.to_string()) // String に変換
    }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");
    let result = longest(&s1, &s2);
    println!("{}", result);
}

メリット 可能な限り参照 (&str) を維持し、必要なら String を作成するため、無駄なメモリ確保を減らせる。 to_string() を毎回呼ぶよりも柔軟。

デメリット Cow を理解する必要があるため、コードの可読性がやや下がる。

  1. 参照ではなく、usize を返してインデックスを取得する ライフタイムの問題を回避する方法として、長い方の文字列そのものではなく 長さの情報だけを返す 方法もある。
fn longest(x: &str, y: &str) -> usize {
    if x.len() > y.len() {
        x.len()
    } else {
        y.len()
    }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");
    let result = longest(&s1, &s2);
    println!("Longest length: {}", result);
}

メリット

ライフタイムの問題を完全に回避できる。 usize は Copy なので、所有権の移動もなくシンプル。

デメリット どの文字列が長いか分からなくなるため、結果としてどちらの文字列を取得するかの処理が増える可能性がある。

  • 'a ライフタイムを指定・・・メモリ効率が良い。が、ライフタイムの概念を理解する必要がある
  • String を返す・・・使いやすく所有権の問題なし。が、毎回メモリを確保するオーバーヘッドあり
  • Cow を返す・・・可能な限り &str を維持。が、Cow の知識が必要
  • usize を返す・・・ライフタイム不要でシンプル。が、文字列そのものを得られない

どの方法を選ぶかは用途次第ですが、基本的には ライフタイムを明示する方法が最も一般的

9. ライフタイムとスコープ

fn main() {
    let r;

    {
        let x = 5;
        r = &x; // TODO: ここを修正してコンパイルエラーを回避する
    }
    
    println!("{}", r);
}

修正後の結果: x のライフタイムを適切に管理して r を有効にする

  • 参照を持つ構造体はライフタイムパラメータが必要。
答え

このコードの問題は、r に x の参照を代入しているが、x は {} のブロックを抜けると スコープを抜けて無効 になってしまうことです。Rust では 借用が無効になる参照を保持することは許されません。

  1. 参照のスコープを広げる 変数 x を r よりも長く生存させることで、コンパイルエラーを解消します。
fn main() {
    let x = 5;
    let r = &x; // ✅ x のスコープが有効
    println!("{}", r);
}

メリット

シンプルで、ライフタイムを考えずに済む。

デメリット

x を早い段階で定義する必要がある。

  1. 参照ではなく、値をコピーする 整数型 (i32) は Copy トレイトを実装しているため、参照ではなく 値をそのまま代入 することで問題を回避できます。
fn main() {
    let r;

    {
        let x = 5;
        r = x; // ✅ 値をコピー
    }

    println!("{}", r); // ✅ 参照ではないので問題なし
}

メリット

値のコピーなのでライフタイムの問題が発生しない。

デメリット

Copy を持たない型(String など)では使えない。

  1. String のような Copy できない型を clone する もし String のような Copy できない型 で同じ問題が起きた場合、clone() を使って所有権を持つ形で代入すれば問題ありません。
fn main() {
    let r;

    {
        let x = String::from("hello");
        r = x.clone(); // ✅ `x` をクローンして新しい所有権を作成
    }

    println!("{}", r);
}

メリット

String のような型でも問題なく使用できる。

デメリット

clone() によるメモリの確保が発生する。

  1. r のライフタイムを明示的に指定する もし r が関数の外にあるようなケースなら、ライフタイムパラメータ を使って参照のスコープを明示的に指定する方法もあります。
fn longest_lived<'a>(x: &'a i32) -> &'a i32 {
    x
}

fn main() {
    let x = 5;
    let r = longest_lived(&x); // ✅ ライフタイム 'a を保証
    println!("{}", r);
}

メリット

関数の引数にライフタイムを適用できる。 他のデータとのライフタイム関係を明示的に表現できる。

デメリット

シンプルなケースでは不要なライフタイム指定が増える。

  1. Box を使ってヒープに保存する もし x を スコープ外に出しても使いたい 場合、Box を使って ヒープに値を保持する 方法もあります。
fn main() {
    let r;

    {
        let x = Box::new(5); // ✅ `x` をヒープに確保
        r = x; // ✅ `r` が所有権を持つ
    }

    println!("{}", r);
}

メリット

スタック上のスコープを超えてもデータが保持される。

デメリット

Box を使うため、ヒープメモリを確保するオーバーヘッドがある。

まとめ

  • スコープを広げる・・・シンプルで直感的。が、x を早く定義する必要あり
  • 値をコピーする・・・Copy 型なら問題なし。が、String などでは使えない
  • clone() を使う・・・String などのCopy 不可型にも対応。が、clone() のコストが発生
  • ライフタイムを指定する・・・関数間でライフタイムを明示できる。が、単純なケースでは冗長
  • Box を使う・・・スコープ外に持ち出せる。が、ヒープ確保のオーバーヘッドあり

最も基本的なのは スコープを広げる方法 ですが、状況に応じて コピー や Box を使う方法も有効です。

10. 構造体とライフタイム

struct Important {
    text: &str,
}

fn main() {
    let s = String::from("Rust is great!");
    let i = Important { text: &s };

    println!("{}", i.text);
}

修正後の結果: "Rust is great!"

答え

このコードは、構造体 Important のフィールド text が &str 型の参照を持っていますが、ライフタイムを明示的に指定していないため、コンパイルエラーになります。

Rust では 参照を含む構造体にはライフタイムを明示する必要 があります。そうしないと、text が s よりも長く生存する可能性があり、無効な参照を持つことになります。

  1. 明示的にライフタイムを指定する 参照を持つ構造体には ライフタイムパラメータ を追加し、フィールドのライフタイムを指定することでエラーを解消できます。
struct Important<'a> {
    text: &'a str,
}

fn main() {
    let s = String::from("Rust is great!");
    let i = Important { text: &s }; // ✅ 明示的なライフタイム指定
    println!("{}", i.text);
}

メリット

text のライフタイムが s と一致することが保証される。

デメリット

シンプルなケースではライフタイムを意識しなければならない。

  1. String を直接持たせる ライフタイムの管理が面倒なら、参照ではなく String を直接 Important に持たせることで解決できます。
struct Important {
    text: String, // ✅ String を所有
}

fn main() {
    let s = String::from("Rust is great!");
    let i = Important { text: s }; // ✅ 所有権を `i` に移動
    println!("{}", i.text);
}

メリット

ライフタイムを考える必要がない。 String の所有権を Important に移すので、s がスコープを抜けても問題ない。

デメリット

String のコピーが必要になる場面がある(ただし move すれば回避可能)。

  1. Box を使ってヒープに保存する もし String を所有しつつ、余計なコピーを避けたい場合は Box を使うのも一つの方法です。
struct Important {
    text: Box<str>, // ✅ ヒープ上にデータを保存
}

fn main() {
    let s = String::from("Rust is great!");
    let i = Important { text: s.into_boxed_str() }; // ✅ `String` を `Box<str>` に変換
    println!("{}", i.text);
}

メリット

String を Box に変換すると、ヒープ上に保持できるため、メモリ効率が良い。 String の所有権を移動できる。

デメリット

into_boxed_str() を使う必要がある。

  1. Rc を使って複数の Important インスタンスが共有 もし text を複数の Important インスタンスで共有したい場合、Rc を使うことで 参照カウントを持つ共有所有権 を作れます。
use std::rc::Rc;

struct Important {
    text: Rc<String>, // ✅ `Rc` による共有所有権
}

fn main() {
    let s = Rc::new(String::from("Rust is great!"));
    let i1 = Important { text: Rc::clone(&s) };
    let i2 = Important { text: Rc::clone(&s) };

    println!("{}", i1.text);
    println!("{}", i2.text);
}

メリット

text を複数の Important インスタンスで共有できる。 Rc の clone() は浅いコピー(参照カウントの増加のみ)なので、パフォーマンスが良い。

デメリット

Rc はスレッドセーフではない(マルチスレッド環境では Arc が必要)。

まとめ

  • ライフタイムを指定・・・シンプルな構造でメモリ効率が良い。が、ライフタイム管理が必要
  • String を所有する・・・ライフタイムを気にしなくて済む。が、String の所有権が必要
  • Box を使う・・・メモリ効率が良い。が、into_boxed_str() を使う必要がある
  • Rc を使う・・・textを複数のImportantで共有可能。が、Rcのオーバーヘッドがある

基本的には ライフタイムを明示するのがベスト ですが、状況に応じて String や Rc を活用するとより柔軟に扱えます。

11. 可変参照の制約(エラー)

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s; // ❌ ここでエラー!

    println!("{}, {}", r1, r2);
}
答え

Rust では 同時に複数の可変参照を作成することはできない ため、r1 のスコープが有効なうちに r2 を作成するとコンパイルエラーになります。 これは Rust の 借用ルール によるもので、データ競合を防ぐための仕様です。

  1. スコープを明示的に分ける 最も基本的な解決策は、 一つの可変参照が使われるスコープを終了させてから、新しい可変参照を作成すること です。
fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &mut s;
        println!("{}", r1);
    } // ✅ r1 のスコープを終了させる
    let r2 = &mut s; // ✅ 新しく可変借用
    println!("{}", r2);
}

{} で r1 のスコープを限定し、r1 が使われなくなった後に r2 を作成。

メリット

借用ルールを守りながら、可変参照を安全に扱える。

デメリット

スコープを分ける必要があるので、コードの自由度が少し下がる。

  1. 可変参照を1つだけ使う もし r2 が不要なら、最初の可変参照 (r1) をそのまま使う のが最もシンプルな解決策です。
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s; // ✅ 1つの可変参照のみ使用
    println!("{}", r1);
}

メリット

余計な可変参照を作らないのでシンプル。

デメリット

r2 のような別の変数に代入したい場合には使えない。

  1. std::mem::swap で値を入れ替える もし s の内容を更新しながら使いたいなら、std::mem::swap を使うことで可変参照を持たずにデータを更新できます。
use std::mem::swap;

fn main() {
    let mut s1 = String::from("hello");
    let mut s2 = String::from("world");

    swap(&mut s1, &mut s2); // ✅ 可変参照を同時に使わない
    println!("s1: {}, s2: {}", s1, s2);
}

メリット

同時に2つの可変参照を持つことなく、データを入れ替えられる。

デメリット

変数の入れ替えが目的でない場合には適用できない。

  1. split_at_mut を使って、部分的な可変参照を取得 配列やスライスの場合、split_at_mut を使うと 部分ごとに分けて可変参照を持つ ことができます。
fn main() {
    let mut numbers = [1, 2, 3, 4, 5];

    let (left, right) = numbers.split_at_mut(2); // ✅ 配列を分割して複数の可変参照を取得
    left[0] = 10;
    right[0] = 20;

    println!("{:?}", numbers); // [10, 2, 20, 4, 5]
}

メリット

同時に複数の部分的な可変参照を持つことが可能。

デメリット

配列やスライス限定の手法。

まとめ

  • スコープを明示的に分ける・・・借用ルールを守りながら可変参照を安全に扱える。が、スコープの管理が必要
  • 可変参照を1つだけ使う・・・シンプルで直感的。が、2つ目の可変参照が必要な場合は対応できない
  • std::mem::swap を使う・・・可変参照なしでデータを更新できる。が、変数の入れ替えが目的でない場合には適用できない
  • split_at_mut を使う・・・配列やスライスなら部分ごとに安全に可変参照を取得できる。が、配列やスライス限定

通常は スコープを明示的に分ける か 可変参照を1つだけ使う のが最適ですが、状況に応じて swap や split_at_mut を使うとより柔軟に対応できます。

12. 借用後の変更(エラー)

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    println!("{}", r1);

    s.push_str(", world!");
    println!("{}", s);
}
答え

r1 = &s によって 不変参照(&s)を作成 したことで、s は不変のままの状態を保持しなければなりません。

しかし s.push_str(", world!") は s を 可変で変更 する処理であるため、不変借用がある状態で可変借用を行うことは許されずエラーになります。

  1. スコープを明示的に分ける 最も簡単な方法は、 不変参照のスコープを終了させた後に s を変更する ことです。
fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &s;
        println!("{}", r1);
    } // ✅ r1 のスコープを終了させる
    s.push_str(", world!"); // ✅ 問題なし
    println!("{}", s);
}

{} を使って r1 のスコープを制限 し、s.push_str() の前に r1 が使えなくなるようにする。

メリット

シンプルで可読性が高い。

デメリット

一時的にスコープを制限する必要があるため、使いどころに注意。

  1. to_owned() を使ってコピー to_owned() や clone() を使って、借用せずに値のコピーを作成 する方法もあります。
fn main() {
    let mut s = String::from("hello");

    let r1 = s.to_owned(); // ✅ s の所有権をコピー
    println!("{}", r1);

    s.push_str(", world!"); // ✅ 問題なし
    println!("{}", s);
}

to_owned() を使うと、s の 新しいコピー (r1) が作成されるため、s を自由に変更できる。

メリット

s のスコープを気にせず変更できる。

デメリット

メモリのオーバーヘッド が発生する(コピーを作成するため)。

  1. format!() を使って新しい文字列を作成 文字列の結合が目的なら、format!() を使うことで s を変更せずに新しい文字列を作る こともできます。
fn main() {
    let s = String::from("hello");

    let new_s = format!("{}, world!", s); // ✅ `s` を変更せずに新しい文字列を作成
    println!("{}", new_s);
}

format!() を使うと、元の s を変更せずに 新しい文字列 (new_s) を作成できる。

メリット

s を変更しないので、安全に扱える。

デメリット

s の コピーが作成されるためメモリ効率が悪くなる可能性がある。

まとめ

  • スコープを明示的に分ける・・・借用ルールを守りながら可変参照を安全に扱える。が、スコープの管理が必要
  • to_owned() を使う・・・s を気にせずコピーして使える。が、メモリのオーバーヘッドが発生
  • format!() を使う・・・s を変更せずに新しい文字列を作成できる。が、メモリ効率が悪くなる可能性がある

通常は スコープを分ける方法が最適ですが、コピーが許容できる場合は to_owned() や format!() を使うと柔軟に対応できます。

13. 参照を返す関数(エラー)

fn get_str() -> &String {
    let s = String::from("Hello, world!");
    &s
}

fn main() {
    let s_ref = get_str();
    println!("{}", s_ref);
}
答え

s は get_str() の ローカル変数 なので、関数が終了すると スコープが終了しメモリが解放 される。 その結果、 &s(参照)を返すことはできない。(ダングリング参照になるためコンパイルエラー)

  1. String の所有権を返す(最も基本的な解決策)
fn get_str() -> String {
    String::from("Hello, world!") // ✅ 所有権を返す
}

fn main() {
    let s_ref = get_str();
    println!("{}", s_ref);
}

&String の 参照ではなく、String を直接返す ことで、スコープの制約を回避。 s の所有権が main() に移動するため、関数のスコープ外でも s を安全に使用可能。

メリット

所有権を持つデータ を返すので、ライフタイムの問題が発生しない。

デメリット

String の ムーブ(所有権の移動) が発生するので、コストがある(ただし String は Box のようなヒープデータなので、大抵は問題にならない)。

  1. 文字列リテラル(&'static str)を使う
fn get_str() -> &'static str {
    "Hello, world!" // ✅ `&'static str`(静的ライフタイム)を返す
}

fn main() {
    let s_ref = get_str();
    println!("{}", s_ref);
}

&'static str は プログラム実行中ずっと有効な文字列リテラル なので、参照を返しても問題なし。

メリット

ライフタイムの問題を回避 できる。 パフォーマンスが良い(String をヒープに確保するコストがない)。

デメリット

リテラルしか扱えない ので、動的に文字列を生成したい場合には不適。

  1. 既存の String の参照を返す もし 外部の String を関数の引数として受け取るなら、参照を返すことが可能。
fn get_str<'a>(s: &'a String) -> &'a String {
    s // ✅ `s` の参照をそのまま返す
}

fn main() {
    let s = String::from("Hello, world!");
    let s_ref = get_str(&s); // ✅ `s` の参照を渡す
    println!("{}", s_ref);
}

s のライフタイムが 'a によって 明示的に管理されている。 main() のスコープ内で s が存続するので、&s を返しても 参照が有効。

メリット

コピーを発生させずに String を利用可能(パフォーマンスが良い)。

デメリット

ライフタイム管理が必要 で、関数のスコープを超えた管理が難しくなる。

まとめ

  • 所有権を返す (String)・・・一番シンプルで安全。が、String のムーブが発生
  • &'static str を返す・・・ライフタイムの問題を回避・コストゼロ。が、動的な文字列を扱えない
  • 外部の String の参照を返す・・・コピーせずに安全に使用可能。が、ライフタイムの管理が必要

最も一般的な修正方法は String を返す方法 です。 一方、 既に String を持っている場合 は &String の参照を返す方法 が適しています。

14. Optionとライフタイム

fn main() {
    let name = String::from("Alice");
    let p: Option<&str>;

    {
        let temp_name = String::from("Bob");
        p = Some(&temp_name); // ❌ ここでエラー!
    } // `temp_name` がここで破棄される

    println!("{:?}", p);
}
答え

temp_name は {}(ブロック内)のローカル変数 なので、ブロックを抜けると メモリが解放 される。 しかし、p には temp_name の参照 &temp_name を格納しているため、ダングリング参照 になりコンパイルエラー。

  1. Option を使い、所有権を移動
fn main() {
    let name = String::from("Alice");
    let p: Option<String>; // ✅ 所有権を持たせる
    {
        let temp_name = String::from("Bob");
        p = Some(temp_name); // ✅ 所有権を移動
    }
    println!("{:?}", p);
}

Option にすることで、参照ではなく所有権を p に移動 させる。 スコープ外に出ても p にデータが残る ため、安全に使用できる。

メリット

ライフタイムの問題が発生しない。 Option の中身を取り出せば通常の String と同様に扱える。

デメリット

String のムーブ(所有権の移動)が発生するため、参照よりコストがかかる。

  1. Option<&'static str> を使う
fn main() {
    let name = String::from("Alice");
    let p: Option<&'static str>; // ✅ `&'static str` に変更

    p = Some("Bob"); // ✅ 文字列リテラルなら `'static` ライフタイム

    println!("{:?}", p);
}

"Bob" は コンパイル時に決まる文字列リテラル なので、'static ライフタイム を持つ。 これにより、ライフタイムの問題が発生しない。

メリット

参照のまま扱えるので、メモリのコストがかからない(コピーなし)。

デメリット

動的な String では使用できない(リテラル限定)。 既存の String の値を格納できない。

  1. Option<&'a str> を使い、外部の String を参照
fn main() {
    let name = String::from("Alice");
    let p: Option<&str>; // ✅ 参照のまま保持

    {
        let temp_name = String::from("Bob");
        p = Some(&name); // ✅ `name` の参照を格納
    } // `name` は `main()` のスコープなので有効

    println!("{:?}", p);
}

p には スコープ外でも有効な name の参照を格納 しているため、問題なく動作する。

メリット

参照のまま使えるので、ムーブやコピーのコストが発生しない。

デメリット

ブロック内で新たに作成した String は参照できない(ライフタイムが短いため)。

まとめ

  • Option(所有権を移動)・・・一番シンプルで安全。が、String のムーブが発生
  • Option<&'static str>(リテラルを使う)・・・&str のまま使える・コストが低い。が、動的な String を使えない
  • Option<&str>(外部の String を参照)・・・参照のまま使える・メモリ効率が良い。が、String をスコープ外で保持する必要がある

一番実用的なのは Option で所有権を持たせる方法 です。 もし、固定の文字列(リテラル) しか扱わないなら、 Option<&'static str> も有効です。

まとめ

  • 所有権のムーブ: s1 の所有権が s2 に移動し、s1 は無効になる。
  • 所有権のクローン: clone() を使えば s1 は有効のままになる。
  • 不変借用: &s を渡せば、所有権を移動せずに関数で使用可能。
  • 可変借用: &mut s を渡して s の内容を変更可能にする。
  • 二重可変借用の回避: スコープを使い r1 を先に解放する。
  • 参照のスコープ: スコープを明確に管理して s を安全に使う。
  • ダングリングポインタの回避: String を返して所有権を移動する。
  • ライフタイムの指定: 参照のライフタイムを明示して longest() を安全にする。
  • ライフタイムとスコープ: x のスコープが有効な間に r を使う。
  • 構造体とライフタイム: text のライフタイムを構造体に明示。
  • 可変参照の制約: 一度に複数の可変参照を持てないため、スコープを区切って解放する。
  • 借用後の変更: 不変参照がある間は可変参照できないため、スコープを明確にする。
  • 参照を返す関数: 関数内の変数を参照で返すとダングリングポインタになるため、所有権ごと返す。
  • Optionとライフタイム: 短いライフタイムの参照を Option に保存できないため、所有権を移動するか 'static を使う。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment