What Is Ownership?
以下の記事を翻訳し、自分なりに理解したことをまとめました。
https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
オーナーシップはガベージコレクタを必要とせずにメモリの安全性を確保する。
Ownership Rules
オーナーシップのルール
- Rustの各値にはオーナーという変数がある。
- オーナーは一人。
- オーナー範囲外になると値は削除される。
Variable Scope
スコープの話。
以下のような変数があった場合
let s = "hello";
以下の範囲で有効となる。
{ // sはここでは無効です。まだ宣言されていません
let s = "hello"; // sはこの時点から有効です
// Sで物事を行う
} // このスコープは終了し、sは無効になりました
The String Type
文字列は以下のように使用していたが、
https://qiita.com/takishita2nd/items/e45fed6660084bf5a84c
もう一つ、以下のように書ける。
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`
この違いは何か?
Memory and Allocation
文字列リテラルの場合はコンパイル時に内容が分かるため、最終的にハードコードされる。
そのため、文字列リテラルは高速である。
しかし、コンパイル時にサイズが不明で変更される可能性がある場合はそうすることが出来ない。
このような場合はヒープ領域に割り当てられる必要がある。
そのためには、
- メモリは実行時にメモリアロケーターから要求される必要がある。
- 文字列を使い終わったらアロケーターに返す方法が必要。
String::fromを実行すると、必要なメモリを要求する。
ガベージコレクタがあると、クリーンアップを考える必要が無いが、
ガベージコレクタが無いと、解放する必要がある。タイミングを誤ったり、忘れていたりするとバグになる。
Rustではスコープを使って解決する。
{
let s = String::from("hello"); // sはこの時点から有効です
// Sで物事を行う
} // このスコープは終了し、sは無効になりました
sがスコープ外になったらRust特別な関数を実行する。
この処理はドロップと呼ばれる。
Rustは}で自動的にドロップする。
Ways Variables and Data Interact: Move
以下の様な場合
let x = 5;
let y = x;
上の処理は、値5をxにバインドします。次に、xの値のコピーを作成し、それをyにバインドします。
では、以下の場合は、
let s1 = String::from("hello");
let s2 = s1;
上と同じようだが、実際は違う。
文字列は下の画像の左側で構成されている。
これらのデータはスタックに格納される。
右側は内容を保持するヒープ上のメモリ。
s1をs2に割り当てると、文字列データがコピーされる。
スタック上のポインタ、長さ、容量がコピーされ、ポインターが参照するヒープ上のデータはコピーしない。
これは以下の様にはならない。
変数がスコープから外れると、自動的にドロップ関数を呼び出し、その変数のヒープメモリを解放すると述べた。
ただし、図4-2は同じ場所を示すデータポインタを持っている。
s2とs1がスコープ外になると、両方とも同じメモリを解放しようとする。
これは二重解放となり、バグとなる。
この安全性を確保するため、s1をs2に割り当てた場合、s1は有効でないと判断する。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
Rustではこれは浅いコピーではなく、移動と呼んでいる。
Ways Variables and Data Interact: Clone
深いコピーをする場合はcloneを使用する。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
こうすることで、図4-3と同じ状態になる。
Stack-Only Data: Copy
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
このコードは有効だが、上の説明と矛盾しているように見える。
これは、コンパイル時にサイズが決まっているため、実際の値を素早くコピーできる。
変数yを作成する後で、xを無効にする理由がない。
なので、cloneは省略できる。
以下がコピーとなる。
- u32などのすべての整数型。
- ブール型のブール値で、値はtrueとfalseです。
- f64などのすべての浮動小数点型。
- 文字タイプ、char。
- タプル(コピーでもあるタイプのみが含まれている場合)たとえば、(i32、i32)はコピーですが、(i32、String)はそうではありません。
Ownership and Functions
関数に変数を渡す場合も同じような事が起こる。
fn main() {
let s = String::from("hello"); // sがスコープに入ります
takes_ownership(s); // sの値が関数に移動します...
// ...そして、ここではもう有効ではありません
let x = 5; // xがスコープに入ります
makes_copy(x); // xは関数に移動し、
// i32はコピーなので、後で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を使用するとエラーとなる。
Return Values and Scope
戻り値はオーナーシップを譲渡することが出来る。
fn main() {
let s1 = gives_ownership(); // give_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 { // 所有権を与えると、その戻り値がそれを呼び出す関数に移動します
let some_string = String::from("hello"); // some_stringがスコープに入ります
some_string // some_stringが返され、呼び出し元の関数に移動します
}
// take_and_gives_backは文字列を取り、それを返す
fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入ります
a_string // a_stringが返され、呼び出し元の関数に移動します
}
オーナーシップの他に返す値がある場合はタプルが使える。
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
しかし、関数を呼ぶ度に譲渡が発生するのは面倒。
なので、Rustには参照という機能がある。