Rust の文法: 所有権 (ownership) と借用 (borrow)

所有権 (ownership)

Rust においてヒープ上で管理される値には 所有権 (ownership) という概念があり、必ず 1 つの変数だけが所有権を保持しています。 所有権を持つ変数が関数などのスコープから外れるときに、メモリが解放され、値が破棄されます。 この仕組み(制約)により、Rust では、ヒープ上に確保された値がどこで解放されるかをコンパイル時に決定できるようになっています(Java のようなガーベジコレクションが必要ありません)。

Rust で独特なのは、所有権が変数間で移動するところで、次のような操作を行ったときに所有権が移動します(この振る舞いを move と呼びます)。

  • 代入演算子 (=) で別の変数に代入したとき
  • 関数の引数として変数を渡したとき
  • 関数の戻り値として変数を返したとき
代入による所有権の移動
// ヒープ上に String インスタンスが確保され、変数 s1 が所有者 (owner) となる
let s1 = String::from("Hello");

// s1 の所有権が s2 に移動 (move) し、s1 は無効化される
let s2 = s1;

// コンパイルエラー!(ここでは s1 はもう使えない)
println!("{}, {}", s1, s2);

次のように clone メソッドで明示的なディープコピーを行えば、所有権は移動しません。 ヒープ上の新しい領域に値がコピーされ、それぞれの変数が別の値の所有者となるからです。

let s1 = String::from("Hello");
let s2 = s1.clone();  // ディープコピー

// s1 も s2 も独立した値の所有者なので両方アクセスできる
println!("{}, {}", s1, s2);  //=> Hello, Hello

i32 型などのプリミティブ型に関しては、(ヒープではなく)スタック上 に値が配置されるので、所有権の概念を持ちません。 代入後もすべての変数が有効です。

スタック上に確保される値に所有権はない
let a = 100;
let b = a;

// a も b も有効
println!("{}, {}", a, b);  //=> 100, 100

プリミティブ型であっても、次のように Box 型でラップすることでヒープ上で管理することができます。

let x = Box::new(7);  // x が Box<i32> を所有する
let y = x;  // 所有権が y に移動し、x は無効化される

借用 (borrow)

所有権の移動 (move) は、関数の引数として変数値を渡すときにも発生します。 下記のコードの calc_len 関数は、引数で渡した変数の所有権を奪ってしまいます。 関数を抜けるときに、その変数は破棄されることになるので、呼び出し元の変数は無効になります。

関数呼び出しにより所有権が奪われる(間違った例)
fn calc_len(s: String) -> usize {
    s.len()
}

let s = String::from("Hello");
let len = calc_len(s);  // ここで s の所有権が奪われる
println!("{}, {}", s, len);  // エラー!(s はもう有効ではない)

これでは変数の値をたかだか 1 回しか参照できなくなってしまうので、Rust は 借用 (borrow) という仕組みを用意することで、所有権を移動させずにその値を参照 できるようにしています。 ある変数に & を付けると、その変数の参照を取得(借用)することができます。 参照の型を表現するときも & プレフィックスを付けて表現します(例: String の参照型は &String)。

let s1: String = String::from("Hello");
let s2: &String = &s1;  // 借用 (borrow)

// s1 も s2 も有効
println!("{} {}", s1, s2);

この仕組みを利用すれば、前述の calc_len を改良して、呼び出し時に所有権を奪わないようにすることができます。 具体的には、次のようにパラメーターの型を参照型にし、呼び出し時に借用した参照を渡すようにします(どちらも & を付けます)。

所有権を奪わない関数の例
// パラメーターの型を参照にした(実際は `&str` の方がよい)
fn calc_len(s: &String) -> usize {
    s.len()
}

let s = String::from("Hello");
let len = calc_len(&s);  // 借用により s の所有権は奪われない
println!("{}, {}", s, len);  //=> Hello, 5

関数内で作成したインスタンスを戻り値として返す場合は、参照ではなく通常の型で返すのが一般的です。 関数の終わりで所有権が移動するのは正しい振る舞いだからです。

戻り値の型は参照型にしない
fn create_message() -> String {
    let s = String::from("Hello");
    s  // 呼び出し側に所有権が移動する(想定通り)
}

可変借用 (mutable borrow)

参照経由で値を変更するには、&mut を付けて 可変参照 を作成する必要があります。 ある変数の可変参照を取得することを 可変借用 (mutable borrow) と呼びます。 これと区別するために、通常の借用 (borrow) によって取得した参照のことを、不変参照とよぶことがあります。

// 可変参照で String を受け取って内容を変更する
fn add_suffix(s: &mut String) {
    s.push_str("BBB")
}

let mut s = String::from("AAA");
add_suffix(&mut s); // 可変借用 (mutable borrow) して作った可変参照を渡す
println!("{}", s);  //=> "AAABBB"

通常の参照 (&) とは異なり、ある変数の 可変参照 (&mut) は 1 度に 1 つまでしか作れない という制約があります。 これは、複数箇所からの同時変更による競合を防ぐための Rust の仕様です。 また、通常の借用で取得した不変参照を保持しているときも、可変参照を作ることはできません。 不変だと思って参照している値が、別の場所から変更されては困るからです。 逆に、可変参照が存在しない状況では、不変参照は同時にいくつでも作成できます。 このあたりの振る舞いは、一般的に Read-Write Lock と呼ばれているデザインパターンそのものです(書き込む人がいなければ、いろんな場所から同時に読み込んでも問題ない)。