Rustには「値」に対して所有権(所有者)という機能があります。
これも、今まで私が経験したJavaScriptやjavaでは聞かない機能です。
所有権はRustの基本の重要機能なので、しっかり理解しないとRustは理解出来ないといってもいいでしょう。(いいかな?)
変数と値は所有者と所有物の関係
- 変数: 所有者
- 値: 所有物
値を変数に代入すると下図のように所有者と所有物の関係になります。
所有者 | 所有物 |
---|---|
s1 | value |
先に言っておくと、この所有権の機能はi32やchar等のスカラー型(プリミティブ型)は機能しません。正式に言うと「スカラー型はCopyトレイトを実装しているから」なのですがトレイトについて知らない人は、今はスカラー型には所有権という機能は無効で、JavaScriptやjavaと同じ扱いになる。とだけ認識して下さい。
Stringは文字列型と呼ばれる標準ライブラリに実装されている型でありスカラー型ではありません。
所有者 | 所有物 |
---|---|
s1 | value |
s2 | value |
この例では、所有者が「a1」「a2」の2つで、それぞれ"value"という値を所有します。
"value"は値としては同じだが物としては別々です。たとえるならs1さんとs2さんは同型のiPhoneを所有しているが実物は違う。1台のiPhoneを共有しているわけではない。でしょうか。。。
所有者と所有物は1対1の関係
所有者と所有物は1対1の関係です。
- 1つの所有者が複数の所有物を所有することは出来ない。
- 1つの所有物は複数の所有者の所有物になることは出来ない。
変数と値の関係で1対n、n対1の関係はおかしいので当たり前かー。
ここまでの説明では、所有権という機能があるからといってJavaScriptやjavaと得に変わった点はありませんでした。変数を宣言しただけなので。
ここからが本題です。
所有権移動
次の例です。
下記のコードは見た感じ問題なさそうですが、Rustではコンパイルエラーになります。
let s1 = String::from("value");
let s2 = s1;
println!("{}", s1);
printlnマクロでs1を使用している箇所でコンパイルエラーになります。
このエラーの理由が「所有権移動」です。
所有権移動を「ムーブ」といいます。
「所有権移動」とは私が個人的に表現しているだけです。
変数から変数に値を代入することは所有物を譲渡するイメージ
所有権移動と表現しましがた、違った表現に言い換えると「所有物を譲渡する」でしょうか。
let s2 = s1;
s2の宣言は、下図のように s1の所有物"value"の値を、s2にコピーする。 というイメージではありません。
下図のように s1の所有物"value"の値を、s2に譲渡する(所有権をs2に移動する)。 というイメージになります。
譲渡すると、s1の所有物はなくなり、なにも値を持っていない状態なので、 譲渡した後はs1を使うことは出来ない というこです。
「所有物の譲渡する」とは私個人的な解釈なのでご了承下さい。
ちなみにRustにはNullという値はありません。
「xxxxx after move」となっていますよね。ムーブした後は使えない。ということです。
関数の引数に渡したときも所有権移動となる
下記のコードも見た感じ問題なさそうですが、Rustはコンパイルエラーになります。
fn main() {
let s = String::from("value");
print_string(s);
// ここでコンパイルエラーになる。
println!("{}", s);
}
fn print_string(s: String) {
println!("{}", s);
}
理由は、main関数のsの値をprint_string関数の引数として渡したからです。
関数の引数に使用した場合も「所有権移動」となり、 main関数のsが所有していた"value"の値がprint_string関数のsに所有権が移動します。
別の言い方だと、main関数のs変数の"value"値がprint_string関数のs変数に譲渡されます。
スカラー型の値は所有権移動されない
前に
先に言っておくと、この所有権の機能はi32やchar等のスカラー型(プリミティブ型)は機能しません。正式に言うと「スカラー型はCopyトレイトを実装しているから」。
と書きましたが、スカラー型の値については、Copyトレイトを実装しているためコピーされます。
トレイトについては別の機会に説明したいと思います。
なので下記のコードはコンパイルエラーになりません。
let i1: i32 = 1;
// 所有権移動はされず、値がコピーされます。
let i2: i32 = i1;
println!("{}", i1);
println!("{}", i2);
下図のように値がコピーされ、i1、i2それぞれ1の値を所有します。
最後に
正直、なんで慣れない所有権なんて機能があるんだ??と思いましたが学習を進めると以下点で所有権の有効性を感じました。個人的な感想なのでご了承下さい。
エコの観点から
Rustに所有権機能がなく、必ず値がコピーされるとどうでしょうか。
たとえば2Kバイトにもなる文字列を下記のように変数代入した場合、
let s1 = Stringf::from("2Kにもなる文字列.....");
let s2 = s1;
let s3 = s2;
所有権移動ではなく、コピーだと 2K x 3 = 6K のヒープメモリを消費することになります。
しかし所有権移動の機能のお陰で2Kで消費が済むことになります。
また、
以前にRustの可変長データはヒープ領域に格納される、と書きました。
このヒープ領域はスタック領域に比べ領域確保(アロケート)に時間がかかります。なのでヒープ領域に対して領域確保を行う処理はパフォーマンス的にもよくありません。ですが所有権移動の機能のお陰で
let s2 = s1;
は、s2用にヒープ領域を確保するのではなく、スタック領域にs2を確保するだけで済むのです。
以上でした。