参照と借用
Listing 4-5 のタプルコードの問題は、String が calculate_length にムーブされるため、calculate_length の呼び出し後も String を使い続けられるように、呼び出し元の関数に String を返さなければならないことです。代わりに、String の値への参照を渡すことができます。参照はポインタに似ており、そのアドレスに格納されているデータへアクセスするためにたどれるアドレスです。そのデータは他の変数に所有されています。ポインタとは異なり、参照は、その参照の存続期間中、特定の型の有効な値を必ず指すことが保証されています。
値の所有権を受け取る代わりに、オブジェクトへの参照を引数として受け取る calculate_length 関数は、次のように定義して使います。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
まず、変数宣言と関数の戻り値にあったタプルのコードがすべてなくなっていることに注目してください。次に、calculate_length に &s1 を渡し、その定義では String ではなく &String を受け取っていることにも注目してください。これらのアンパサンドは参照を表しており、所有権を受け取らずに何らかの値を参照できるようにします。図4-6はこの概念を示しています。
図4-6: String s1 を指す &String s の図
注:
&を使った参照の反対は 参照外し で、参照外し演算子*で行います。参照外し演算子のいくつかの使い方は第8章で見て、参照外しの詳細は第15章で議論します。
ここでの関数呼び出しを、もう少し詳しく見てみましょう。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
&s1 という構文により、s1 の値を 参照する けれども所有はしない参照を作れます。参照は所有権を持たないので、それが指している値は、参照が使われなくなってもドロップされません。
同様に、関数シグネチャでは & を使って、引数 s の型が参照であることを示しています。説明の注釈をいくつか加えてみましょう。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
// it refers to, the String is not dropped.
変数 s が有効なスコープは、他のどの関数引数のスコープとも同じです。しかし、s は所有権を持っていないため、参照が指している値は s が使われなくなってもドロップされません。関数が実際の値ではなく参照を引数として受け取る場合、所有権を返すために値を返す必要はありません。そもそも所有権を受け取っていないからです。
参照を作るこの動作を 借用 と呼びます。現実世界と同じように、誰かが何かを所有しているなら、それを借りることができます。使い終わったら返さなければなりません。あなたのものではないのです。
では、借用しているものを変更しようとするとどうなるでしょうか。Listing 4-6 のコードを試してみてください。ネタバレすると、うまくいきません!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
変数がデフォルトで不変であるのと同じように、参照もデフォルトで不変です。参照しているものを変更することは許されません。
可変参照
Listing 4-6 のコードは、代わりに 可変参照 を使うようにいくつか小さな修正を加えるだけで、借用した値を変更できるように直せます。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
まず、s を mut に変更します。次に、change 関数を呼び出す箇所で &mut s を使って可変参照を作り、関数シグネチャも some_string: &mut String として可変参照を受け取るように更新します。これにより、change 関数が借用した値を変更することが非常に明確になります。
可変参照には大きな制約が1つあります。ある値への可変参照がある場合、その値への他の参照を持つことはできません。s への可変参照を2つ作ろうとする次のコードは失敗します。
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
このエラーは、このコードが無効であることを示しています。なぜなら、一度に2回以上 s を可変として借用することはできないからです。最初の可変借用は r1 にあり、それは println! で使われるまで継続しなければなりません。しかし、その可変参照を作ってから実際に使うまでの間に、r1 と同じデータを借用する別の可変参照を r2 に作ろうとしました。
同じデータに対する複数の可変参照を同時に禁止するこの制約により、変更は可能でありながらも、非常に制御された形でしか行えません。ほとんどの言語では好きなときに変更できるため、これは Rust を始めたばかりの Rustacean が苦労する点です。この制約の利点は、Rust がデータ競合をコンパイル時に防げることです。データ競合 は競合状態に似たもので、次の3つの振る舞いが起きると発生します。
- 2つ以上のポインタが同時に同じデータへアクセスしている。
- そのポインタの少なくとも1つが、データへの書き込みに使われている。
- データへのアクセスを同期する仕組みが使われていない。
データ競合は未定義動作を引き起こし、実行時に追跡して診断・修正するのは難しいことがあります。Rust はデータ競合のあるコードのコンパイルを拒否することで、この問題を防ぎます!
いつものように、中かっこを使って新しいスコープを作れば、複数の可変参照を使えます。ただし、同時に ではありません。
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
}
Rust は、可変参照と不変参照を組み合わせる場合にも同様のルールを強制します。このコードはエラーになります。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {r3}");
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, and {r3}");
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
ふう! 同じ値への不変参照がある間は、やはり 可変参照を持つこともできません。 不変参照を使っている人は、その値がいつの間にか突然変わってしまうとは思っていません! しかし、複数の不変参照は許されています。データを読み取っているだけの人は、ほかの人のデータの読み取りに影響を与えることができないからです。
参照のスコープは、その参照が導入された場所から始まり、その参照が最後に使用されるところまで続くことに注意してください。たとえば、次のコードはコンパイルできます。というのも、不変参照が最後に使われるのは println! の中であり、その後に可変参照が導入されるからです。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.
let r3 = &mut s; // no problem
println!("{r3}");
}
不変参照 r1 と r2 のスコープは、それらが最後に使われる println! の後で終わり、これは可変参照 r3 が作成される前です。これらのスコープは重なっていないため、このコードは許可されます。コンパイラは、スコープの終わりより前の時点で、その参照がもはや使われていないことを判断できます。
借用エラーはときどきイライラさせられるかもしれませんが、それは Rust コンパイラが潜在的なバグを早い段階で(実行時ではなくコンパイル時に)指摘し、問題がどこにあるのかを正確に示してくれているのだと覚えておいてください。そうすれば、なぜデータが自分の思っていたものと違っていたのか、その原因を追跡する必要はありません。
ダングリング参照
ポインタを持つ言語では、そのメモリへのポインタを保持したままメモリを解放することで、誤って ダングリングポインタ を簡単に作ってしまいます。これは、すでにほかの誰かに渡されているかもしれないメモリ上の位置を参照するポインタです。これに対して Rust では、コンパイラが参照が決してダングリング参照にならないことを保証します。あるデータへの参照があるなら、コンパイラは、そのデータへの参照より先にそのデータがスコープを抜けないようにします。
コンパイル時エラーによって Rust がどのようにダングリング参照を防ぐのかを見るために、実際にダングリング参照を作ってみましょう。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
このエラーメッセージは、まだ扱っていない機能であるライフタイムに言及しています。ライフタイムについては第 10 章で詳しく説明します。しかし、ライフタイムに関する部分をひとまず脇に置いても、このメッセージには、なぜこのコードが問題なのかを示す鍵が含まれています。
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
dangle のコードの各段階で何が起きているのかを、もう少し詳しく見てみましょう。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
s は dangle の内部で作成されるため、dangle のコードが終わると、s は解放されます。しかし、私たちはそれへの参照を返そうとしました。つまり、この参照は無効な String を指すことになります。これはだめです! Rust はこれを許しません。
ここでの解決策は、String を直接返すことです。
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
これは何の問題もなく動作します。所有権がムーブされ、何も解放されません。
参照のルール
ここで、参照についてこれまでに説明したことを振り返りましょう。
- 任意の時点で、持てるのは 1 つの可変参照 か 任意個の不変参照 のどちらか一方です。
- 参照は常に有効でなければなりません。
次に、別の種類の参照であるスライスを見ていきます。