前: 5.7. Ownership 次: 5.9. Lifetimes
足りない英語力と日本語力はエスパーと訳注で補え。
誤訳や改善案等、見つけた場合はご指摘の程宜しくお願いします。
- 元記事 : https://doc.rust-lang.org/book/ownership.html のRust 1.6時点
- ライセンス : MIT license, Apache License 2.0.
- See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.
参照と借用
このガイドはRustの所有権システムに含まれる3要素の内の1つについて示したものです。これはRustの開発者が熟達すべきであり、かつRustの最もユニークで魅力的な機能の1つです。所有権はRustの最大の目的であるメモリ安全性を実現する方法です。以下の章でそれぞれ別個の概念について説明しています。
- キーコンセプトが[所有権][ownership]です
- 今あなたが読んでいるのが借用です
- 借用を進めた概念が[生存期][lifetimes]です
これら3つの章はその順序にも関係があります。所有権システムの完全な理解のためには3つ全ての読解が必要となるでしょう。
[ownership]: http://qiita.com/_Nnwww/items/0266080c4112b489ff9f
[lifetimes]: https://doc.rust-lang.org/book/lifetimes.html
メタ1
詳細について見て行く前に、所有権システムについて2つ重要な注意事項があります。
Rustは安全性と速度に焦点を当てています。それら目的は、Rustにおいて動作のための抽象化コストを可能な限り抑えることを意味する'ゼロコストの抽象化'を多く行うことで達成しています。所有権システムはゼロコストの抽象化の典型的な例です。私たちがこのガイドで話す詳細の全ては_コンパイル時に行われます_。あなたはこれらの機能のために実行時コストを払うことはありません。
しかしながら、このシステムは確実にコストを要し、その影響は学習曲線に現れます。多くのRustの新規ユーザは自身が有効だと考えているプログラムのコンパイルをコンパイラに拒否されるという、所謂'ボローチェッカーとの戦い'を経験しています。これがしばしば起きるのはプログラマが頭の中で考える所有権がこのように動作するはずだというモデルとRustに実装された実際のルールが一致しないためです。あなたも恐らく初めは同じようなことを経験するでしょう。しかしながら良い知らせもあります。経験豊富なRustの開発者曰く、一度でもある程度の期間所有権システムのルールの下で作業すれば、借用チェッカーとの戦いは次第に減っていくとのことです。
このことを念頭において、所有権について学んでいきましょう。
借用
[所有権][ownership]の章の最後に, 私たちは以下の様な酷い関数を見てきました。
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// v1とv2で何かする
// 返して欲しい所有権と関数の結果
(v1, v2, 42)
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let (v1, v2, answer) = foo(v1, v2);
これでは借用の恩恵を得られず、Rustらしくありません。まず初めに以下のようにしてみます。
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// v1とv2で何かする
// 結果を返す
42
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let answer = foo(&v1, &v2);
// v1とv2をここで使う事ができます!
引数をVec<i32>
とする代わりに、その参照である&Vec<i32>
にします。またv1
とv2
を直接渡す代わりに、&v1
と&v2
を渡します。私たちは&T
型を'参照'と呼んでおり、参照はリソース自体を所有するのではなくその所有権を借用します。借用している束縛はスコープの外に出てもリソースを解放しません。つまりfoo()
の呼び出し後、元の束縛を再び使うことができるわけです。
参照は束縛と同じくイミュータブルです。つまり以下のfoo()
内で、vectorは全く変えられません。
fn foo(v: &Vec<i32>) {
v.push(5);
}
let v = vec![];
foo(&v);
エラーは以下のようになります。
error: cannot borrow immutable borrowed content `*v` as mutable
v.push(5);
^
値のプッシュはvectorを変化させるため許されません。
&mut 参照
2種類目の参照が&mut T
です。'ミュータブルな参照'はあなたが借用しているリソースの変化を許します。例えば、
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x);
これは6
を出力します。x
へのミュータブルな参照y
を作り、y
が指す値に1を加えています。x
も同様にmut
と付ける必要があることに気が付くと思いますが、もしそれが無い場合、ミュータブルな借用をイミュータブルな値として束縛できません。
また、私たちがy
の前にアスタリスク(*
)を加えたことに気付くと思いますが、*y
とするのはy
が&mut
参照であるためです。これも参照の内容にアクセスするために必要です。
以上のことを除けば、&mut
参照は通常の参照と同じです。2つの間には大きな違いが_ある_ものの、これらは互いに影響を及ぼし合います2。あなたは前述の例について、{
と}
による余計なスコープが必要なのはおかしいと言いたくなるかもしれませんが、これを取り除くとエラーが発生します。
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow ends
let y = &mut x;
^
note: previous borrow ends here
fn main() {
}
^
結論から言えば、ルールがあるのです。
借用のルール
以下でRustにおける借用のルールについて説明します。
第1に、任意の借用のスコープは元の持ち主より長く続いてはいけません。第2に、あなたは2種類の借用の内どちらか一方を用いても良いですが、同時に両方を用いることは許されません。
- リソースへの1つ以上の参照(
&T
)か、 - ただ1つだけのミュータブルな参照(
&mut T
)のどちらかです。
データ競合(data race)の定義に良く似ていることに気付くかもしれませんが、厳密には異なります。
2つ以上のポインタが同じメモリ領域へ同時にアクセスした時、その何れか1つが書き込み命令であり、かつ命令が同期的でなければデータ競合が生じたとします。
参照については、書き込むことができないため好きなだけ確保できます。もし書き込む場合、2つ以上のポインタが同じメモリを指していることになるため、あなたが1度に持てる&mut
は1つだけです。これがRustにおけるコンパイル時にデータ競合を防ぐ方法であり、このルールを破るとエラーが発生します。
このことを念頭において、先程の例を再び考えていきましょう。
スコープについて考える
let mut x = 5;
let y = &mut x;
*y += 1;
println!("{}", x);
このコードは以下のエラーを引き起こします。
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
これはルールを違反したからです。x
を指す&mut T
があるため、&T
を作成することは許されません。あくまでどちらか一方です。noteはこの問題に対する考え方のヒントになります。
note: previous borrow ends here
fn main() {
}
^
これを言い換えると、ミュータブルな借用は前述の例の終わりまで保持されていたということです。私たちが欲しいのは、println!
を呼び出そうとする_前に_ミュータブルな借用を終わらせ、その後イミュータブルな借用を作成するための何かです。Rustにおいて、借用の作成は借用が有効であるスコープと関わりがあります。スコープは以下になります。
let mut x = 5;
let y = &mut x; // -+ ここからxの&mut借用が始まります
// |
*y += 1; // |
// |
println!("{}", x); // -+ - ここでxの借用を試みます
// -+ ここでxの&mut借用が終わります
2つのスコープが衝突しています。y
のスコープ内に居る間、&x
は作成できません。
こういった時は波括弧を追加します。
let mut x = 5
{
let y = &mut x; // -+ ここから&mut借用が始まります
*y += 1; // |
} // -+ ... そしてここで終わります
println!("{}", x); // <- ここでxの借用を試みます
問題は解消されました。イミュータブルな借用を作成する前にミュータブルな借用のスコープが終了しています。スコープは借用がどれだけ続くかを視覚化するための鍵なのです。
借用によって防げる問題
なぜこれらのような制限があるのでしょうか?既に説明したように、これらのルールはデータ競合を防ぐためにあります。ではデータ競合を引き起こす問題とは何でしょうか?ここでは幾つかの問題について説明します。
イテレータの無効化
例の1つが'イテレータの無効化'であり、イテレート中にコレクションへの変更を試みた際に発生します。Rustのボローチェッカーはこれの発生を防ぎます。
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
}
これは1から3を出力します。vectorをイテレートしている間、私たちに与えられるのは要素への参照だけです。そしてv
それ自体はイミュータブルとして借用されますから、イテレート中にv
を変えることもできないというわけです。
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
v.push(34);
}
以下がエラーになります。
error: cannot borrow `v` as mutable because it is also borrowed as immutable
v.push(34);
^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
^
note: previous borrow ends here
for i in &v {
println!(“{}”, i);
v.push(34);
}
^
ループにイミュータブルとして借用されているためv
は変更できません。
解放後の使用
参照は参照元のリソースより長く生存してはいけません。Rustはこれが真であることを保証するためにあなたが作成した参照のスコープを確認します。
もしRustがこれを確認しなければ、私たちは不正になった参照をうっかり使用できてしまいます。例えば、
let y: &i32;
{
let x = 5;
y = &x;
}
println!("{}", y);
これは以下のエラーが発生します。
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
{
let x = 5;
y = &x;
}
note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}
つまり、y
はx
が存在するスコープの間でのみ有効なのです。x
が寿命を迎えるやいなや、それへの参照は不正な値になってしまいます。このような場合、エラーは適切な時間有効になっていないことから'doesn't live long enough (十分な長さ生存しない)'と報告します。
参照するつもりの変数より_前に_その参照自体が宣言される場合にも同様の問題が発生します。同じスコープ内のリソースは宣言された順序の逆順に開放されるからです。
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
従って以下のエラーが発生します。
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
}
note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
let x = 5;
y = &x;
println!("{}", y);
}
上述の例において、y
はx
の前に宣言されており、y
がx
よりも長く生存するため、コンパイルは通りません。