rustの所有権を理解する

所有権とは?

rustでは、「値はある変数に所有されている」らしいです。

逆にいうと、「ある変数は値に対して所有権を持つことができる」ということですね。

…所有権って何?

まず言葉の意味を確認してみます。

所有権(wikipediaから)

所有権とは、物の全面的支配すなわち自由に使用・収益・処分する権利

つまり、「ある変数は値を自由に使用し、処分することができる」ということ。

思ったより、とっつきやすそうですね。

プログラムに対して、使用、処分を捉え直してみると、

  • 使用:値を自由に変更できる。
  • 処分:値を破棄できる。

ということになりそうです。

値の変更

所有者による値の使用をrustのコードで書いてみます。

let mut a = 10;
a += 10;

こんな感じで値を変更できます。

値の破棄

次に所有者による値の破棄をrustのコードで書いてみます。

{
    let mut a = 10;
	a += 10;
    println!("{}",a); //ok!
}
println!("{}",a); // not allowed!

{}の中はスコープと呼ばれているらしいです。スコープ内で定義された変数はスコープから出ると破棄されます。つまり、aが存在するのはスコープ内のみです。

値はどうなったの?

所有者であるaがスコープから抜けた際、aが所有していた値も破棄されるらしいです。なので、「所有者がスコープから抜ける = 値が破棄される」ようです。

let b;
{
    let a = 10;
    b = &a;
    println!("{}",b); //ok!
}
println!("{}",b); // not allowed!

bは参照と言って、aから値を借りることができるものです。aから値を借りていましたが、aがいなくなり、値も破棄されたため、bで参照できる値は無くなってしましました。このコードはコンパイルエラーになります。

所有者は1人だけ

ちなみに、上の定義から所有者は1人になりそうです。

例えば、ある値をAさんとBさんが所有しているとすると、Aさんが値を破棄した時、Bさんは値を自由に変更できなくなる(値が破棄されたので)。つまり、Bさんは所有権を使えていないので、所有者はAさん1人だけです。

所有者がいなくなっても値を使用したい

所有権を持つ変数は値を自由に使用、破棄できることが分かりました。「所有者がスコープから出たら値を破棄する」というルールは、値の保存場所を空けることに繋がり効率が良さそうです。しかし少し不便でもあります。所有者がいなくなっても、値を使いたい時もあります。

Rustはこのような場合、2つの方法があるようです。1つは値をコピーして別の変数に割り当てる方法。もう1つは所有権を別の変数に譲渡する方法です。

値をコピーする

所有者がいなくなっても値を使いたい場合、値をコピーし、その値に別の所有者を割り当てます。

let b;
{
    let a = 10;
    b = a;
    println!("{}",a);
    println!("{}",b);
}
println!("{}",a); // not allowed!
println!("{}",b); // ok

先ほど書いたコードと似ています。違いはbが参照ではなくなったことです。このコードでは、スコープ内で定義したaが持つ値をコピーし、bに所有権を与えています。{}のスコープ内ではa、bどちらの値も使用することができます。スコープを出ると所有者aはいなくなりますが、bは引き続き使用できます。

ちなみに、以下のようにすると、変数が保存されている場所(アドレス)も見ることができます。

let b;
{
    let a = 10;
    b = a;
    println!("{:p}",&a);
    println!("{:p}",&b);
}

実行結果

0x16db9ec0c
0x16db9ec08

aとbは違う場所に保存されていることが分かりました。

所有権を譲渡する

所有者がいなくなっても、値を使用したい場合、所有権を別の変数に渡してしまうことができます。

let b;
{
    let a = vec![10];
    b = a;
    println!("{:?}",a); // not allowed!
    println!("{:?}",b); // ok
}
println!("{:?}",a); // not allowed!
println!("{:?}",b); // ok

{}スコープ内でaが定義されています(aがVecになった理由については後述)。代入式ではaの所有権をbに譲渡します。譲渡した後はaは所有者ではないため、値を使用できなくなりました。代わりに、bは値を使用できます。{}スコープから出ても、bは値を使用できるままです。これはbが{}スコープ外で定義されているので、b自体が破棄されておらず、所有している値も破棄されていないからです。{}スコープから出るときaは所有権を持っておらず、値を破棄できません。

以下のようにして、値が保存されているアドレスを見てみます。

let b;
{
    let a = vec![10];
    println!("{:p}",&a);
    b = a;
    println!("{:p}",&b);
}
println!("{:p}",&b);

実行結果

0x16f9e2be0
0x16f9e2bc0
0x16f9e2bc0

全部同じアドレスになっていました!

これは所有権のみが移動したことを表しています。

コピーと譲渡の使い分けはどうするの?

2つ方法があるのは分かりましたが、どのように使い分けるのでしょうか。実はrustではどちらの方法を使うのかは、型によって決まっています。ざっくり言うと、「確保する値のサイズが小さいとコピー、それ以外は譲渡」となっています。これはコピーと譲渡それぞれの動きを考えてみると、分かりやすいと思います。

確保する値のサイズ小さいときはコピーが向いている

今までの例を見ると、いつでもコピーしてくれた方がプログラムは書きやすそうですね。しかし、いつでもコピーするには2つの問題があります。1つは実行速度、もう1つはメモリの無駄遣いです。これらの問題が無視できるのは、確保している値のサイズが小さい時のみです。

実行速度について

値をコピーするわけなので、値のサイズ分を他の場所にコピーする手間がかかります。値のサイズが大きいと時間がかかってしまいます。

メモリの無駄遣いについて

いつでもコピーしてしまうと、不要なメモリを確保してしまいます。保存する値のサイズが大きいほど、メモリがたくさん必要になります。

「確保する値のサイズが小さい」の基準は?

primitive typeと呼ばれているものです。例えばusizeとかcharなど。詳しくはThe Rust Programming Languageに記載があります。

Vecや自分で作った型などは譲渡

コピーする手間や無駄なメモリ確保をしないため、比較的大きいサイズの型は「譲渡」を使います。譲渡が適用される型は、primitive type以外だと思っておけば良さそうです。「値をコピーする」と「所有権を譲渡する」で書いたrustのコードで変数aが所有する値の型が違うのは、型によって挙動が異なるからです。

意外と色々なところで起こる譲渡

let a = b;みたいな代入だけで譲渡が行われているわけではありません。譲渡は思ったより色々なところで起こっているようです。例えば、構造体のインスタンスを作成するとき、所有権を持ったVecを渡すと、構造体インスタンスに値が譲渡されます。

struct Hoge{
    v: Vec<i32>
}

let a = vec![10];
let hoge = Hoge{v:a};
println!("{:?}",a); // not allowed!

その他にも、構造体のメソッドでも所有権の譲渡が起きます。

struct Hoge{
    v: Vec<i32>
}

impl Hoge {
    fn take(self) {
        println!("{:?}",self.v);
    }
}

let a = vec![10];
let hoge = Hoge{v:a};
println!("{:?}",hoge.v); // ok
hoge.take();
println!("{:?}",hoge.v); // not allowed!

この例では、hoge.take()を実行するとき、構造体インスタンスの所有権をtake関数内に譲渡します。take関数終了時、所有権はselfにあるので、値は破棄されます。すると、hoge.take()終了後、インスタンスの変数は使用できなくなっています。

このように、意外と色々なところで所有権の譲渡が行われています。rustでは所有権のルールを破った時コンパイルエラーが起きて、どこがおかしいのかヒントをくれます。コンパイルエラーが起きた時には、「所有権がどう移動しているのか」を追えるようになりたいですね。

譲渡ではなくコピーをしたい

デフォルトで譲渡されるVec等の値でも、コピーしたいときもあります。以下のようにするとコピーできます。

let b;
{
    let a = vec![10];
    b = a.clone();
    println!("{:?}",a); // ok!
    println!("{:?}",b); // ok
}
println!("{:?}",a); // not allowed!
println!("{:?}",b); // ok

{}スコープ内で、aの値をコピーして、bに所有権を与えています。すると、{}内ではaもbもどちらも使用可能です。{}スコープから出ると、所有者aはいなくなり、aが所有していたaも破棄されます。bが所有している値は残っているので、{}外でも使用可能です。

所有権は要らないけど、ちょっと値を貸して欲しい

例えば以下の例(再掲)では、hoge.take()で所有権を譲渡し、take()のスコープから出るときに値が破棄されてしまいます。

struct Hoge{
    v: Vec<i32>
}

impl Hoge {
    fn take(self) {
        println!("{:?}",self.v);
    }
}

let a = vec![10];
let hoge = Hoge{v:a};
println!("{:?}",hoge.v); // ok
hoge.take();
println!("{:?}",hoge.v); // not allowed!

この場合「所有権は要らないけど、ちょっと値を貸して欲しい」が発生します。rustではこの要求を満たす機能が用意されています。この機能を「参照」といいます。

「参照」については、別の記事で書くことにします。

コメント

タイトルとURLをコピーしました