暫く放置していたのですがモチベが湧いたので再開します。
今の所、所有権借用生存期から並列処理周りの翻訳を行っていきたいと考えています。
前: 5.23. Trait Objects 次: 5.8. References and Borrowing
誤訳や改善案等、見つけた場合はご指摘の程宜しくお願いします。
- 元記事 : https://doc.rust-lang.org/book/ownership.html のRust1.5時点
- ライセンス : MIT license, Apache License 2.0.
- See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.
所有権
このガイドはRustの所有権システムに含まれる3要素の内の1つについて示したものです。これはRustの開発者が熟達すべきであり、かつRustの最もユニークで魅力的な機能の1つです。所有権はRustの最大の目的であるメモリ安全性を実現する方法です。以下の章でそれぞれ別個の概念について説明しています。
これら3つの章はその順序にも関係があります。所有権システムの完全な理解のためには3つ全ての読解が必要となるでしょう。
メタ1
詳細について見て行く前に、所有権システムについて2つ重要な注意事項があります。
Rustは安全性と速度に焦点を当てています。それら目的は、Rustにおいて動作のための抽象化コストを可能な限り抑えることを意味する'ゼロコストの抽象化'を多く行うことで達成しています。所有権システムはゼロコストの抽象化の典型的な例です。私たちがこのガイドで話す詳細の全ては_コンパイル時に行われます_。あなたはこれらの機能のために実行時コストを払うことはありません。
しかしながら、このシステムは確実にコストを要し、その影響は学習曲線に現れます。多くのRustの新規ユーザは自身が有効だと考えているプログラムのコンパイルをコンパイラに拒否されるという、所謂'ボローチェッカーとの戦い'を経験しています。これがしばしば起きるのはプログラマが頭の中で考える所有権がこのように動作するはずだというモデルとRustに実装された実際のルールが一致しないためです。あなたも恐らく初めは同じようなことを経験するでしょう。しかしながら良い知らせもあります。経験豊富なRustの開発者曰く、一度でもある程度の期間所有権システムのルールの下で作業すれば、借用チェッカーとの戦いは次第に減っていくとのことです。
このことを念頭において、所有権について学んでいきましょう。
所有権
変数束縛はRustにおいて所有権を持つ方法です。束縛している対象の'所有権を持っている'のです。これは束縛している変数がスコープ外に出ると、束縛されていたリソースが開放されることを意味しています。例えば、
fn foo() {
let v = vec![1, 2, 3];
}
v
がスコープに入ってくると、新たなVec<T>
が作成されます。このケースでは、vectorは3つの要素のためにヒープ領域の空間をアロケートします。foo()
が終了しv
がスコープの外へ出ると、Rustはヒープ領域にアロケートされたメモリも含めvectorに関連する全てを片付けます。これはスコープの終端に達したとき、確定的に行われます。
ムーブセマンティクス
少し細かい話になりますが、Rustはあらゆるリソースについて束縛が_厳密に1つ_だけあることを保証します。例えばvectorを作成したとして、もう1つ束縛をアサインできます。
let v = vec![1, 2, 3];
let v2 = v;
しかし、その後v
を使おうとすると、エラーが発生します。
let v = vec![1, 2, 3];
let v2 = v;
println!("v[0] is: {}", v[0]);
エラーの内容は次のようになります。
error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
^
私たちが所有権を受け取る関数を定義し、引数として渡した後に変数を使おうとする場合にも同じようなことが起きます。
fn take(v: Vec<i32>) {
// ここで起きることは重要ではありません。
}
let v = vec![1, 2, 3];
take(v);
println!("v[0] is: {}", v[0]);
'use of moved value'、同じエラーですね。何か他の対象へ所有権を譲渡するとき、私たちはその値を'ムーブ'したと言います。このとき特別なアノテーションの類は必要なく、Rustの動作はこれがデフォルトです。
詳細
ムーブした後の値を使うことができない理由は細かいようですが重要な話です。このようなコードを書いているとき、
let v = vec![1, 2, 3];
let v2 = v;
1行目はvectorオブジェクトv
と格納しているデータのためにメモリをアロケートしています。vectorオブジェクトはスタック領域に保存されており、ヒープ領域に保存されている要素([1, 2, 3]
)へのポインタを格納しています。v
をv2
へムーブすると、v2
のためにポインタのコピーを作成します。するとヒープ領域上のvectorの要素を指すポインタが2つになってしまい、データの競合が引き起こされ、Rustの安全性の保証を違反することになります。従って、Rustはムーブした後のv
の使用を禁じているのです。
また、状況に応じて、最適化がスタック領域上のデータのコピーを取り除く可能性に気を払うことも大切です。ですから最初に思ったほど非効率的ではないかもしれません。
Copy
型
所有権が他の束縛に移譲されると元の束縛が使用できなくなる所までは話してきました。しかしながら、その振る舞いを変えるトレイトがあり、それはCopy
と呼ばれています。私たちはまだトレイトについて議論することができませんから、今だけ振る舞いを追加する特殊な型のためのアノテーションと考えてください。例えば、
let v = 1;
let v2 = v;
println!("v is: {}", v);
このケースでは、v
はCopy
トレイトを実装するi32
です。これはムーブと同じくv
をv2
へアサインする際に、データのコピーが生成されるという意味です。ただし、ムーブと異なり、その後もv
を使うことができます。何故ならi32
はどんなポインタも含んでおらず、「データのコピー」とは値自体を丸ごとコピーすることだからです。
全てのプリミティブ型はCopyトレイトを実装しているため、それらの所有権は'所有権のルール'に則ったムーブが行われません。例を挙げるならi32
とbool
型がCopy
トレイトを実装していますから、以下の2つのスニペットをコンパイルしてみます。
fn main() {
let a = 5;
let _y = double(a);
println!("{}", a);
}
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
let a = true;
let _y = change_truth(a);
println!("{}", a);
}
fn change_truth(x: bool) -> bool {
!x
}
もしこれがCopy
トレイトを実装していない型だったとすると、ムーブされた値を使おうとすることになりますからコンパイルエラーになるでしょう。
error: use of moved value: `a`
println!("{}", a);
^
あなた自身が作った型をCopy
する方法についてはトレイトの章で論じています。
所有権よりも
勿論、元々持っていた所有権を関数に返して欲しければこう書くでしょう。
fn foo(v: Vec<i32>) -> Vec<i32> {
// vで何かする
// 所有権を返す
v
}
これだと非常にうんざりすることになるはずです。受け取りたい所有権が多くなる程悪化してしまいます。
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はこの問題を解決するのに役立つ、借用という機能を提供しています。それが次の章の話題です!
訳注
-
所有権システム3要素の全ページに記載されている話です。内容は変わりません。 ↩