所有権とは?
rustでは、「値はある変数に所有されている」らしいです。
逆にいうと、「ある変数は値に対して所有権を持つことができる」ということですね。
…所有権って何?
まず言葉の意味を確認してみます。
所有権とは、物の全面的支配すなわち自由に使用・収益・処分する権利
つまり、「ある変数は値を自由に使用し、処分することができる」ということ。
思ったより、とっつきやすそうですね。
プログラムに対して、使用、処分を捉え直してみると、
- 使用:値を自由に変更できる。
- 処分:値を破棄できる。
ということになりそうです。
値の変更
所有者による値の使用を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ではこの要求を満たす機能が用意されています。この機能を「参照」といいます。
「参照」については、別の記事で書くことにします。
コメント