前回の記事
https://qiita.com/revioness/items/6534b03432db5b9347a8
4. 所有権を理解する
所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。この章では、所有権以外にも、関連する機能を いくつか話していきます: 借用、スライス、そして、コンパイラがデータをメモリにどう配置するかです。
Rustのコアの部分なのでかなり細かくやっていくつもりです。
4.1 所有権とは
- 他の言語には2種類のメモリの使用方法を管理する方法がある
ガベージコレクション:定期的に使用していないメモリを検索してメモリを開放する
明示的にメモリの確保や解放:C言語などで使われている手法。慣れていないとメモリの解放忘れなどで頻繁にメモリリークが起きる - Rustでは第三の方法として所有権システムというものがある
所有権規則
まず、所有権のルールについて見ていきましょう。 この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください
- Rustの各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
-
スコープについて
fn main() { { // sは、ここでは有効ではない。まだ宣言されていない let s = "hello"; // sは、ここから有効になる // sで作業をする } // このスコープは終わり。もうsは有効ではない }
sがスコープに入ると有効になる
スコープを抜けるまで有効なまま
この辺はほかの言語と同じ
例えばfunction foo(){ var s = 5; for(var i=0; i<3; i++){ var m = 3; } }
jsで書いているがこの場合
s
はfoo関数内で有効であり、m
はfor文の中でのみ有効になっている
この認識を元に次へ進む
スタックとヒープ
- 公式ドキュメントは難しいので簡単な概念を説明する
スタックとヒープ
- どちらも実行時に使用できるメモリ領域のこと
- スタック
- ラストインファーストアウトの意味
- 簡単にいうとメモリに変数などを保存する際にメモリ領域が決まっており、追加される際に後ろにどんどん入れていく
- 取り出す際には古いものから順に取り出していく
- 新しいデータを置いたり取り出したりする時に場所を探す必要がないので非常に高速
- しかし、可変の配列などを入れることができない(データを入れた瞬間にケツのメモリが決まっているのでメモリが後ろのデータとかぶってしまうため)
- ヒープ
- ヒープ領域にデータを格納する場合はある程度の空きを持ったメモリ領域を確保し、メモリのポインタ(場所)を返す
- ヒープ領域の場合はメモリのアドレスの場所を探さなければいけないのでスタックより低速
- もしそのある程度の空きを持ったメモリ領域のメモリがいっぱいになってしまったら再度割当て直すようになっている
ここではこの程度の認識でよい
String型
- 例としてString型の所有権にまつわる部分を扱う
let s = String::from("hello");
文字列リテラル(固定長)からString型(可変長)をこのように生成できる
let mut s = String::from("hello");
s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える
println!("{}", s); // これは`hello, world!`と出力する
メモリの確保
- 文字列リテラルの場合は固定長のため、コンパイル時にバイナリファイルに変換され"直接"ハードコーディングされる
- このため文字列リテラルは高速
- String型は可変な文字列を実装するためにコンパイル時には不明な量のメモリをヒープ領域に確保するようになっている
-
この時二つの手順が発生する
メモリは、実行時にOSに要求される。(ヒープ領域への割当て) String型を使用し終わったら、OSにこのメモリを返還する方法が必要である。(メモリの解放)
-
メモリは、実行時にOSに要求される。(ヒープ領域への割当て)
- これは宣言時に自動で行われている
let s = String::from("hello");
- これは宣言時に自動で行われている
-
String型を使用し終わったら、OSにこのメモリを返還する方法が必要である。(メモリの解放)
- これが所有権にまつわる話になっている
こんなコードがあった時に、どこでメモリを解放するのが自然だろうか
{ let mut s = String::from("hello"); s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える println!("{}", s); // これは`hello, world!`と出力する }
もちろんsがスコープを抜ける瞬間(以降使用されなくなる瞬間)
println!("{}", s); // これは`hello, world!`と出力する
ここの後だろう
Rustはこのタイミングでs
のdrop関数というものを呼び出す
これによってs
に割り当てられていたメモリは自動的に開放される
仮にjsやpythonなどの言語をやっていた場合はGCが動くまでメモリは確保されたままである
- これが所有権にまつわる話になっている
-
変数とデータの相互作用法: ムーブ
-
Rustは複数の変数が同じデータに対して異なる手段で相互作用することができる
let x = 5; let y = x; println!("x:{}", x); println!("y:{}", y);
x:5 y:5
この場合は当然このように表示される
では以下の場合はどうなるだろうかlet s1 = String::from("hello"); let s2 = s1; println!("s1:{}", s1); println!("s2:{}", s2);
error[E0382]: borrow of moved value: `s1` --> src\main.rs:5:23 | 2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 3 | let s2 = s1; | -- value moved here 4 | 5 | println!("s1:{}", s1); | ^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0382`. error: could not compile `own` due to previous error
なぜこうなってしまうのか
String型は以下のようにメモリ上に保存されている- ポインタ(ヒープ領域のポインタ)
- 長さ
- 容量
つまりここには実際に入っている値は保存されていない(文字列リテラルの場合は先ほど言ったように、固定長なので文字列がそのままハードコーディングされる)
この状態でs1
をs2
を代入すると
s1
- ポインタ
- 長さ
- 容量
s2
- ポインタ
- 長さ
- 容量
当然このように同じものが二つできる
つまり、s1
とs2
は同じヒープポインタを指すものが入る(値はコピーされていないので一つしかない)
もし仮に以下の状態で先ほど説明したdrop関数が呼び出されるとlet s1 = String::from("hello"); let s2 = s1; //ここでs1 drop println!("s2:{}", s2); //ここでs2 drop
これは2重解放エラーとなる(そもそもs1をprintする時点でエラーになるが)
これを避けるためにRustはs1
がs2
に代入された時点で有効でないと判断し何もしなくなる
そのためdropもしないし参照もできなくなる
これが所有権
の考え方
この場合はs1に入っていたString型のhelloの持ち主はs2
になる(これをムーブと呼ぶ)- もしどちらの変数も使いたい場合は?
- その場合はよくあるメソッドとして
clone
メソッドがある。例えば以下のようなコードはコンパイルが通るcloneメソッドが実行された時にlet s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2);
s1
のhello
が新たにヒープメモリに追加され、そこへのポインタがs2
に入るようになっている
これをdeep copy
と呼ぶ
- その場合はよくあるメソッドとして
スタックのみのデータ: コピー
- では以下のコードはどうだろうか
これはコンパイルが通るようになっている
let x = 5; let y = x; println!("x = {}, y = {}", x, y);
なぜかというと、これは5はイミュータブルであり、固定長であるため、スタック領域を使用しており、値のコピーも簡単だからである(わざわざx
を無効化する理由がない)
所有権と関数
- ここまでムーブとコピーを学んだが、関数に値を渡すことは、ムーブとコピーに似ている。以下のコードがあったとする
fn main() {
let s = String::from("hello"); // sがスコープに入る
takes_ownership(s); // sの値が関数にムーブされ...
// ... ここではもう有効ではない
let x = 5; // xがスコープに入る
makes_copy(x); // xも関数にムーブされるが、
// i32はCopyなので、この後にxを使っても
// 大丈夫
} // ここでxがスコープを抜け、sもスコープを抜ける。ただし、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がスコープを抜ける。何も特別なことはない。
基本的にはコメントにある通り
仮に
takes_ownership(s);
println!({}, s);
このようにするとコンパイルエラーになる
関数に値を渡すと、その値の所有権は関数に奪われ、sは無効化されてしまう
ではこの場合はどうだろう
makes_copy(x);
println!({}, x);
この場合はコンパイルが通る
xはi32
型になっているので、コピーされるため
Copyされる型は以下(一部)
- あらゆる整数型。u32など。
- 論理値型であるbool。trueとfalseという値がある。
- あらゆる浮動小数点型、f64など。
- 文字型であるchar。
- タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。
戻り値とスコープ
fn main() {
let s1 = gives_ownership(); // gives_ownershipは、戻り値をs1に
// ムーブする
let s2 = String::from("hello"); // s2がスコープに入る
let s3 = takes_and_gives_back(s2); // s2はtakes_and_gives_backにムーブされ
// 戻り値もs3にムーブされる
} // ここで、s3はスコープを抜け、ドロップされる。s2もスコープを抜けるが、ムーブされているので、
// 何も起きない。s1もスコープを抜け、ドロップされる。
fn gives_ownership() -> String { // gives_ownershipは、戻り値を
// 呼び出した関数にムーブする
let some_string = String::from("hello"); // some_stringがスコープに入る
some_string // some_stringが返され、呼び出し元関数に
// ムーブされる
}
// takes_and_gives_backは、Stringを一つ受け取り、返す。
fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。
a_string // a_stringが返され、呼び出し元関数にムーブされる
}
これもコメントの通り
戻り値を受け取ることで、関数内の変数にあった所有権を、関数外の値にムーブすることができる
実際これだとかなりめんどうくさい
- 変数を定義
- その変数を関数に渡す(この時点で所有権はなくなる)
- 戻り値を受け取る用の変数を用意してその変数に関数の戻り値を入れる
それを解消するのが次の章
4.2 参照と借用
- 関数に参照を渡すことで値は変更できないが、値を見ることができるようになる
これは参照を渡しているだけなので、参照先の値は変更することができない
//これが上でやったもの let s = String::from("hello"); takes_and_gives_back(s); fn takes_and_gives_back(a_string: String) -> String { a_string; } //参照を渡すパターン let s = String::from("hello"); takes_and_gives_back(&s); //前に&をつけることで参照を生成することができる fn takes_and_gives_back(a_string: &String) -> String { //受け取る側も参照を受け取っていると明記する必要がある a_string; }
所有権は渡されていないのでa_string
のスコープを抜けても、参照が指しているものはdropされることはない
このように参照を渡すことを借用
という
参照を変更したい場合は?
- 参照を渡すことで所有権を失わずに関数へ値を渡すことができるということがわかったが、仮にその参照先の値を変更したい場合はどうすればいいか
-
簡単にいうと、参照をミュータブルにしてしまえばいい
fn main() { let mut s = String::from("hello"); change(&mut s); println!("{}", s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
この場合の出力は以下
hello, world
可変参照には制約があり、同時に一つしか可変な参照は取れない
以下の場合はエラーになるlet mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
以下のパターンもエラーになる
let mut s = String::from("hello"); let r1 = &s; // 問題なし let r2 = &s; // 問題なし let r3 = &mut s; // 大問題!
r1
とr2
はあくまで参照を渡しているだけなのでいくつ渡しても値を変更されることがないので問題ないが、参照を渡している部分が一つでもある場合は可変参照は渡すことはできない
-
4.3 スライス型
- 勉強次第追記します