Rustの所有権ルールが引き起こすコンパイルエラー対処法

先生

Rustの所有権エラー?もう怖くない!コンパイラと友達になるための完全ガイド。

Rustの所有権とは?コンパイラエラーの根本原因を理解する

Rustは、メモリ安全性を重視したプログラミング言語です。その核心となるのが「所有権」という概念です。所有権システムは、ガベージコレクションなしにメモリリークやデータ競合を防ぐための強力な仕組みですが、最初はコンパイルエラーの嵐に遭遇することも少なくありません。

この記事では、Rustの所有権ルールが原因で発生する代表的なコンパイルエラーとその対処法を、具体的なコード例を交えながら解説します。所有権を理解し、コンパイラと対話できるようになれば、Rustプログラミングはより楽しく、そして安全になります。

所有権システムを理解することは、Rustプログラミングの第一歩です。所有権は、すべての値が必ず一つの変数によって「所有」されるという原則に基づいています。この所有者は、値が不要になった時点でメモリを解放する責任を負います。

所有権には、以下の3つの主要なルールがあります。

1. 各値は、必ず一つの所有者を持つ。

2. 所有者がスコープから外れると、値は破棄される。

3. 所有権は、ムーブ(move)または借用(borrow)によって移転または共有される。

これらのルールを理解することで、コンパイラがなぜ特定のエラーを出すのかが理解できるようになります。

よくある所有権エラーと解決策

所有権ルールに違反すると、コンパイル時にエラーが発生します。ここでは、よくあるエラーとその解決策を見ていきましょう。

ムーブは、所有権がある変数から別の変数への所有権の移転です。ムーブ後、元の変数は無効になり、使用できなくなります。


let s1 = String::from("hello");
let s2 = s1; // s1からs2へ所有権がムーブ

println!("{}", s1); // エラー! s1はもう有効ではない

このコードはコンパイルエラーになります。なぜなら、s1の所有権がs2に移転された後、s1を使用しようとしているからです。

解決策としては、clone()メソッドを使って値を複製する方法があります。


let s1 = String::from("hello");
let s2 = s1.clone(); // s1の値を複製してs2に割り当てる

println!("s1 = {}, s2 = {}", s1, s2); // OK

借用は、所有権を移転せずに値への参照を渡すことです。借用には、可変参照(mutable reference)と不変参照(immutable reference)の2種類があります。

可変参照は、値を変更できる参照です。ただし、ある時点で一つの可変参照しか存在できません。

不変参照は、値を変更できない参照です。複数の不変参照を同時に持つことができます。


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

let r1 = &s; // 不変参照
let r2 = &s; // 不変参照

println!("{} and {}", r1, r2); // OK

let r3 = &mut s; // 可変参照
println!("{}", r3); //OK

// let r4 = &s; // エラー!可変参照がある場合、不変参照は作成できない

上記のコードでは、可変参照と不変参照を同時に持つことができないため、コンパイルエラーが発生します。

解決策としては、可変参照が使用されるスコープを制限するか、可変参照を使用しないようにコードを修正します。

所有権と関数

関数に値を渡すとき、所有権はムーブまたは借用されます。

ムーブの場合、関数が値を所有し、関数が終了すると値は破棄されます。

借用の場合、関数は値への参照を受け取り、関数が終了しても値は破棄されません。


fn takes_ownership(some_string: String) { // some_stringがスコープに入る
    println!("{}", some_string);
} // ここでsome_stringはスコープを抜けdropが呼ばれる。メモリが解放される

fn makes_copy(some_integer: i32) { // some_integerがスコープに入る
    println!("{}", some_integer);
} // ここでsome_integerはスコープを抜ける。何も特別なことは起こらない

fn main() {
    let s = String::from("hello"); // sがスコープに入る
    takes_ownership(s); // sの値が関数にムーブされ...
    // println!("{}", s); // ここでsを使おうとするとコンパイルエラー!

    let x = 5; // xがスコープに入る
    makes_copy(x); // xの値は関数にムーブされるが、i32はCopyなので、その後のxの利用はOK
    println!("{}", x); // OK!
}

上記のコードでは、takes_ownership関数にsを渡すと、sの所有権が関数に移転されます。そのため、関数呼び出し後にsを使用しようとするとコンパイルエラーが発生します。

makes_copy関数にxを渡す場合、i32型はCopyトレイトを実装しているので、値がコピーされ、所有権は移転されません。そのため、関数呼び出し後もxを使用できます。

ライフタイム(Lifetime)

ライフタイムは、参照が有効な期間を表します。Rustコンパイラは、ライフタイムを使って、ダングリングポインタ(無効なメモリを参照するポインタ)を防止します。

ライフタイム注釈は、コンパイラがライフタイムを推論できない場合に必要になります。


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

上記のコードでは、<'a>はライフタイム注釈を表します。xyのライフタイムが同じであることを示しています。また、返り値のライフタイムもxyと同じであることを示しています。

ライフタイム注釈は、関数のシグネチャにのみ現れ、関数の本体には現れません。ライフタイム注釈は、コンパイラに対するヒントであり、プログラムの動作には影響を与えません。

参考リンク

まとめ

Rustの所有権システムは、最初は難解に感じるかもしれませんが、メモリ安全性を保証するための強力なツールです。所有権、借用、ライフタイムの概念を理解し、コンパイラからのエラーメッセージを丁寧に読むことで、より安全で効率的なRustコードを書けるようになります。この記事が、Rustの所有権エラーに立ち向かうための一助となれば幸いです。