所有権とは?
Rustの中心的な機能は、所有権です。この機能は、説明するのは簡単なのですが、言語の残りの機能全てにかかるほど 深い裏の意味を含んでいるのです。
全てのプログラムは、実行中にコンピュータのメモリの使用方法を管理する必要があります。プログラムが動作するにつれて、 定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、 プログラマが明示的にメモリを確保したり、解放したりしなければなりません。Rustでは第3の選択肢を取っています: メモリは、コンパイラがコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。
所有権は多くのプログラマにとって新しい概念なので、慣れるまでに時間がかかります。 嬉しいことに、Rustと、所有権システムの規則の経験を積むと、より自然に安全かつ効率的なコードを構築できるようになります。 その調子でいきましょう!
所有権を理解した時、Rustを際立たせる機能の理解に対する強固な礎を得ることになるでしょう。この章では、 非常に一般的なデータ構造に着目した例を取り扱うことで所有権を学んでいきます: 文字列です。
スタックとヒープ
多くのプログラミング言語において、スタックとヒープについて考える機会はそう多くないでしょう。 しかし、Rustのようなシステムプログラミング言語においては、値がスタックに積まれるかヒープに置かれるかは、 言語の振る舞い方や、特定の決断を下す理由などに影響以上のものを与えるのです。 この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。
スタックもヒープも、実行時にコードが使用できるメモリの一部になりますが、異なる手段で構成されています。 スタックは、得た順番に値を並べ、逆の順で値を取り除いていきます。これは、 last in, first out(
訳注
: あえて日本語にするなら、「最後に入れたものが最初に出てくる」といったところでしょうか)と呼ばれます。 お皿の山を思い浮かべてください: お皿を追加する時には、山の一番上に置き、お皿が必要になったら、一番上から1枚を取り去りますよね。 途中や一番下に追加したり、取り除いたりすることもできません。データを追加することは、 スタックにpushするといい、データを取り除くことは、スタックからpopすると表現します(訳注
: 日本語では単純に英語をそのまま活用してプッシュ、ポップと表現するでしょう)。データへのアクセス方法のおかげで、スタックは高速です: 新しいデータを置いたり、 データを取得する場所を探す必要が絶対にないわけです。というのも、その場所は常に一番上だからですね。 スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定サイズでなければならないということです。
コンパイル時にサイズがわからなかったり、サイズが可変のデータについては、代わりにヒープに格納することができます。 ヒープは、もっとごちゃごちゃしています: ヒープにデータを置く時、あるサイズのスペースを求めます。 OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返します。ポインタとは、その場所へのアドレスです。 この過程は、ヒープに領域を確保する(allocating on the heap)と呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。 (
訳注
: こちらもこなれた日本語訳はないでしょう。allocateは「メモリを確保する」と訳したいところですが) スタックに値を積むことは、メモリ確保とは考えられません。ポインタは、既知の固定サイズなので、 スタックに保管することができますが、実データが必要になったら、ポインタを追いかける必要があります。レストランで席を確保することを考えましょう。入店したら、グループの人数を告げ、 店員が全員座れる空いている席を探し、そこまで誘導します。もしグループの誰かが遅れて来るのなら、 着いた席の場所を尋ねてあなたを発見することができます。
ヒープへのデータアクセスは、スタックのデータへのアクセスよりも低速です。 ポインタを追って目的の場所に到達しなければならないからです。現代のプロセッサは、メモリをあちこち行き来しなければ、 より速くなります。似た例えを続けましょう。レストランで多くのテーブルから注文を受ける給仕人を考えましょう。最も効率的なのは、 次のテーブルに移らずに、一つのテーブルで全部の注文を受け付けてしまうことです。テーブルAで注文を受け、 それからテーブルBの注文、さらにまたA、それからまたBと渡り歩くのは、かなり低速な過程になってしまうでしょう。 同じ意味で、プロセッサは、 データが隔離されている(ヒープではそうなっている可能性がある)よりも近くにある(スタックではこうなる)ほうが、 仕事をうまくこなせるのです。ヒープに大きな領域を確保する行為も時間がかかることがあります。
コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。
どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。
所有権規則
まず、所有権のルールについて見ていきましょう。 この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください:
- Rustの各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
変数スコープ
第2章で、Rustプログラムの例はすでに見ています。もう基本的な記法は通り過ぎたので、
fn main() {
というコードはもう例に含みません。従って、例をなぞっているなら、
これからの例はmain
関数に手動で入れ込まなければいけなくなるでしょう。結果的に、例は少々簡潔になり、
定型コードよりも具体的な詳細に集中しやすくなります。
所有権の最初の例として、何らかの変数のスコープについて見ていきましょう。スコープとは、 要素が有効になるプログラム内の範囲のことです。以下のような変数があるとしましょう:
#![allow(unused)] fn main() { let s = "hello"; }
変数s
は、文字列リテラルを参照し、ここでは、文字列の値はプログラムのテキストとしてハードコードされています。
この変数は、宣言された地点から、現在のスコープの終わりまで有効になります。リスト4-1には、
変数s
が有効な場所に関する注釈がコメントで付記されています。
#![allow(unused)] fn main() { { // sは、ここでは有効ではない。まだ宣言されていない let s = "hello"; // sは、ここから有効になる // sで作業をする } // このスコープは終わり。もうsは有効ではない }
言い換えると、ここまでに重要な点は二つあります:
s
がスコープに入ると、有効になる- スコープを抜けるまで、有効なまま
ここで、スコープと変数が有効になる期間の関係は、他の言語に類似しています。さて、この理解のもとに、
String
型を導入して構築していきましょう。
String
型
所有権の規則を具体化するには、第3章の「データ型」節で講義したものよりも、より複雑なデータ型が必要になります。 以前講義した型は全てスタックに保管され、スコープが終わるとスタックから取り除かれますが、 ヒープに確保されるデータ型を観察して、 コンパイラがどうそのデータを掃除すべきタイミングを把握しているかを掘り下げていきたいと思います。
ここでは、例としてString
型を使用し、String
型の所有権にまつわる部分に着目しましょう。
また、この観点は、標準ライブラリや自分で生成する他の複雑なデータ型にも適用されます。
String
型については、第8章でより深く議論します。
既に文字列リテラルは見かけましたね。文字列リテラルでは、文字列の値はプログラムにハードコードされます。
文字列リテラルは便利ですが、テキストを使いたいかもしれない場面全てに最適なわけではありません。一因は、
文字列リテラルが不変であることに起因します。別の原因は、コードを書く際に、全ての文字列値が判明するわけではないからです:
例えば、ユーザ入力を受け付け、それを保持したいとしたらどうでしょうか?このような場面用に、Rustには、
2種類目の文字列型、String
型があります。この型はヒープにメモリを確保するので、
コンパイル時にはサイズが不明なテキストも保持することができるのです。from
関数を使用して、
文字列リテラルからString
型を生成できます。以下のように:
#![allow(unused)] fn main() { let s = String::from("hello"); }
この二重コロンは、string_from
などの名前を使うのではなく、
String
型直下のfrom
関数を特定する働きをする演算子です。この記法について詳しくは、
第5章の「メソッド記法」節と、第7章の「モジュール定義」でモジュールを使った名前空間分けについて話をするときに議論します。
この種の文字列は、可変化することができます:
#![allow(unused)] fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える println!("{}", s); // これは`hello, world!`と出力する }
では、ここでの違いは何でしょうか?なぜ、String
型は可変化できるのに、リテラルはできないのでしょうか?
違いは、これら二つの型がメモリを扱う方法にあります。
メモリと確保
文字列リテラルの場合、中身はコンパイル時に判明しているので、テキストは最終的なバイナリファイルに直接ハードコードされます。 このため、文字列リテラルは、高速で効率的になるのです。しかし、これらの特性は、 その文字列リテラルの不変性にのみ端を発するものです。残念なことに、コンパイル時にサイズが不明だったり、 プログラム実行に合わせてサイズが可変なテキスト片用に一塊のメモリをバイナリに確保しておくことは不可能です。
String
型では、可変かつ伸長可能なテキスト破片をサポートするために、コンパイル時には不明な量のメモリを
ヒープに確保して内容を保持します。つまり:
- メモリは、実行時にOSに要求される。
String
型を使用し終わったら、OSにこのメモリを返還する方法が必要である。
この最初の部分は、既にしています: String::from
関数を呼んだら、その実装が必要なメモリを要求するのです。
これは、プログラミング言語において、極めて普遍的です。
しかしながら、2番目の部分は異なります。ガベージコレクタ(GC)付きの言語では、GCがこれ以上、
使用されないメモリを検知して片付けるため、プログラマは、そのことを考慮する必要はありません。
GCがないなら、メモリがもう使用されないことを見計らって、明示的に返還するコードを呼び出すのは、
プログラマの責任になります。ちょうど要求の際にしたようにですね。これを正確にすることは、
歴史的にも難しいプログラミング問題の一つであり続けています。もし、忘れていたら、メモリを無駄にします。
タイミングが早すぎたら、無効な変数を作ってしまいます。2回解放してしまっても、バグになるわけです。
allocate
とfree
は完璧に1対1対応にしなければならないのです。
Rustは、異なる道を歩んでいます: ひとたび、メモリを所有している変数がスコープを抜けたら、
メモリは自動的に返還されます。こちらの例は、
リスト4-1のスコープ例を文字列リテラルからString
型を使うものに変更したバージョンになります:
#![allow(unused)] fn main() { { let s = String::from("hello"); // sはここから有効になる // sで作業をする } // このスコープはここでおしまい。sは // もう有効ではない }
String
型が必要とするメモリをOSに返還することが自然な地点があります: s
変数がスコープを抜ける時です。
変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。この関数は、drop
と呼ばれ、
ここにString
型の書き手はメモリを返還するコードを配置することができます。Rustは、閉じ波括弧で自動的にdrop
関数を呼び出します。
注釈: C++では、要素の生存期間の終了地点でリソースを解放するこのパターンを時に、 RAII(Resource Aquisition Is Initialization: リソースの獲得は、初期化である)と呼んだりします。 Rustの
drop
関数は、あなたがRAIIパターンを使ったことがあれば、馴染み深いものでしょう。
このパターンは、Rustコードの書かれ方に甚大な影響をもたらします。現状は簡単そうに見えるかもしれませんが、 ヒープ上に確保されたデータを複数の変数に使用させるようなもっと複雑な場面では、コードの振る舞いは、 予期しないものになる可能性もあります。これから、そのような場面を掘り下げてみましょう。
変数とデータの相互作用法: ムーブ
Rustにおいては、複数の変数が同じデータに対して異なる手段で相互作用することができます。 整数を使用したリスト4-2の例を見てみましょう。
#![allow(unused)] fn main() { let x = 5; let y = x; }
もしかしたら、何をしているのか予想することができるでしょう:
「値5
をx
に束縛する; それからx
の値をコピーしてy
に束縛する。」これで、
二つの変数(x
とy
)が存在し、両方、値は5
になりました。これは確かに起こっている現象を説明しています。
なぜなら、整数は既知の固定サイズの単純な値で、これら二つの5
という値は、スタックに積まれるからです。
では、String
バージョンを見ていきましょう:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
このコードは先ほどのコードに酷似していますので、動作方法も同じだと思い込んでしまうかもしれません:
要するに、2行目でs1
の値をコピーし、s2
に束縛するということです。ところが、
これは全く起こることを言い当てていません。
図4-1を見て、ベールの下でString
に何が起きているかを確かめてください。
String
型は、左側に示されているように、3つの部品でできています:
文字列の中身を保持するメモリへのポインタと長さ、そして、許容量です。この種のデータは、スタックに保持されます。
右側には、中身を保持したヒープ上のメモリがあります。
長さは、String
型の中身が現在使用しているメモリ量をバイトで表したものです。許容量は、
String
型がOSから受け取った全メモリ量をバイトで表したものです。長さと許容量の違いは問題になることですが、
この文脈では違うので、とりあえずは、許容量を無視しても構わないでしょう。
s1
をs2
に代入すると、String
型のデータがコピーされます。つまり、スタックにあるポインタ、長さ、
許容量をコピーするということです。ポインタが指すヒープ上のデータはコピーしません。言い換えると、
メモリ上のデータ表現は図4-2のようになるということです。
メモリ上の表現は、図4-3のようにはなりません。これは、
Rustが代わりにヒープデータもコピーするという選択をしていた場合のメモリ表現ですね。Rustがこれをしていたら、
ヒープ上のデータが大きい時にs2 = s1
という処理の実行時性能がとても悪くなっていた可能性があるでしょう。
先ほど、変数がスコープを抜けたら、Rustは自動的にdrop
関数を呼び出し、
その変数が使っていたヒープメモリを片付けると述べました。しかし、図4-2は、
両方のデータポインタが同じ場所を指していることを示しています。これは問題です: s2
とs1
がスコープを抜けたら、
両方とも同じメモリを解放しようとします。これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの一つになります。
メモリを2回解放することは、memory corruption (訳注
: メモリの崩壊。意図せぬメモリの書き換え) につながり、
セキュリティ上の脆弱性を生む可能性があります。
メモリ安全性を保証するために、Rustにおいてこの場面で起こることの詳細がもう一つあります。
確保されたメモリをコピーしようとする代わりに、コンパイラは、s1
が最早有効ではないと考え、
故にs1
がスコープを抜けた際に何も解放する必要がなくなるわけです。s2
の生成後にs1
を使用しようとしたら、
どうなるかを確認してみましょう。動かないでしょう:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
コンパイラが無効化された参照は使用させてくれないので、以下のようなエラーが出るでしょう:
error[E0382]: use of moved value: `s1`
(ムーブされた値の使用: `s1`)
--> src/main.rs:5:28
|
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value used here after move
| (ムーブ後にここで使用されています)
|
= note: move occurs because `s1` has type `std::string::String`, which does
not implement the `Copy` trait
(注釈: ムーブが起きたのは、`s1`が`std::string::String`という
`Copy`トレイトを実装していない型だからです)
他の言語を触っている間に"shallow copy"と"deep copy"という用語を耳にしたことがあるなら、
データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。
ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、
ムーブとして知られているわけです。この例では、s1
はs2
にムーブされたと表現するでしょう。
以上より、実際に起きることを図4-4に示してみました。
これにて一件落着です。s2
だけが有効なので、スコープを抜けたら、それだけがメモリを解放して、
終わりになります。
付け加えると、これにより暗示される設計上の選択があります: Rustでは、 自動的にデータの"deep copy"が行われることは絶対にないわけです。それ故に、あらゆる自動コピーは、実行時性能の観点で言うと、 悪くないと考えてよいことになります。
変数とデータの相互作用法: クローン
仮に、スタック上のデータだけでなく、本当にString
型のヒープデータのdeep copyが必要ならば、
clone
と呼ばれるよくあるメソッドを使うことができます。メソッド記法については第5章で議論しますが、
メソッドは多くのプログラミング言語に見られる機能なので、以前に見かけたこともあるんじゃないでしょうか。
これは、clone
メソッドの動作例です:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
これは問題なく動作し、図4-3で示した動作を明示的に生み出します。ここでは、 ヒープデータが実際にコピーされています。
clone
メソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高いと把握できます。
何か違うことが起こっているなと見た目でわかるわけです。
スタックのみのデータ: コピー
まだ話題にしていない別の問題があります。 この整数を使用したコードは、一部をリスト4-2で示しましたが、うまく動作する有効なものです:
#![allow(unused)] fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
ですが、このコードは一見、今学んだことと矛盾しているように見えます:
clone
メソッドの呼び出しがないのに、x
は有効で、y
にムーブされませんでした。
その理由は、整数のようなコンパイル時に既知のサイズを持つ型は、スタック上にすっぽり保持されるので、
実際の値をコピーするのも高速だからです。これは、変数y
を生成した後にもx
を無効化したくなる理由がないことを意味します。
換言すると、ここでは、shallow copyとdeep copyの違いがないことになり、
clone
メソッドを呼び出しても、一般的なshallow copy以上のことをしなくなり、
そのまま放置しておけるということです。
RustにはCopy
トレイトと呼ばれる特別な注釈があり、
整数のようなスタックに保持される型に対して配置することができます(トレイトについては第10章でもっと詳しく話します)。
型がCopy
トレイトに適合していれば、代入後も古い変数が使用可能になります。コンパイラは、
型やその一部分でもDrop
トレイトを実装している場合、Copy
トレイトによる注釈をさせてくれません。
型の値がスコープを外れた時に何か特別なことを起こす必要がある場合に、Copy
注釈を追加すると、コンパイルエラーが出ます。
型にCopy
注釈をつける方法について学ぶには、付録Cの「導出可能なトレイト」をご覧ください。
では、どの型がCopy
なのでしょうか?ある型について、ドキュメントをチェックすればいいのですが、
一般規則として、単純なスカラー値の集合は何でもCopy
であり、メモリ確保が必要だったり、
何らかの形態のリソースだったりするものはCopy
ではありません。ここにCopy
の型の一部を並べておきます。
- あらゆる整数型。
u32
など。 - 論理値型である
bool
。true
とfalse
という値がある。 - あらゆる浮動小数点型、
f64
など。 - 文字型である
char
。 - タプル。ただ、
Copy
の型だけを含む場合。例えば、(i32, i32)
はCopy
だが、(i32, String)
は違う。
所有権と関数
意味論的に、関数に値を渡すことと、値を変数に代入することは似ています。関数に変数を渡すと、 代入のようにムーブやコピーされます。リスト4-3は変数がスコープに入ったり、 抜けたりする地点について注釈してある例です。
ファイル名: src/main.rs
fn main() { let s = String::from("hello"); // sがスコープに入る takes_ownership(s); // sの値が関数にムーブされ... // ... ここではもう有効ではない let x = 5; // xがスコープに入る makes_copy(x); // xも関数にムーブされるが、 // i32はCopyなので、この後に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
を呼び出そうとすると、コンパイラは、コンパイルエラーを投げるでしょう。
これらの静的チェックにより、ミスを犯さないでいられます。s
やx
を使用するコードをmain
に追加してみて、
どこで使えて、そして、所有権規則により、どこで使えないかを確認してください。
戻り値とスコープ
値を返すことでも、所有権は移動します。リスト4-4は、リスト4-3と似た注釈のついた例です。
ファイル名: src/main.rs
fn main() { let s1 = gives_ownership(); // gives_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 { // gives_ownershipは、戻り値を // 呼び出した関数にムーブする let some_string = String::from("hello"); // some_stringがスコープに入る some_string // some_stringが返され、呼び出し元関数に // ムーブされる } // takes_and_gives_backは、Stringを一つ受け取り、返す。 fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。 a_string // a_stringが返され、呼び出し元関数にムーブされる }
変数の所有権は、毎回同じパターンを辿っています: 別の変数に値を代入すると、ムーブされます。
ヒープにデータを含む変数がスコープを抜けると、データが別の変数に所有されるようムーブされていない限り、
drop
により片付けられるでしょう。
所有権を取り、またその所有権を戻す、ということを全ての関数でしていたら、ちょっとめんどくさいですね。 関数に値は使わせるものの所有権を取らないようにさせるにはどうするべきでしょうか。 返したいと思うかもしれない関数本体で発生したあらゆるデータとともに、再利用したかったら、渡されたものをまた返さなきゃいけないのは、 非常に煩わしいことです。
タプルで、複数の値を返すことは可能です。リスト4-5のようにですね。
ファイル名: src/main.rs
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()メソッドは、Stringの長さを返します (s, length) }
でも、これでは、大袈裟すぎますし、ありふれているはずの概念に対して、作業量が多すぎます。 私たちにとって幸運なことに、Rustにはこの概念に対する機能があり、参照と呼ばれます。