所有権とは
Rust には、メモリを管理するための所有権システムが含まれています。 コンパイル時、所有権システムでは一連のルールがチェックされ、所有権機能により、速度を低下することなくプログラムを実行できることが保証されます。
所有権について理解するために、まず、Rust の "スコープ ルール" と "移動セマンティクス" を見てみましょう。
スコープ ルール
Rust の場合、他のほとんどのプログラミング言語と同様に、変数は特定の "スコープ" 内でのみ有効です。 Rust では、スコープは多くの場合、中かっこ {}
を使用して示されます。 一般的なスコープには、関数本体と、if
、else
、match
の分岐が含まれます。
Note
Rust では、"変数" は "バインド" と呼ばれることがよくあります。 これは、Rust の "変数" がそれほど可変ではないことが理由です。これらは、既定では変更できないため、多くの場合、変更されません。 代わりに、多くの場合、名前をデータに "結び付ける" と考えます。"バインド" という言い方はここから来ています。 このモジュールでは、"変数" と "バインド" という用語を同じ意味で使用します。
たとえば、スコープ内に定義されている文字列である mascot
変数があるとします。
// `mascot` is not valid and cannot be used here, because it's not yet declared.
{
let mascot = String::from("ferris"); // `mascot` is valid from this point forward.
// do stuff with `mascot`.
}
// this scope is now over, so `mascot` is no longer valid and cannot be used.
スコープの外で mascot
を使用しようとすると、この例のようなエラーが表示されます。
{
let mascot = String::from("ferris");
}
println!("{}", mascot);
error[E0425]: cannot find value `mascot` in this scope
--> src/main.rs:5:20
|
5 | println!("{}", mascot);
| ^^^^^^ not found in this scope
この例は、Rust Playground でオンラインで実行できます。
変数は、宣言されているポイントからそのスコープの末尾まで有効です。
所有権と削除
Rust では、スコープの概念にひねりを加えています。 オブジェクトは、スコープ外になると "削除" されます。変数を削除すると、関連付けられているすべてのリソースが解放されます。 ファイルの変数の場合、ファイルは閉じられることになります。 関連付けられたメモリが割り当てられている変数の場合、そのメモリは解放されます。
Rust において、バインドが削除されたときに解放される "関連付けられた" ものを含むバインドは、それらのものを "所有する" と言います。
上の例では、mascot
変数は、関連付けられた String データを所有しています。 String
自体で、その文字列の文字を保持するヒープ割り当てメモリが所有されます。 スコープの終わりで mascot
は "削除" され、所有している String
が削除されます。最後に、String
が所有するメモリが解放されます。
{
let mascot = String::from("ferris");
}
// mascot is dropped here. The string data memory will be freed here.
移動セマンティクス
場合によっては、変数に関連付けられているものがスコープの最後に削除されないようにしたいことがあります。 代わりに、バインド間で項目の所有権を移転することができます。
最も簡単な例は、新しいバインドを宣言する場合です。
{
let mascot = String::from("ferris");
// transfer ownership of mascot to the variable ferris.
let ferris = mascot;
}
// ferris is dropped here. The string data memory will be freed here.
ここで重要なのは、所有権が移転されると、古い変数は有効でなくなることです。 上の例で、String
の所有権を mascot
から ferris
に移転した後、mascot
変数は使用できなくなります。
Rust では、"所有権の移転" は "移動" と呼ばれます。 つまり、String
値の所有権は mascot
から ferris
に移動されています。
String
を mascot
から ferris
に移動した後に mascot
を使用しようとすると、コードはコンパイラでコンパイルされません。
{
let mascot = String::from("ferris");
let ferris = mascot;
println!("{}", mascot) // We'll try to use mascot after we've moved ownership of the string data from mascot to ferris.
}
error[E0382]: borrow of moved value: `mascot`
--> src/main.rs:4:20
|
2 | let mascot = String::from("ferris");
| ------ move occurs because `mascot` has type `String`, which does not implement the `Copy` trait
3 | let ferris = mascot;
| ------ value moved here
4 | println!("{}", mascot);
| ^^^^^^ value borrowed here after move
この結果は、"移動後の使用" というコンパイル エラーとして知られています。
重要
Rust では、一度に 1 つのものだけがデータを "所有" できます。
関数における所有権
引数として関数に渡される文字列の例を見てみましょう。 何かを関数に引数として渡すと、それが関数に "移動" します。
fn process(input: String) {}
fn caller() {
let s = String::from("Hello, world!");
process(s); // Ownership of the string in `s` moved into `process`
process(s); // Error! ownership already moved.
}
コンパイラは、s
値が "移動" されたというエラーを返します。
error[E0382]: use of moved value: `s`
--> src/main.rs:6:13
|
4 | let s = String::from("Hello, world!");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
5 | process(s); // Transfers ownership of `s` to `process`
| - value moved here
6 | process(s); // Error! ownership already transferred.
| ^ value used here after move
前のスニペットでわかるように、process
の最初の呼び出しにより、変数 s
の所有権が移転されます。 コンパイラによって所有権が追跡されるので、process
の 2 回目の呼び出しによりエラーが発生します。 リソースが移動された後は、以前の所有者を使用できなくなります。
このパターンは、Rust コードの記述方法に大きく影響します。 これは、Rust によって提示されるメモリの安全性の保証の中核です。
他のプログラミング言語では、関数に渡される前に、s
変数の String
値を暗黙的にコピーできます。 しかし、Rust では、このアクションは発生しません。
Rust では、所有権の移転 (つまり、移動) が既定の動作です。
移動せずにコピーする
前の例の (むしろ情報を提供している) コンパイラのエラー メッセージで、Copy
特性が言及されていることに気付いたかもしれません。 特性についてはまだ説明していませんが、Copy
特性を実装する値は移動されず、コピーされます。
Copy
特性を実装する値 u32
を見てみましょう。 次のコードは破損したコードと同じですが、問題なくコンパイルされます。
fn process(input: u32) {}
fn caller() {
let n = 1u32;
process(n); // Ownership of the number in `n` copied into `process`
process(n); // `n` can be used again because it wasn't moved, it was copied.
}
数値のような単純型では、型を ''コピー'' します。 Copy
特性を実装します。つまり、移動されず、コピーされます。 ほとんどの単純型で同じアクションが発生します。 数値のコピーは低コストであるため、これらの値をコピーするのが理にかなっています。 文字列、ベクター、またはその他の複合型は、コピーするとコストが高くなる場合があるため、Copy
特性を実装せず、代わりに移動されます。
Copy
を実装しない型をコピーする
前の例のエラーを回避する方法の 1 つは、移動前に型を明示的にコピーすることです。これは Rust で複製と呼ばれます。 .clone
を呼び出すと、メモリが複製され、新しい値が生成されます。 新しい値は移動されるので、古い値を引き続き使用できます。
fn process(s: String) {}
fn main() {
let s = String::from("Hello, world!");
process(s.clone()); // Passing another value, cloned from `s`.
process(s); // s was never moved and so it can still be used.
}
この方法は便利ですが、clone
を呼び出すたびにデータの完全なコピーが作成されるため、コードの実行速度が低下するおそれがあります。 この方法には多くの場合、メモリ割り当てや他のコストが高い操作が含まれます。 "参照" を使用して値を "借用" する場合は、これらのコストを回避できます。 次のユニットでは、参照を使用する方法について学習します。