OwnershipはRustで最も独特な機能であり、ガベージコレクションなしにRustのメモリ安全を保証するものである。 したがって、RustでどのようにOwnershipが動作するかを理解することは重要である。本章では、Ownershipはもちろん、関連するいくつかの機能 - borrowing(借用)、slices、Rustによるメモリへのデータ格納 - について述べる。
-
-
Save S-Shimotori/4bb330cab155d39648f2fd0743443ce7 to your computer and use it in GitHub Desktop.
Rustの中心となる機能はownershipである。この機能を説明するのは簡単だが、言語の残りの部分に対して深い含みを持つ。
全てのプログラムは実行中のコンピュータのメモリの使用方法を管理する必要がある。一部の言語は、プログラム実行時にもう使われないメモリを絶えず探すガベージコレクションを採用している。別の言語ではプログラマーが明示的にメモリの割り当てと解放を行わなければならないようになっている。どうか辛抱してほしい。
多くのプログラマーにとってownershipは新しい概念なので、慣れるまでにある程度の時間を必要とする。幸い、Rustとownershipシステムの経験を積んでいくと安全で効率的なコードの開発を自然に行えるようになる。
ownershipを理解したとき、Rustを独特なものとするこの機能への理解の堅実な基礎ができあがっていることだろう。本章では、非常に一般的なデータ構造であるstringを用いるいくつかの例を通してownershipを学習する。
多くのプログラミング言語では、頻繁にスタック領域やヒープ領域のことを考えなくても良くなっている。 しかしRustのようなシステムプログラミング言語では、値がスタック領域とヒープ領域のどちらに格納されているのかは「どのように言語が振る舞うか」「なぜ我々が一定の決定をする必要があるか」といったことに対してより強い影響を持っている。 本章では後にスタック領域及びヒープ領域に関連する部分のownershipの説明を行うので、ここで簡単な説明をして備える。
スタック領域とヒープ領域はどちらも実行時にコードが使うことのできるメモリの一部分だが、それぞれ異なった方法で構造化されている。スタック領域は取得順に値を格納し、その逆の順で値を削除する。これは後入れ先出しと呼ばれる。 お皿のスタックを考える - 追加されたお皿はお皿の山の1番上に置かれ、必要な時は1番上から1枚取る。山の真ん中や下にお皿を追加したり取り出したりしてもうまくいかないだろう。データの追加を「スタック領域にpushする」といい、データの削除を「スタック領域からpopする」という。
データへのアクセス方法の関係でスタック領域は高速に動作する。新しいデータの追加先やデータ取得元の場所は必ず1番上なので、探索を行う必要はない。 スタック領域が高速に動作するもう1つの要因は、スタック領域内の全データが既知の固定サイズを占有しなければならないことである。
コンパイル時にサイズのわからないデータやサイズが変わる可能性のあるデータは、代わりにヒープ領域に格納される。ヒープ領域はあまり整理されていない。ヒープ領域にデータを格納するときはある程度の量のスペースを要求する。 OSはヒープ領域のいずれかの十分に大きい空き領域を探し、使用中であるとマークし、その場所のポインタを返却する。このプロセスはヒープ領域への割り当てといい、しばしば単に割り当てと呼ぶ。スタック領域への値のpushは割り当てとはみなされない。 ポインタは既知で固定のサイズなのでそのポインタをスタック領域に格納することができるが、実データが必要な時にそのポインタをたどる必要がある。
レストランに着席することを考える。入店した時、グループの人数を伝え、店員は全員が座れるような空きテーブルを探してそこに連れて行く。もしグループの中の誰かが遅刻してくるなら、あなたを探すためにどこに座ったのか訪ねることができる。
ヒープ領域内のデータへのアクセスは、その場所に到達するためにポインタをたどる必要があるため、スタック領域内のデータへのアクセスよりも時間がかかる。少ないメモリでジャンプするなら最近のプロセッサは速く動作する。 同様にして、多くのテーブルから注文を受けているレストランの給仕人を考える。次のテーブルに移る前に1つのテーブルで全ての注文を受けるのが最も効率的である。 テーブルAから1つ注文を受け、次にテーブルBから1つ注文を受け、そしてまたAから1つ注文を受けて再びBから1つ注文を受けるのでは、よりプロセスが遅くなる。 これと同じで、遠く(ヒープ領域内)よりも近く(スタック領域内)にあるデータに対して動作する時の方がプロセッサはより良い働きをする。また、ヒープ領域に巨大なスペースを割り当てるのにも時間がかかる。
コードが関数を呼び出すとき、関数に渡す値(潜在的にはヒープ領域内のデータへのポインタを含む)や関数のローカル変数はスタック領域にpushされる。関数が終了した時にはそれらの値はスタック領域からpopされる。
コードがヒープ領域内のどのデータを使用しているのかを把握し、ヒープ領域内の重複したデータの量を最小限にし、領域を使い切らないようにヒープ領域内の未使用データを片付けることが、ownershipが取り組んでいる全ての問題である。 一旦ownershipを理解すれば、頻繁にスタック領域やヒープ領域のことを考える必要はなくなるが、ヒープ領域の管理がownershipの存在理由であることを知れば、何故ownershipがそのように動作するのかを説明するのに役立つ。
ヒープデータを管理することはオーナーシップが存在する理由で、なぜそれが動作するのかを説明するのに役立ちます。
まずownershipの規則を見る。ルールを説明する例を扱う前にこれらの規則を覚えておく。
- Rustの各値はownerと呼ばれる変数を持つ。
- 所有できるownerは同時に1つしか存在しない。
- ownerがスコープの外に出たとき値は削除される。
すでに2章でRustのプログラムの例を動作させた。基本構文の紹介が終わって例の中に全ての fn main() コードを含めることはしないので、理解のためには以下の例をmain関数の中に自分の手で入れる必要がある。全てのコードを含めないことで例が少し簡潔になり、典型的なコードではなく現在の細目に焦点をあてられるようになった。
ownershipの最初の例として、いくつかの変数のスコープに着目する。スコープはアイテムが有効となるプログラムの範囲である。次のような変数があるとする。
let s = "hello";変数 s は文字列リテラルを参照しており、文字列の値はプログラム中に直接記述されている。この変数は宣言された地点から現在のスコープの終わりまで有効である。リスト4-1は変数 s が有効な場所をコメントで示している。
{ // まだ宣言していないのでここではsは有効ではない
let s = "hello"; // ここからsは有効
// sを用いていろいろなことをする
} // ここでスコープが終了したのでsはもう有効ではないリスト 4-1: 変数とその変数が有効なスコープ
言い換えれば、ここに2つの重要なポイントがある。 In other words, there are two important points in time here:
sがスコープに加わったとき、sは有効である。sはスコープの外に到達するまで有効のままである。
この時点で、スコープと変数が有効なときの関係は他のプログラミング言語と似ている。次に String 型を紹介して理解を重ねる。
ownershipの規則の説明には、3章で扱ったデータ型よりも複雑なデータ型が必要である。ここまで見てきたデータ型は全てスタック領域に格納され、スコープが終了した時にスタック領域からpopされる。ここではヒープ領域に格納されるデータに注目し、Rustがいつデータを片付けるかをどのように知るのかを考える。
ここでは String を例に用い、ownershipに関する部分の String に集中する。これらの特徴は標準ライブラリが提供し、作成する他の複雑なデータ型にも適用される。 String については8章で詳しく説明する。
すでに文字列リテラルは紹介していて、その文字列の値はプログラム中に直接記述されていた。文字列リテラルは便利だがテキストを使用する全ての状況に適しているわけではない。その理由の1つは文字列リテラルがimmutableであることである。そしてもう1つはコードを記述している時に全ての文字列の値がわかっている訳ではないことである。例えば、ユーザから入力を受けて格納したい場合はどうするか?このような状況に対し、Rustは第2の文字列型 String を持っている。この型はヒープ領域に割り当てられ、コンパイル時にはわからない量のテキストを格納することができる。 String は from 関数を用いることで文字列リテラルから次のように作成することができる。
let s = String::from("hello");コロン2つ :: は string_from のような何らかの名前を用いるのではなく、この from 関数を String 型の名前空間にする演算子である。この構文については5章のメソッド構文の節の中で、モジュールにおける名前空間については7章で紹介する。
この種類の文字列は変更することができる。
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`ここでの違いは何か?なぜ String は変更することができてリテラルはできないのか?違いは2つの型のメモリの扱い方である。
文字列リテラルの場合、コンパイル時に内容がわかるのでテキストを最終実行可能ファイルに直接記述でき、高速かつ効率的なものにする。しかしこれらの特性はimmutabilityから来るものである。残念ながら、コンパイル時にサイズが不明でプログラム実行時にサイズ変更の恐れのある各テキストのバイナリに対してメモリを与えることはできない。
String 型では、mutableで拡張可能なテキストをサポートするため、コンパイル時には不明な量のヒープ領域内のメモリを内容保持のために割り当てる必要がある。このことは
- メモリは実行時にOSから要求される。
Stringに対する処理が終わった時にメモリをOSに返却する手段が必要である。
ということを意味する。
1つ目は、実装は String::from を呼んだ時に必要な量を要求するようにすることで解決した。これはプログラミング言語では普遍的な方法である。
ところが2つ目は異なる。GCを用いる言語では、GCはもう使用されないメモリを追跡して片付けるので、プログラマーとして考える必要はない。GCがない場合、メモリが使われなくなるのがいつかを特定し明示的に返却するコードを呼ぶのはプログラマーの責任である。これを正しく行うことは歴史的に難しいプログラミングの問題となった。もし忘れたらメモリを無駄遣いすることになる。もし返却が早すぎれば無効な変数を持つことになる。返却を2度行うとバグとなる。1つの割り当てに対し1つの解放をちょうど組にする必要がある。
Rustは別の方法をとった。メモリは所有する変数がスコープ外に出た時に自動的に返却される。リスト4-1で文字列リテラルの代わりに String を使用した場合のスコープ例を示す。
{
let s = String::from("hello"); // sはこの時点以降で有効
// sで何かする
} // スコープはここで終了するので、sはもう
// 有効ではないString が必要とするメモリをOSに返却する自然なポイントがある: s がスコープ外に出る時である。変数がスコープ外に出るとき、Rustは特殊な関数を呼ぶ。この関数を drop といい、 String の作成者がメモリ返却のコードを設置する場所である。Rustは } で自動的に drop を呼ぶ。
補足: C++では、アイテムのライフサイクルの最後でリソースを解放するこのパターンをしばしばResource Acquisition Is Initialization (RAII・「リソースの確保は初期化時に」「リソースの取得と初期化」)と呼ぶ。Rustの drop 関数はRAIIパターンを使用したことがある人にはお馴染みだろう。
このパターンはRustコードの実装方法に大きな影響を与える。今は単純に見えるかもしれないが、ヒープ領域内に割り当てたデータを複数の変数で使用したい場合、より複雑な状況下でのコードの振る舞いが予測できないことがある。
そのような状況をいくつか探っていこう。
Rustでは、同じデータに対して複数の変数が異なる方法でやり取りすることができる。リスト4-2は整数を用いた例である。
let x = 5;
let y = x;リスト4-2: 変数 x の整数値の y への代入
他の言語での経験から、これが何をしているのかおそらく予測できるだろう: "x に 5 を代入し、 x の値をコピーして y に代入する"。2つの変数 x y があり、どちらも 5 と等しい。整数は既知で固定のサイズの単純な値であり、2つの 5 はスタック領域にpushされるので、これは実際に行われていることである。
String 版を見てみよう:
let s1 = String::from("hello");
let s2 = s1;一見前のコードととても似ていて、同じ動作をすると考えるだろう。つまり、2行目では s1 の値をコピーして s2 に代入するだろう。しかしこれは全く行われない。
より徹底的に説明するために、図4-3で String がどのように見えるかを見てみる。左側にあるように、 String は文字列の内容を保持するメモリへのポインタ、長さ、容量の3つの部分で構成されている。このデータのグループはスタック領域に保存される。右側はヒープ領域内で内容を保持するメモリである。
これをより完全に説明するために、図4-3の文字列がどのように見えるかを見てみましょう。 文字列は、左に示すように、文字列の内容、長さ、および容量を保持するメモリへのポインタの3つの部分で構成されています。 このデータグループはスタックに格納されます。 右側には内容を保持するヒープ上のメモリがあります。
図4-3: s1 の "hello" を保持する String のメモリ表現
長さは String の内容が今使っているメモリ量で、バイト単位である。容量は String がOSから受け取ったメモリの総量で、バイト単位である。長さと容量の違いは重要だがここではそうではない。今のところは容量を無視しても問題ない。
s1 に s2 を代入すると String データがコピーされる、つまりスタック領域にあるポインタ、長さ、容量がコピーされる。ポインタが参照するヒープ領域のデータはコピーしない。つまり、メモリ内のデータ表現は図4-4のようになる。
s1にs2を代入すると、文字列データがコピーされます。つまり、スタック上にあるポインタ、長さ、および容量がコピーされます。 ポインタが参照するヒープ上のデータはコピーしません。 つまり、メモリ内のデータ表現は図4-4のようになります。
図4-4: s1 のポインタ、長さ、容量のコピーを持つ変数 s2 のメモリ表現
図4-5で示したRustがヒープ領域のデータもコピーした場合のメモリの様子と異なっている。もしRustがそのようにしてしまうと、 s2 = s1 の操作はヒープ領域のデータが大きかった場合に実行時のパフォーマンスが非常に悪くなる可能性がある。
図4-5: Rustがヒープ領域のデータもコピーした場合に考えられる s2 = s1 の操作の別の可能性
変数がスコープから外れるとRustは自動的に drop 関数を読んで変数のヒープメモリを片付けると前に述べた。しかし図4-4は両方のデータポインタが同じ場所を指していることを示している。これは問題である: s1 と s2 がスコープから外れたとき、両方とも同じメモリを解放しようとする。これはdouble free errorとして知られており、前に述べたメモリ安全のバグの1つである。メモリを2回解放するとメモリ破損を引き起こし、セキュリティ上の脆弱性を引き起こす可能性がある。
以前、変数がスコープから外れると、Rustは自動的にdrop関数を呼び出し、その変数のヒープメモリをクリーンアップします。 しかし、図4-4は、両方のデータポインタが同じ場所を指していることを示しています。 これは問題です.s2とs1が範囲外になると、両方とも同じメモリを解放しようとします。 これはダブルフリーエラーとして知られており、前述したメモリの安全性のバグの1つです。 メモリを2回解放するとメモリ破損につながり、セキュリティ上の脆弱性が発生する可能性があります。
メモリ安全を保証するために、Rustではこの状況で何が起きるかもう少し詳細がある。割り当てられたメモリをコピーしようとする代わりに、Rustは s1 をもはや有効ではないとみなすので、 s1 がスコープ外に出た時にRustは何も解放する必要がなくなる。 s2 を作成した後に s1 を使おうとした時にどうなるかを確認してみよう:
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);無効な参照を使用するのをRustが防ぎ、次のようなエラーが起きるだろう:
error[E0382]: use of moved value: `s1`
--> src/main.rs:4:27
|
3 | let s2 = s1;
| -- value moved here
4 | 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
他の言語を扱っている時に"shallow copy"や"deep copy"という単語を聞いたことがあれば、ポインタや長さ、容量をデータのコピーなしにコピーする考え方はshallow copyのように聞こえるだろう。しかしRustは更に最初の変数を無効にするので、shallow copyとは呼ばずにmoveと呼ぶ。ここでは s1 をs2 に移動したとして読むことができる。実際に何が起きるのかを図4-6に示す。
図4-6: s1 が無効になった後のメモリ表現
これが我々の問題を解決してくれる。 s2 のみが有効で、スコープから外れた時に s2 のみメモリが解放されて完了する。
それが私たちの問題を解決する! 有効なs2のみで、範囲外になると、それだけでメモリが解放され、完了です。
さらに、このことはRustが自動的にデータの deep copyを作成することはないというデザインの選択を示している。従って、全ての自動コピーは実行時パフォーマンスにおいて安価であると見なすことができる。
スタックデータ以外に String のヒープデータもdeep copyしたい場合は、cloneと呼ばれる一般的なメソッドを使用することができる。5章でメソッド公文について述べるが、メソッドは多くのプログラミング言語で共通の機能なので、これまでにも見たことがあるだろう。
以下は clone メソッドの実行の例である。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);これは正常に動作し、図4-5に示すようなヒープデータのコピーを明示的に作成する方法である。
clone が呼ばれているとき、いくつかの任意のコードが実行されてそのコードが高価かもしれないと知る。これは何か違うことが行われている視覚的な指標である。
まだ説明していない別の考えがある。前述のリスト4-2の一部である、整数を使用したこのコードは有効に機能する。
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);しかしこのコードは学んだことと矛盾しているように見える。 clone を呼び出す必要はないが、 x はまだ有効で y にmoveしなかった。
しかし、このコードは私たちがちょうど学んだことと矛盾するようです:クローンを呼び出す必要はありませんが、 x はまだ有効であり、 y に移動しませんでした。
その理由は、コンパイル時に既知のサイズを持つ整数のような型は全てスタック領域に格納されるため、実際の値のコピーは素早く作成できるからである。これは変数 y を作成した後で x が有効にならないようにする必要がないことを示す。言い換えると、ここではdeep copyとshallow copyの違いがないので、 clone の呼び出しは通常のshallow copyと違いがなく、忘れてもいいようになっている。
Rustには Copy traitと呼ばれる、整数のようなスタック領域に格納される値に設定できる特殊なアノテーションがある。(traitについては10章で述べる。)型が Copy traitを持つなら、古い方の変数は代入後も引き続き使用することができる。Rustは型やその一部分が Drop traitを実装しているときには Copy traitで型をアノテーションすることはできない。型がスコープ外になってその型に Copy アノテーションが与えられている時にその型が特別な何かが必要な場合、コンパイル時エラーが発生する。型に Copy アノテーションを与える方法についてはAppendix Cの Derivable traitを参照すること。
Copy とはどんな型か?確かに与えられた型のドキュメントを確認することはできるが、原則として、単純なスカラ値の任意のグループは Copy になることができ、割り当てを要したり何らかのリソースの形をしたもので Copy であるものはない。 Copy である型としては次のようなものがある。
- 全ての整数型、
u32など trueとfalseのブール型bool- 全ての浮動小数点型、
f64など Copyである型のみを含むタプル。(i32, i32)はCopyだが(i32, String)はCopyではない。
値を関数に渡すセマンティクスは、変数に値を代入するのと似ている。関数に変数を渡すと、割り当てと同じようにmoveまたはcopyが行われる。リスト4-7は、変数がスコープに出入りする場所を示すアノテーションを含む例である。
ファイル名: src/main.rs
fn main() {
let s = String::from("hello"); // sがスコープに入る。
takes_ownership(s); // sの値が関数にmoveし
// ... ここではもう有効ではない。
let x = 5; // xがスコープに入る。
makes_copy(x); // xが関数にmoveするが、
// i32は Copyなのでこの先でxを使っても大丈夫
} // ここでxはスコープ外に来る。sはmoveされたので特に何も起こらない。
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がスコープ外になるが特別なことは起こらない。リスト4-7: ownershipとスコープアノテーションと関数
takes_ownership を呼んだ後に s を使おうとすると、Rustはコンパイル時エラーを投げる。これらの静的解析は間違いから我々を守ってくれる。 main 関数に s や x を用いるコードを追加し、どこでそれらを使用できるか、ownershipの規則が使用を妨げる場所はどこかを見てみてほしい。
戻り値もownershipを渡すことができる。次の例はリスト4-7のものと似たアノテーションを用いている。
ファイル名: src/main.rs
fn main() {
let s1 = gives_ownership(); // gives_ownershipが `s1` に
// 戻り値をmoveする
let s2 = String::from("hello"); // s2がスコープに入る。
let s3 = takes_and_gives_back(s2); // s2がtakes_and_gives_backにmoveされる、
// さらに戻り値がs3にmoveする
} // ここでs3がスコープ外に来てdropされる。s2もスコープ外に出るが
// moveされているので何も起きない。s1はスコープ外に出てdropされる。
fn gives_ownership() -> String { // gives_ownershipは戻り値を
// 呼び出し元にmoveする。
let some_string = String::from("hello"); // some_stringがスコープに入る。
some_string // some_stringが返却されて、
// 呼び出し元にmoveされる。
}
// takes_and_gives_backはStringをとってStringを返す。
fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに
// 入る。
a_string // a_stringが返却されて呼び出し元にmoveされる。
}変数のownershipは毎回同じパターンに従う: 別の変数に代入されるとownershipが移動する。ヒープ領域内のデータを含む変数がスコープ外に出ると、別の変数にデータのownershipがmoveしていない限り drop によって片付けられる。
全ての関数でownershipを取得し返すのは少し面倒である。関数に値を使用させるがownershipをもたせたくない場合はどうすればいいか?関数本体から返された値だけでなく、関数に渡した値も再び使用したい場合はとても厄介である。
タプルを用いて複数の値を返却すれば可能である。
Filename: 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にはこの概念の機能があり、referenceと呼ばれている。
前の節の最後のタプルの問題点は、 String が calculate_length にmoveされたため、 calculate_length を呼んだ後でも String を使用できるように String を呼び出し元に返却する必要があることである。
以下は値のownershipを取得する代わりにパラメータとしてオブジェクトに対するreferenceを持った calculate_length 関数の定義と使用方法である。
ファイル名: src/main.rs
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}まず、変数宣言と関数の戻り値からタプルが全てなくなったことに注目してほしい。次に、 &s1 を calculate_length に渡し、その定義の中で String ではなく &String を取得していることに注意してほしい。
まず、変数宣言内のすべてのタプルコードと関数の戻り値がなくなったことに注目してください。 次に、&s1をcalculate_lengthに渡します。その定義では、Stringではなく&Stringを取ることに注意してください。
これらのアンパサンドはreferenceであり、ownershipの取得なしに値を参照できるようにするものである。図4-8はダイアグラムである。
図4-8: String s1 を指す &String s
ここで関数呼び出しを詳しく見てみる。
let s1 = String::from("hello");
let len = calculate_length(&s1);構文 &1 は所有せずに s1 の値へ参照するreferenceを作る。所有しないので、referenceがスコープ外に出ても指す値はdropされない。
同様に、関数のシグネチャはパラメータ s の型がreferenceであることを示すために & を使用する。説明的なアノテーションを追加しよう:
fn calculate_length(s: &String) -> usize { // sはStringへのreference
s.len()
} // ここでsがスコープ外に出るが、参照先のownershipを持っていないので
// 何も起こらない変数 s が有効となるスコープは任意の関数の引数のスコープと同じだが、所有権を持っていないのでスコープ外に出た時にreferenceが指すものをdropしない。実際の値ではなく引数としてreferenceを持つ関数は、ownershipを持たないので与えられたownershipを返すために値を返す必要はない。
関数の引数としてreferenceを持つことをborrowingと呼ぶ。現実の人生と同様に、人が何かを所有しているなら、あなたはその人からそれを借りることができる。あなたの用が済んだらそれを返さなければならない。
もしborrowingしたものを変更しようとすると何が起きるか?リスト4-9のコードを試してほしい。ネタバレ注意: 動かない!
ファイル名: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}リスト4-9: Listing 4-9: borrowingした値を変更してみる
次のようなエラーが出る
error: cannot borrow immutable borrowed content `*some_string` as mutable
--> error.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^
Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to.
リスト4-9のエラーは小さい微調整で修正できる。
Filename: src/main.rs
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}まず、 s を mut にする必要がある。そして &mut s でmutable referenceを作り、 some_string: &mut String でmutable referenceを受けなければならない。
しかしmutable referencesには大きな制限がある。特定スコープ内の特定のデータに対し、1つだけmutable referenceを持つことができる。次のコードは失敗する:
ファイル名: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;次のようなエラーが出る:
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> borrow_twice.rs:5:19
|
4 | let r1 = &mut s;
| - first mutable borrow occurs here
5 | let r2 = &mut s;
| ^ second mutable borrow occurs here
6 | }
| - first borrow ends here
この制限はmutationを許可するが、非常に制御された様式の元にある。ほとんどの言語がいつでも変更できるので、Rust初心者が苦労しているところである。この制限の利点はRustがコンパイル時にデータ競合を防げる点にある。
データ競合はこれら3つの振る舞いが起きる競合状態の種類の1つである。
- 2つ以上のポインタが同じデータに同時にアクセスする
- 少なくとも1つがそのデータの書き込みに使用されている
- データへのアクセスを同期させるためのメカニズムがない
データ競合は未定義の動作を引き起こす上、実行時に追跡しようとすると診断して修正するのが難しくなることがある。Rustはデータ競合時にコードをコンパイルしないので、この問題が起こるのを防いでくれる。
いつものように、中括弧で新しいスコープを作成し、同時に1つではなく複数のmutable referenceを可能にする:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1がここでスコープ外に出るが、問題なく新しいreferenceを作ることができる。
let r2 = &mut s;mutable referenceとimmutable referenceの同時使用に対する似たような規則がある。このコードはエラーになる:
let mut s = String::from("hello");
let r1 = &s; // 問題なしno problem
let r2 = &s; // 問題なしno problem
let r3 = &mut s; // 大いに問題あり次のようなエラーになる:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> borrow_thrice.rs:6:19
|
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 | }
| - immutable borrow ends here
すごい!immutable referenceがある間はmutable referenceを持つことはできない。immutable referenceの使用者は値が突然変更されることを予期しない。しかし、ただデータを読むだけで他者のデータ読み込みに影響を与えないので、複数のimmutable referenceは問題ない。
これらのエラーに時には不快に感じるかもしれないが、Rustコンパイラは潜在的なバグを早期に(実行時ではなくコンパイル時に)指摘し、そうすべきと考えたものとデータがしばしば異なる理由をあなたが見つけ出す代わりに問題がどこにあるかを正確に示すものである。
ポインタを扱える言語では、 danglingポインタ、つまり誰かに与えられた可能性のあるメモリ内を参照するポインタを、そのメモリを参照する他のポインタを維持しながらメモリを解放することで誤って作成することは簡単である。一方Rustではコンパイラがreferenceがdangling referenceでないことを保証する: あるデータに対するreferenceがあるなら、データへのreferenceがスコープ外に出る前にそのデータがスコープ外に出ることはないことをコンパイラは保証する。
dangling referenceを作ってみよう:
ファイル名: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}次のようなエラーになる。
error[E0106]: missing lifetime specifier
--> dangle.rs:5:16
|
5 | fn dangle() -> &String {
| ^^^^^^^
|
= help: this function's return type contains a borrowed value, but there is no
value for it to be borrowed from
= help: consider giving it a 'static lifetime
error: aborting due to 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 dangle() -> &String { // dangleがStringへのreferenceを返す
let s = String::from("hello"); // sは新しく作ったString
&s // Stringへのreferenceであるsを返す
} // ここでsがスコープ外に出てdropされ、メモリが解放される。
// 危険!s は dangle の中で作成されているので、 dangle のコードが終了した時に s は解放される。しかし s へのreferenceを返そうとした。これはこのreferenceが無効な String へのポインタとなることを意味している。これはよくない。Rustではこのようなことはできない。
String を直接返すのが解決策である。
fn no_dangle() -> String {
let s = String::from("hello");
s
}これは問題なく動く。ownershipがmoveされるので、何も解放されない。
referenceについて述べてきたことを要約しよう:
- いつでも、以下のどちらか一方だけの状態
- mutable referenceが1つだけ
- immutable referencesを好きな数だけ
- referenceはいつでも有効でなければならない
次は異なる種類のreferenceであるsliceを見ていく。
ownershipを持たないもう1つのデータ型がsliceである。sliceにより、コレクション全体ではなくコレクション内の要素の連続したシーケンスを参照することができる。
ここに小さなプログラミングの問題がある: 文字列を受け取って、その文字列中の最初の単語を返す関数を書け。もし関数がその文字列中にスペースを見つけられなかったら、文字列全体が1つの単語ということになるので文字列全体を返すこと。
関数のシグネチャを考えてみよう。
fn first_word(s: &String) -> ?この first_word 関数は &String をパラメータとして持つ。ownershipは不要なのでこれでよい。しかし何を返すべきなのか?実際に文字列の一部を伝える方法を持っていない。しかし、単語の終わりのインデックスを返すことはできる。リスト4-10に示す:
ファイル名: src/main.rs
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}リスト4-10: String パラメータのバイトインデックスの値を返す first_word 関数
Let’s break down this code a bit. Because we need to go through the String element by element and check whether a value is a space, we’ll convert our String to an array of bytes using the as_bytes method:
このコードを少し分解してみよう。 String 要素を要素単位で調べ、値がスペースかどうかを調べる必要があるため、 String を as_bytes メソッドでバイト配列に変換している。
let bytes = s.as_bytes();次に、 iter メソッドでバイト配列のイテレーターを作る。
for (i, &item) in bytes.iter().enumerate() {イテレータについては13章で詳しく説明する。 iter はコレクションの各要素を返すメソッドで、 enumerate は iter の結果をラップし代わりに各要素をタプルの一部として返すメソットである。戻り値のタプルの最初の要素はインデックスで、2番目の要素は要素へのreferenceである。これは自分でインデックスを計算するよりも少し便利である。
enumerate メソッドはタプルを返すので、Rustの他の箇所と同様に、パターンを使用してタプルを分解することができる。 for ループではタプル内のインデックスに i 、タプル内の単一バイトに &item を持つパターンを指定している。 .iter().enumerate() から要素へのreferenceを取得するので、パターン内で & を使用している。
バイトリテラル構文により、スペースを表すバイトを探す。スペースを見つけるとその位置を返す。あるいは、 s.len() を使用して文字列の長さを返す。
if item == b' ' {
return i;
}
}
s.len()今や文字列中の最初の単語の終わりのインデックスを発見する方法はあるが、問題がある。勝手に usize を返すが、これは &String のコンテキストの中でのみ意味のある数字である。言い換えれば、 String とは別の値なので、今後も有効であり続けるという保証はない。リスト4-10の first_word を用いるリスト4-11のプログラムを考えてみよう。
ファイル名: src/main.rs
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // wordは値5を受け取る。
s.clear(); // `String` を空にし、""と等しくする。
// wordはここでまだ5の値を持っているが、値5と共に意味を持って使える文字列はない。
// wordは完全に無効である!
}リスト4-11: first_word 関数の戻り値を格納し String の中身を変更する
このプログラムはエラーなしにコンパイルされる。 s.clear() を呼んだ後に word を使用した場合も同様である。 wordは s の状態と全く繋がっていないので、 word には値 5 が格納されたままである。変数 s に値 5 を用いて最初の単語を抽出しようとするが、 word に 5 を格納した後に s の中身を変更しているのでバグとなる。
word のインデックスが s のデータと同期しなくなることを気にするのは面倒で、エラーが起こりやすい。 second_word 関数を書いた場合、これらのインデックスの管理は更に脆弱になる。シグネチャは次のようになる:
fn second_word(s: &String) -> (usize, usize) {ここでは開始インデックスと終了インデックスを追跡していて、特定の状態下のデータから計算するがその状態と結びついていない値が増えている。ここでは同期させておく必要のある3つの関係ない変数が漂っている。
幸いにも、Rustにはこの問題を解決する方法がある: string sliceである。
string sliceはStringの一部へのreferenceで、次のようになる:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];String 全体へのreferenceを取るのと似ているが、余分に [0..5] がついている。これは String 全体へのreferenceではなく String の一部へのreferenceである。 start..end 構文は start から続いて end を含まない範囲である。
括弧つきの範囲で [starting_index..ending_index] とすることでsliceを作ることができる。ここで starting_index はsliceに含まれる最初の位置で、 ending_index はsliceに含まれる最後の位置よりも1大きいものである。内部的には、sliceのデータ構造は最初の位置とsliceの長さを格納していて、これは ending_index から starting_index を引いたものにあたる。 let world =&s [6..11];の場合、 word は s の6番目のバイトへのポインタと長さ 5 を含むsliceになる。
図4-12はダイアグラムで示したものである。
図4-12: String の一部分を参照する String slice
Rustの .. 範囲構文では、最初のインデックス(0)から始めたい時は、2つのピリオドの前の値を省略することができる。つまりこれらは等しい:
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];同じトークンにより、 String の最後のバイトをsliceに含ませるなら、末尾の数字を省略することができる。つまりこれらは等しい:
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];文字列全体を取得するのに両方の値を省略することができる。よってこれらは等しい:
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];これらすべての情報を覚えて、 first_word がsliceを返すように書き換えてみよう。string sliceを表す型は &str で表される:
ファイル名: src/main.rs
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}リスト4-10と同じ方法、最初のスペースを探すことで単語の終わりのインデックスを取得する。スペースを見つけたら、文字列の最初とスペースのインデックスをインデックスの開始と終了として用いてstring sliceを返す。
first_word を呼び出すと、元のデータに基づいた単一の値が返される。値はsliceの開始点へのreferenceとslice内の要素数で構成されている。
sliceの返却は second_word 関数でも働く。
fn second_word(s: &String) -> &str {コンパイラは String へのreferenceが有効であることを保証するので、混乱しづらい簡単なAPIがある。リスト4-11のプログラムのバグを思い出して欲しい、最初の単語の最後のインデックスを取ったあと、インデックスを無効にするために文字列を片付けた場合はどうか?あのコードは論理的に間違っていたがすぐにエラーは出なかった。もし空文字列に対して最初の単語のインデックスを使おうとすると後になって問題が出てくる。slice版の first_word を用いるとコンパイル時にエラーが出る。
ファイル名: src/main.rs
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // エラー!
}これがコンパイルエラーである:
17:6 error: cannot borrow `s` as mutable because it is also borrowed as
immutable [E0502]
s.clear(); // Error!
^
15:29 note: previous borrow of `s` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `s` until the borrow ends
let word = first_word(&s);
^
18:2 note: previous borrow ends here
fn main() {
}
^
immutable referenceがあるとmutable referenceを取得できないというborrowの規則を思い出して欲しい。 clear は String を切り捨てる必要があるのでmutable referenceを取得しようとするが失敗する。RustはAPIを使いやすくしただけでなく、コンパイル時にエラーの全種類をなくすようにしている。
バイナリの中に格納される文字列リテラルについて述べた。sliceについて学習したので、文字列リテラルについて正しく理解できる。
let s = "Hello, world!";s の型は &str であり、これはバイナリの特定位置を指すsliceである。そしてこれは文字列リテラルがimmutableである理由でもある。 &str はimmutable referenceである。
リテラルや String のsliceを取れることから、 first_word を更に改善することができる。
fn first_word(s: &String) -> &str {String と &str に対して同じ関数を使えることから、Rustの経験者なら次のように書き換えられるだろう。
fn first_word(s: &str) -> &str {もしstring sliceを持っているなら、直接代入することができる。もし String を持っているなら、全 String のsliceを代入することができる。 String へのreferenceの代わりにstring sliceをとる関数を定義することで、機能性を失うことなくAPIはより一般化され使いやすくなる。
ファイル名: src/main.rs
fn main() {
let my_string = String::from("hello world");
// first_wordが `String` のsliceに対して働く
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_wordが文字列リテラルのsliceに対して働く
let word = first_word(&my_string_literal[..]);
// 文字列リテラルは既にstring sliceなので、
// slice構文なしに働く
let word = first_word(my_string_literal);
}想像通り、string sliceは文字列に特有のものである。しかし、より一般的なslice型もある。この配列を考えてみよう:
let a = [1, 2, 3, 4, 5];文字列の一部を参照したいのと同様、次のようにして配列の一部を参照したいと思うかもしれない。
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];このsliceは &[i32] 型である。これはstring 最初の要素へのreferenceと長さを格納することでstring sliceと同じように働く。この種のsliceはあらゆる種類のコレクションにも使うことができる。コレクションについては8章のベクトルについて述べた時に紹介する。
ownership、borrow、sliceの概念はコンパイル時にRustプログラムのメモリ安全を保証するものである。Rustでは他のシステムプログラミング言語のようにメモリの使用を制御できるが、所有者がスコープ外に出たときにデータの所有者がデータを自動的に削除するということは、制御するために追加でコードを書いてデバッグする必要がないということである。
ownershipはRustの他の多くの部分がどう働くかに影響するので、これらの概念についてはこの本の残りで詳しく説明する。次の章では構造体の中でのデータをグループ化に着目する。
| ## Ownership of Struct Data | |
| リスト5-1の構造体 `User` の定義の中では、string slice型の `&str` ではなく所有権ありの `String` 型を使っていた。この構造体のインスタンスが全データを所有し、構造体全体が有効である限りデータが有効であるようにするための、意図的な選択である。 | |
| 他が所有するデータへの参照を構造体に格納することは可能だが、そうするためには10章で説明する機能であるlifetimeの使用が必要である。lifetimeは構造体が参照するデータが構造体が有効である限り有効であることを保証する。lifetimeを指定せずに構造体の中でreferenceを格納しようとしてみよう。 | |
| ファイル名: src/main.rs | |
| ```rust | |
| struct User { | |
| username: &str, | |
| email: &str, | |
| sign_in_count: u64, | |
| active: bool, | |
| } | |
| fn main() { | |
| let user1 = User { | |
| email: "[email protected]", | |
| username: "someusername123", | |
| active: true, | |
| sign_in_count: 1, | |
| }; | |
| } | |
| ``` | |
| コンパイラはlifetimeの指定が必要であるとする。 | |
| ``` | |
| error[E0106]: missing lifetime specifier | |
| --> | |
| | | |
| 2 | username: &str, | |
| | ^ expected lifetime parameter | |
| error[E0106]: missing lifetime specifier | |
| --> | |
| | | |
| 3 | email: &str, | |
| | ^ expected lifetime parameter | |
| ``` | |
| 10章ではこのエラーを修正して構造体内にreferenceを格納できるようにする方法を述べるが、今は `&str` のようなreferenceの代わりに `String` のような所有された型の使用でエラーを修正する。 |
| # lifetimeによるreferenceの検証 | |
| 4章でreferenceについて述べたとき、重要な詳細を残していた: Rustの各referenceはlifetimeを持っていて、これはreferenceが有効なスコープである。たいていの場合で型が推論されるように、たいていの場合lifetimeも暗黙的で推論される。多数の方が可能なので型にアノテーションする必要がある場合と同様に、referenceのlifetimeがいくつかの異なる方法で関連する場合があるので、実行時に使用する実際のreferenceが確実に有効であることを保証できるように、Rustでは一般的なlifetimeパラメータを使って関係性をアノテーションする必要がある。 | |
| これは少し珍しいもので、他のプログラミング言語で使用してきたツールとは異なるものだろう。ある点では、lifetimeはRustの独特な特徴である。 | |
| lifetimeはこの章ですべて網羅できないほどに大きいトピックであり、概念に慣れるためにこの章ではlifetimeの構文と出会う可能性のある一般的な手法を説明する。19章でlifetimeができる全てのものに関する高度な情報を説明する。 | |
| ## dangling referenceを防ぐlifetime | |
| lifetimeの主な目的はdangling referenceを防ぐことで、これは参照したいデータと異なるデータをプログラムが参照してしまう原因である。外部スコープと内部スコープを含むリスト10-16のプログラムを見てみよう。外部スコープは `r` という名前を初期値なしで宣言し、内部のスコープは `x` という名前の変数を初期値 `5` で宣言する。内部スコープの中では `x` へのreferenceを `r` の値に設定しようとしている。そして内部スコープが終了し、 `r` の値を出力しようとする。 | |
| ```rust | |
| { | |
| let r; | |
| { | |
| let x = 5; | |
| r = &x; | |
| } | |
| println!("r: {}", r); | |
| } | |
| ``` | |
| リスト10-16: 値がスコープ外に出るreferenceを使おうとする | |
| ### Uninitialized Variables Cannot Be Used | |
| 次の例は初期値なしで変数を宣言していて、変数名が外部スコープに存在している。これはRustがnullを持たないことと矛盾するように見える。しかし、変数が初期化される前に使おうとするとコンパイル時エラーになる。やってみて! | |
| コンパイルすると次のようなエラーが出る。 | |
| ``` | |
| error: `x` does not live long enough | |
| | | |
| 6 | r = &x; | |
| | - borrow occurs here | |
| 7 | } | |
| | ^ `x` dropped here while still borrowed | |
| ... | |
| 10 | } | |
| | - borrowed value needs to live until here | |
| ``` | |
| 変数 `x` は"live long enough."ではない。なぜ違うのか?7行目の閉じ中括弧を入力した時に内部スコープが終わり、 `x` はスコープから外れる。 | |
| しかし `r` は外部スコープで有効である。このスコープはより長いもので、"lives longer."と呼ぶ。このコードが働くのをRustが許可したとすると、 `x` がスコープから外れた時に解放されるメモリを `r` が参照してしまい、 `r` を使ってやろうとしていたことが全て正しく動作しなくなる。 | |
| では、Rustはどのようにしてこのコードを許可しないと判断しているのか? | |
| ### borrowチェッカー | |
| borrowチェッカーと呼ばれるコンパイラの部分は、全borrowが有効かどうか判断するためにスコープを比較する。リスト10-17は10-16と同じ例で、変数のlifetimeを示すアノテーションを加えてある。 | |
| ```rust | |
| { | |
| let r; // -------+-- 'a | |
| // | | |
| { // | | |
| let x = 5; // -+-----+-- 'b | |
| r = &x; // | | | |
| } // -+ | | |
| // | | |
| println!("r: {}", r); // | | |
| // | | |
| // -------+ | |
| } | |
| ``` | |
| リスト10-17: それぞれ `'a` , `'b` と名付けられた `r` と `x` のlifetimeのアノテーション | |
| `r` のlifetimeに `'a` , `x` のlifetimeに `'b` アノテーションした。見ての通り、内部スコープ `'b` のブロックは外部スコープ `'a` のブロックより短い。コンパイル時にRustは2つのlifetimeのサイズを比較し、 `r` が `'a` のlifetimeを持っていることを見るが、これは `'b` のlifetimeをもつオブジェクトを参照している。lifetime `'b` はlifetime `'a` より短いのでこのプログラムはリジェクトされる。referenceの対象はreferenceほど長くは存続しない。 | |
| dangling referenceを使用せずエラーの出ないリスト10-18の例を見てみよう。 | |
| ```rust | |
| { | |
| let x = 5; // -----+-- 'b | |
| // | | |
| let r = &x; // --+--+-- 'a | |
| // | | | |
| println!("r: {}", r); // | | | |
| // --+ | | |
| } // -----+ | |
| ``` | |
| リスト10-18: データがreferenceよりも長いlifetimeを持つので有効なreference | |
| ここで、 `x` はlifetime `'b` を持っていて、今回は `'a` よりも長い。これは `r` が `x` を参照できることを意味する。Rustは `r` の中のreferenceが `x` が有効である限り常に有効であることを知っている。 | |
| 具体的な例を用いてreferenceのlifetimeがどこにあるかを示し、referenceが常に有効であることを保証するためにどのようにRustがlifetimeを解析しているのかを議論したので、関数のコンテキストにおけるパラメータと戻り値の一般的なlifetimeについて述べていく。 | |
| ## 関数における一般的なlifetime | |
| 2つのstring sliceのうち長い方を返す関数を記述してみよう。2つのstring sliceを渡すことで関数を呼び、string sliceが返るようにしたい。リスト10-19のコードは `longest` 関数を実装すると `The longest string is abcd` を表示する。 | |
| ファイル名: src/main.rs | |
| ```rust | |
| fn main() { | |
| let string1 = String::from("abcd"); | |
| let string2 = "xyz"; | |
| let result = longest(string1.as_str(), string2); | |
| println!("The longest string is {}", result); | |
| } | |
| ``` | |
| リスト10-19: 2つのstring sliceのうち長い方を得る `longest` 関数を呼ぶ `main` 関数 | |
| `longest` 関数に引数のownershipを取られたくないので、この関数にはstring slice(4章で説明したreference)を受け取ってほしいことに注意する。関数には文字列リテラル(変数 `string2` が持っているもの)と同様に `String` のsliceも受理するようにしてほしい。 | |
| これらが我々の求める引数である理由についての議論については4章の"String Slices as Arguments"節に戻ってほしい。 | |
| リスト10-20のように `longest` 関数を実装しようとするとコンパイルすることができない。 | |
| メソッド名: src/main.rs | |
| ```rust | |
| fn longest(x: &str, y: &str) -> &str { | |
| if x.len() > y.len() { | |
| x | |
| } else { | |
| y | |
| } | |
| } | |
| ``` | |
| リスト10-20: 2つのstring sliceのうち長い方を返す `longest` 関数の実装だがこのままではコンパイルできない | |
| 代わりにlifetimeに関するエラーが得られる: | |
| ``` | |
| error[E0106]: missing lifetime specifier | |
| | | |
| 1 | fn longest(x: &str, y: &str) -> &str { | |
| | ^ expected lifetime parameter | |
| | | |
| = help: this function's return type contains a borrowed value, but the | |
| signature does not say whether it is borrowed from `x` or `y` | |
| ``` | |
| 返却されるreferenceが `x` と `y` のどちらを参照しているのかRustはわからないので、ヘルプテキストは戻り値の型がgeneric lifetime parameterを必要していることを教えてくれている。 | |
| 実際には、この関数の本体の中の `if` ブロックが `x` へのreferenceを返し `else` ブロックが `y` へのreferenceを返すので我々もわからない。 | |
| この関数を実装した結果、この関数に渡される具体的な値がわからないので、`if` と `case` のどちらが実行されるのかわからない。 また、渡されるreferenceの具体的なlifetimeもわからないので、返すreferenceが常に有効であることを決定するためにリスト10-17や10-18のようにスコープに注目することができない。 `x` と `y` のlifetimeが戻り値のlifetimeにどのように関わるのか知らないので、borrowチェッカーはこれを判別できない。borrowチェッカーが解析できるようにreference間の関係を定義するgeneric lifetime parameterを与えていく。 | |
| ## lifetimeアノテーション構文 | |
| lifetimeアノテーションはreferenceのいずれかがどのくらい長く存続に影響したかを変えない。シグネチャが総称型パラメータを指定する時に関数が任意の型を受け入れるのと同じように、シグネチャがgeneric lifetime parameterを指定するとき関数は任意のlifetimeのreferenceを受け入れる。lifetimeアノテーションが何をするかは相互に参照する複数のreferenceのlifetimeに関連している。 | |
| lifetimeアノテーションには少し珍しい構文がある: lifetimeパラメータの名前はアポストロフィ `'` で始めなければならない。lifetimeパラメータの名前は通常全てlowercaseで、総称型と同様に、その名前は非常に短い。 `'a` はほとんどの人がデフォルトとして使う名前である。lifetimeパラメータアノテーションはreferenceの `&` の後に来て、スペースを用いてreferenceの型からlifetimeアノテーションを分ける。 | |
| ここにいくつか例がある: lifetimeパラメータなしの `i32` へのreference、`'a` という名前のlifetime parameterを持つ `i32` へのreference、そして同じくlifetime `'a` を持つ `i32` へのmutable referenceである。 | |
| ```rust | |
| &i32 // reference | |
| &'a i32 // 明示的にlifetimeを持つreference | |
| &'a mut i32 // 明示的にlifetimeを持つmutable reference | |
| ``` | |
| 1つのlifetimeアノテーションだけではあまり意味がない: lifetimeアノテーションは複数のreferenceのgeneric lifetime parameterがどのようにお互いに参照しあっているかをRustに伝えるものである。lifetime `'a` の `i32` へのreferenceが最初の、lifetime `'a` の `i32` への別のreferenceが2番目のパラメータの関数があるとき、同じ名前のこれら2つのlifetimeアノテーションは1つ目と2つ目のreferenceが同じgeneric lifetimeの間存続し続ける必要があることを示している。 | |
| ## 関数シグネチャ中のlifetimeアノテーション | |
| 取り組んでいる `longest` 関数のコンテキスト中のlifetimeアノテーションに注目しよう。総称型パラメータのように、generic lifetime parametersは関数名とパラメータのリストの間に山括弧を用いて宣言する必要がある。パラメータ中のreferenceと戻り値に対してRustに伝えたい制約はそれら全てが同じlifetimeを持つことであり、リスト10-21のように `'a` と名付けて各referenceに追加する。 | |
| ファイル名: src/main.rs | |
| ```rust | |
| fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | |
| if x.len() > y.len() { | |
| x | |
| } else { | |
| y | |
| } | |
| } | |
| ``` | |
| リスト10-21: シグネチャ中の全referenceが `'a` という同じlifetimeを持つようにした`longest` 関数の定義 | |
| リスト10-19の `main` 関数とともに使うとコンパイルされて結果が得られる。 | |
| この関数のシグネチャは、lifetime `'a` に対し、関数がパラメータを2つ受け取って、それらstring sliceの両方が少なくともlifetime `'a` と同じ長さだけ存続するとしている。そしてlifetime `'a` と少なくとも同じだけ続くstring sliceを返す。これは戻り値に強制したいことをRustに伝える約定である。 | |
| 関数シグネチャでlifetime parameterを指定することで、引数や戻り値のlifetimeが変わることはないが、この約定に沿わない値は全てborrowチェッカーによりリジェクトされるべきである。 | |
| この関数は `x` と `y` がどのくらいの期間存続するかを正確に知らないし知る必要もないが、このシグネチャを満たす `a` の代わりに使用できるスコープがあることだけは知る必要がある。 | |
| 関数のlifetimeにアノテーションをつけるとき、アノテーションは関数のシグネチャに入るが関数本体のコードの中には入らない。これはRustが助けなしに関数内のコードを解析できるからだが、関数が関数の外のコードとの間にreferenceを持つ場合は、関数が呼ばれるたびに引数や戻り値のlifetimeが異なる可能性がある。これは非常にコストが高くRustが理解するのは不可能である。この場合は我々自身がlifetimeにアノテーションをつけなければならない。 | |
| 具体的なreferenceが `longest` に渡されるとき、 `'a` と置き換えられる具体的なlifetimeは `y` のスコープと重なる `x` のスコープの一部である。スコープは常に入れ子になっているので、言い換えれば、generic lifetime `'a` はlifetime `x` と `y` のうち短い方と等しい具体的なlifetimeを得るということになる。同じlifetimeパラメータ `'a` を戻り値のreferenceにアノテーションしたので、戻り値のreferenceはlifetime `x` と `y` のうち短い方とおなじだけ有効であることが保証される。 | |
| 異なる具体的なlifetimeを持つreferenceを渡し、 `longest` 関数の使用をどのように制限するか見てみよう。リスト10-22はどの言語からの直感にも合う簡単な例である: `string1` は外部スコープの終わりまで有効で、 `string2` は内部スコープの終わりまで有効で、 `result` は何かを参照していてこれは内部スコープの終わりまで有効である。borrowチェッカーはこのコードを受理する; これはコンパイルされて、実行すると `The longest string is long string is long` と出力する: | |
| ファイル名: src/main.rs | |
| ```rust | |
| fn main() { | |
| let string1 = String::from("long string is long"); | |
| { | |
| let string2 = String::from("xyz"); | |
| let result = longest(string1.as_str(), string2.as_str()); | |
| println!("The longest string is {}", result); | |
| } | |
| } | |
| ``` | |
| リスト10-22: 異なる具体的なlifetimeを持つ `String` の値へのreferenceとともに`longest` 関数を使う | |
| 次に、結果のreferenceのlifetimeが必ず2つの引数のうち短い方のlifetimeになることを示す例を見てみよう。外部スコープの外に変数 `result` の宣言を移動させるが、 `result` の変数への値の代入は `string2` のスコープ内に残す。次に、 `result` を利用する `println!` を、終わった後の内部スコープの外に移動する。 リスト10-23のコードはコンパイルされない: | |
| ファイル名: src/main.rs | |
| ```rust | |
| fn main() { | |
| let string1 = String::from("long string is long"); | |
| let result; | |
| { | |
| let string2 = String::from("xyz"); | |
| result = longest(string1.as_str(), string2.as_str()); | |
| } | |
| println!("The longest string is {}", result); | |
| } | |
| ``` | |
| リスト10-23: `string2` がスコープ外に出た後に `result` を使おうとするのはコンパイルされない | |
| コンパイルしようとすると次のようなエラーになる: | |
| ``` | |
| error: `string2` does not live long enough | |
| | | |
| 6 | result = longest(string1.as_str(), string2.as_str()); | |
| | ------- borrow occurs here | |
| 7 | } | |
| | ^ `string2` dropped here while still borrowed | |
| 8 | println!("The longest string is {}", result); | |
| 9 | } | |
| | - borrowed value needs to live until here | |
| ``` | |
| `result`が `println!` に対して有効であるためには `string2` が外部スコープの終了まで有効でなければならないとエラーは述べている。関数の引数のlifetimeをアノテーションし同じlifetimeパラメータ `'a` とともに値を返したのでRustはこれを知っている。 | |
| このコードを人間の目で見てみると、 `string1` の方が長く、その結果 `string1` へのreferenceが `result` に格納される。 `string1` はまだスコープ外に出ていないので、 `string1` へのreferenceは `println!` に対してまだ有効である。しかし、lifetimeパラメータでRustに伝えたのは、 `longest` 関数が返すreferenceのlifetimeが、渡されてくるreferenceのlifetimeのうち短い方と同じであるということである。したがって、無効なreferenceがある可能性があるとしてborrowチェッカーはリスト10-23のコードを許可しない。 | |
| `longest` 関数に渡されるreferenceの値とlifetimeや返されるreferenceの使い方を変える実験を設計してみてほしい。どの実験がborrowチェッカーを通過するかコンパイルする前に仮説を立てて、それが正しいかどうか確認してほしい。 | |
| ## lifetimeに関して考える | |
| lifetimeパラメータを指定する正確な方法は関数が何をするかに依存する。たとえば、 `longest` 関数が長い方のstring sliceではなく常に最初の引数を返すように実装を変えた場合、 `y` パラメータのlifetimeを指定する必要はない。このコードをコンパイルすると: | |
| ファイル名: src/main.rs | |
| ```rust | |
| fn longest<'a>(x: &'a str, y: &str) -> &'a str { | |
| x | |
| } | |
| ``` | |
| この例ではパラメータ `x` と戻り値の型にlifetimeパラメータ `'a` を指定しているが、 `y` のlifetimeは `x` や戻り値のlifetimeとは何の関係もないのでパラメータ `y` には設定していない。 | |
| 関数からreferenceを返すとき、戻り値の型のlifetimeパラメータは引数のうちの1つのlifetimeパラメータと一致する必要がある。返されるreferenceがどの引数も参照しない場合、唯一の可能性はそれが関数の中で作られた値を参照することで、値は関数の終わりでスコープから外れるのでこれはdangling referenceになる可能性がある。コンパイルされない `longest` 関数の試しの実装を考えてみよう: | |
| ファイル名: src/main.rs | |
| ```rust | |
| fn longest<'a>(x: &str, y: &str) -> &'a str { | |
| let result = String::from("really long string"); | |
| result.as_str() | |
| } | |
| ``` | |
| 戻り値の型にlifetimeパラメータ `'a` を指定したにもかかわらず、戻り値のlifetimeが引数のlifetimeと全く関係しないのでこの実装はコンパイルに失敗する。次のようなエラーが表示される: | |
| ``` | |
| error: `result` does not live long enough | |
| | | |
| 3 | result.as_str() | |
| | ^^^^^^ does not live long enough | |
| 4 | } | |
| | - borrowed value only lives until here | |
| | | |
| note: borrowed value must be valid for the lifetime 'a as defined on the block | |
| at 1:44... | |
| | | |
| 1 | fn longest<'a>(x: &str, y: &str) -> &'a str { | |
| | ^ | |
| ``` | |
| 問題は、 `result` がスコープ外になって `longest` 関数の最後に片付けられることであり、我々は関数から得た `result` へのreferenceを返そうとしている。dangling referenceを変更するlifetimeパラメータを指定する方法はないし、Rustはdangling referenceの作成を許可しない。この場合、referenceではなく所有されたデータ型を返すのが最も良い解決策であり、呼び出し側の関数がその値を片付ける責任を持つ。 | |
| 結局、lifetime構文とは様々な引数のlifetimeと関数の戻り値を結びつけることである。これらが結びつくと、Rustはメモリ安全な操作を許可しdanglingポインタやその他のメモリ安全に反する操作を禁止するための十分な情報を得る。 | |
| ## 構造体定義におけるlifetimeアノテーション | |
| 今までは所有する型を保持する構造体のみを定義していた。referenceを保持する構造体を定義することはできるが、構造体の中の全てのreferenceにlifetimeアノテーションを追加しなければならない。リスト10-24はstring sliceを持つ `ImportantExcerpt` という名の構造体である。 | |
| ファイル名: src/main.rs | |
| ```rust | |
| struct ImportantExcerpt<'a> { | |
| part: &'a str, | |
| } | |
| fn main() { | |
| let novel = String::from("Call me Ishmael. Some years ago..."); | |
| let first_sentence = novel.split('.') | |
| .next() | |
| .expect("Could not find a '.'"); | |
| let i = ImportantExcerpt { part: first_sentence }; | |
| } | |
| ``` | |
| リスト10-24: referenceを持つ構造体なので、定義にはlifeアノテーションが必要 | |
| この構造体にはフィールド `part` があり、referenceであるstring sliceを保持している。総称型のデータ型と同様、構造体の名前の後ろに山括弧で囲んでgeneric lifetimeパラメータの名前を宣言し、構造体定義の本体でlifetimeパラメータを使えるようにする必要がある。 | |
| `main` 関数は、変数 `novel` が保持する `String` の最初の文へのreferenceを保持する、 `ImportantExcerpt` 構造体のインスタンスを作成する。 | |
| ## lifetimeの省略 | |
| この章では全referenceにlifetimeがあり、referenceを使う関数や構造体に対しlifetimeパラメータを指定する必要があることを学んできた。しかし4章のString Slices節では、リスト10-25で示すように、lifetimeアノテーションなしでコンパイルされる関数を扱った。 | |
| ファイル名: src/lib.rs | |
| ```rust | |
| fn first_word(s: &str) -> &str { | |
| let bytes = s.as_bytes(); | |
| for (i, &item) in bytes.iter().enumerate() { | |
| if item == b' ' { | |
| return &s[0..i]; | |
| } | |
| } | |
| &s[..] | |
| } | |
| ``` | |
| リスト10-25: 4章で定義した関数は引数や戻り値がreferenceであるにもかかわらずlifetimeアノテーションなしでコンパイルされた | |
| この関数がlifetimeアノテーションなしでコンパイルされる理由は歴史的なものである。1.0より前の初期のバージョンのRustではコンパイルされなかった。全てのreferenceは明示的なlifetimeが必要だった。当時は関数シグネチャは次のように書かれていた。 | |
| ```rust | |
| fn first_word<'a>(s: &'a str) -> &'a str { | |
| ``` | |
| たくさんのRustのコードが書かれた後、RustチームはRustプログラマーが特定の状況で何度も同じlifetimeアノテーションを書いていることを発見した。これらの状況は予測可能でいくつかの決定論的パターンに従っていた。そこでRustチームは、borrowチェッカーがこのような状況の時に、プログラマーがアノテーションを明示的に追加しなくてもlifetimeを推論できるようパターンをコンパイラのコードに追加した。 | |
| より決定的なパターンが出現しコンパイラに追加される可能性があるので、Rustの歴史について言及した。将来的には必要となるlifetimeアノテーションがさらに減る可能性もある。 | |
| Rustのreferenceの解析にプログラムされたパターンはlifetime elision rulesと呼ばれる。 | |
| これはプログラマーが従うべき規則ではない。規則はコンパイラが検討する特定のケースの集合であり、もしコードがこれらのケースに適合するなら明示的にlifetimeを書く必要はない。 | |
| elision rulesは完全な推論を提供するものではない。Rustが決定論的にルールを適用してもreferenceのlifetimeに曖昧さが残っている場合、残りのreferenceのlifetimeを推測することはできない。この場合コンパイラは、referenceがお互いにどのように参照し合っているかについてのあなたの意図に対応するlifetimeアノテーションを追加することで解決するエラーを示す。 | |
| まず用語を定義しよう: 関数やメソッドの引数のlifetimeをinput lifetimeと呼び、戻り値のlifetimeをoutput lifetimeと呼ぶ。 | |
| 明確なアノテーションがないとき、referenceがどんなlifetimeを持つかを把握するためにコンパイラが使用する規則について説明する。最初の規則はinput lifetimeに適用され、残りの2つの規則はoutput lifetimeに適用される。コンパイラが3つの規則の最後に到達し、なおlifetimeがわからないreferenceがあるとき、コンパイラはエラーとともに停止する。 | |
| 1. referenceである各パラメータは独自のlifetimeパラメータを1つ持つ。言い換えれば、引数が1つの関数は1つのlifetimeパラメータを持ち( `fn foo<'a>(x: &'a i32)` )、2つの引数を持つ関数は異なる2つのlifetimeパラメータを持つ( `fn foo<'a, 'b>(x: &'a i32, y: &'b i32)` )など。 | |
| 2. ちょうど1つのinput lifetimeパラメータを持つとき、そのlifetimeは全てのoutput lifetimeパラメータに割り当てられる:` fn foo<'a>(x: &'a i32) -> &'a i32` | |
| 3. 複数のinput lifetimeパラメータがあるもののメソッドなのでそのうちの1つが `&self` や `&mut self` のとき、 `self` のlifetimeは全てのoutput lifetimeパラメータに割り当てられる。これによりメソッドをよりよく実装できる。 | |
| コンパイラになりきって、リスト10-25の `first_word` 関数のシグネチャの中のreferenceのlifetimeを調べるためにこれらの規則を適用してみよう。シグネチャにはreferenceに対するlifetimeのアノテーションがない。 | |
| ```rust | |
| fn first_word(s: &str) -> &str { | |
| ``` | |
| コンパイラになりきった我々は最初の規則である「各パラメータは独自のlifetimeを1つ持つ」を適用する。我々はいつも通りそのlifetimeを `'a` と呼ぶので、シグネチャは次のようになる: | |
| ```rust | |
| fn first_word<'a>(s: &'a str) -> &str { | |
| ``` | |
| ちょうど1つinput lifetimeがあるので、2番目の規則を適用する。2番目の規則ではそのinputパラメータのlifetimeをoutput lifetimeに適用するとしているので、シグネチャは次のようになる: | |
| ```rust | |
| fn first_word<'a>(s: &'a str) -> &'a str { | |
| ``` | |
| 今この関数シグネチャの全referenceにはlifetimeがあり、関数シグネチャ内にプログラマがlifetimeをアノテーションする必要なしに、コンパイラは分析を続けることができる。 | |
| 別の例を見てみよう。今度はリスト10-20で扱った、lifetimeパラメータを持たない `longest` 関数である。 | |
| ```rust | |
| fn longest(x: &str, y: &str) -> &str { | |
| ``` | |
| 再びコンパイラになりきって最初の規則を適用してみよう。各パラメータはそれぞれlifetimeを持つ。今回は2つパラメータがあるので2つのlifetimeがある。 | |
| ```rust | |
| fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { | |
| ``` | |
| 2つ目の規則を見てみると、input lifetimeが複数あるので適用されない。3つ目の規則を見ると、これはメソッドではなく関数なので適用されず、どのパラメータも `self` ではない。よって我々は規則から外れてしまうが、戻り値の型のlifetimeが何かわからないままである。これがリスト10-20のコードがコンパイルするとエラーになる理由である。コンパイラはlifetime elision rulesを適用したが、シグネチャ内のreferenceのすべてのlifetimeを把握することはできていない。 | |
| 3番目の規則はメソッドのシグネチャのみに適用されるので、今回の状況でlifetimeを見て、3番目の規則がメソッドシグネチャの時にlifetimeをアノテーションする必要がないことを指す理由を確認してほしい。 | |
| ## メソッド定義におけるlifetimeのアノテーション | |
| lifetimeを持つ構造体にメソッドを実装する場合、構文はリスト10-10で示したgeneric typeパラメータと同じである。lifetimeパラメータが宣言され使われる場所は、lifetimeパラメータが構造体のフィールドあるいはメソッド引数や戻り値どちらに関係しているかに依存している。 | |
| 構造体のフィールドに対するlifetimeの名前は常に `impl` キーワードの後ろで宣言し、さらにこのlifetimeは構造体の型の一部なので構造体の名前の後ろで使う必要がある。 | |
| `impl` ブロック中のメソッドシグネチャでは、referenceは構造体のフィールドのreferenceのlifetimeに結び付けられているか、あるいは独立している。さらに、lifetime elision ruleによりlifetimeアノテーションがメソッドシグネチャで不要になることがよくある。リスト10-24に定義する構造体 `ImportantExcerpt` を使用するいくつかの例を見てみよう。 | |
| まず、 `level` という名前のメソッドがある。唯一のパラメータは `self` へのreferenceで、その戻り値はただの `i32` であり、どこかへのreferenceではない。 | |
| ```rust | |
| impl<'a> ImportantExcerpt<'a> { | |
| fn level(&self) -> i32 { | |
| 3 | |
| } | |
| } | |
| ``` | |
| `impl` の後のlifetimeパラメータ宣言と型名の後のlifetimeパラメータ使用は必須だが、elision ruleの1番目により `self` へのreferenceのlifetimeのアノテーションは必要ない。 | |
| ここに3番目のlifetime elision ruleの適用の例がある: | |
| ```rust | |
| impl<'a> ImportantExcerpt<'a> { | |
| fn announce_and_return_part(&self, announcement: &str) -> &str { | |
| println!("Attention please: {}", announcement); | |
| self.part | |
| } | |
| } | |
| ``` | |
| これにはinput lifetimeが2つあるので、Rustは1つめのlifetime elision ruleを適用し、 `&self` と `announcement` 両方に独自のlifetimeを与える。そして、引数のうちの1つが `&self` なので、戻り値の型は `&self` のlifetimeを得、全lifetimeが構成される。 | |
| ## 静的lifetime | |
| 議論する必要のある特別なlifetime、 `'static` がある。 `'static` lifetimeはプログラムの全期間である。全文字列リテラルは `'static` lifetimeを持ち、次のようにアノテーションできる。 | |
| ```rust | |
| let s: &'static str = "I have a static lifetime."; | |
| ``` | |
| この文字列のテキストはプログラムのバイナリに直接保持され、プログラムのバイナリは常に使用可能である。したがって、全文字列リテラルのlifetimeは `'static` である。 | |
| エラーメッセージのヘルプテキストで `'static` lifetimeの使用を勧められることがあるかもしれないが、referenceのlifetimeに `'static` を使用する前に、そのreferenceがプログラムの全lifetimeに渡って実際に存続するかどうかどうか(あるいは長く存続してほしいと思うのか)を考えて欲しい。ほとんどの場合、そのコードの問題点はdangling referenceや有効なlifetimeの不一致を生み出そうとしている点にあり、 `'static` lifetimeを使用するところではない。 | |
| ## Generic Type Parameters, Trait Bounds, and Lifetimes Together | |
| 関数のgeneric typeパラメータ、trait bounds、そしてlifetimeの全てを指定する構文を簡単に見てみよう。 | |
| ジェネリック型パラメータ、特性境界、およびすべての関数を1つの関数で指定する構文を簡単に見てみましょう。 | |
| Let's briefly look at the syntax of specifying generic type parameters, trait bounds, and lifetimes all in one function! | |
| ```rust | |
| use std::fmt::Display; | |
| fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str | |
| where T: Display | |
| { | |
| println!("Announcement! {}", ann); | |
| if x.len() > y.len() { | |
| x | |
| } else { | |
| y | |
| } | |
| } | |
| ``` | |
| これはリスト10-21の2つのstring sliceのうち長い方を返す `longest` 関数だが、 `ann` という余分な引数を持つ。 `ann` の型は総称型 `T` であり、これは `where` 節で指定される `Display` trait を実装している任意の型で記入することができる。関数がstring sliceの長さを比較する前にこの余分な引数が出力される。これが `Display` trait boundが必要な理由である。lifetimeは一種の総称型なので、lifetimeパラメータ `'a` と総称型パラメータ `T` の両方の宣言が関数名の後ろの山括弧の中の同じリストに入る。 | |
| ## まとめ | |
| この章では多くの項目をカバーした。総称型パラメータ、trait、trait bounds、generic lifetime parameterについて知ったので、様々な状況で使用できるコードを重複せずに実装する準備が整った。総称型パラメータはそのコードを様々な型に適用することを意味する。traitとtrait boundsは型が総称型であってもその型がコードに必要な振る舞いをすることを保証する。lifetimeアノテーションで指定されるreferenceのlifetime間の関係性は、この柔軟性のあるコードがdangling referenceを持たないことを保証する。そしてこれらはコンパイル時に発生するので、実行時のパフォーマンスには影響しない。 | |
| 信じようが信じまいが、これらの分野ではさらに学ぶべきことがある。17章ではtrait objectというtraitを使う別の方法について説明する。19章ではlifetimeアノテーションを含むより複雑なシナリオについて説明する。20章では高度な型システムの機能について説明する。 | |
| 次はRustでテストを書く方法を話す。これにより、コードで使用しているこれらすべての機能が望むように動作することを確認することができる。 |
10章では、Rustが異なるreferenceのlifetimeがどう関係しているかを理解するのを助けるために、どのようにlifetimeパラメータでreferenceをアノテーションするかを学んだ。たいていの場合Rustはlifetimeを伸ばすことができるが、各referenceはlifetimeを持っていることを見た。まだカバーしていないlifetimeの高度な機能としてlifetime subtyping、lifetime bounds、trait object lifetimeがある。
パーサを実装することを考えてみてほしい。そのために、パースする文字列へのreferenceを保持する構造体を用意し、これを Context と呼ぶ。この文字列をパースして成功または失敗を返すパーサを実装する。これを実装するとリスト19-12のコードのようになり、これはlifetimeアノテーションを残しているのでコンパイルできない。
struct Context(&str);
struct Parser {
context: &Context,
}
impl Parser {
fn parse(&self) -> Result<(), &str> {
Err(&self.context.0[1..])
}
}Listing 19-12: string sliceを持つ構造体 Context と、 Context インスタンスへのreferenceを持つ構造体 Parser 、常にstring sliceを参照するエラーを返す parse メソッドの定義
簡単にするために、ここでの parse 関数は Result<(), &str> を返す。つまり成功すると何も返さず、失敗すると正しくパースされなかったstring sliceの部分を返す。実際の実装ではより多くのエラー情報があり、パースに成功した時には実際に作成されたものが返されるが、この例のlifetimeの部分には関係ないためこれらの実装の部分は残している。また、最初のバイトの後に常にエラーを生成する parse も定義している。最初のバイトが有効な文字境界にない場合パニックになる可能性があることに注意する。関連するlifetimeに集中するために例を単純化している。
それでは、 Context のstring sliceと Parser の Context へのreferenceのlifetimeパラメータをどのように埋めるのか?リスト19-13のように、どこでも同じlifetimeを使うのが使用するのが最も簡単である。
struct Context<'a>(&'a str);
struct Parser<'a> {
context: &'a Context<'a>,
}
impl<'a> Parser<'a> {
fn parse(&self) -> Result<(), &str> {
Err(&self.context.0[1..])
}
}リスト19-13: Context と Parser の全referenceに同じlifetimeパラメータをアノテーションする
これはコンパイルされる。次に、リスト19-14で、 Context のインスタンスを受け取ってそのコンテキストをパースするのに Parser を使い、 parse の戻り値を返す関数を実装してみる。これはうまく動かない:
fn parse_context(context: Context) -> Result<(), &str> {
Parser { context: &context }.parse()
}リスト19-14: Context を取って Parse を用いる parse_context 関数を追加しようとしてみる
parse_context 関数を追加したコードをコンパイルしようとすると、非常に冗長なエラーが2つ発生する:
error: borrowed value does not live long enough
--> <anon>:16:5
|
16 | Parser { context: &context }.parse()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not live long enough
17 | }
| - temporary value only lives until here
|
note: borrowed value must be valid for the anonymous lifetime #1 defined on the
body at 15:55...
--> <anon>:15:56
|
15 | fn parse_context(context: Context) -> Result<(), &str> {
| ________________________________________________________^
16 | | Parser { context: &context }.parse()
17 | | }
| |_^
error: `context` does not live long enough
--> <anon>:16:24
|
16 | Parser { context: &context }.parse()
| ^^^^^^^ does not live long enough
17 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the anonymous lifetime #1 defined on the
body at 15:55...
--> <anon>:15:56
|
15 | fn parse_context(context: Context) -> Result<(), &str> {
| ________________________________________________________^
16 | | Parser { context: &context }.parse()
17 | | }
| |_^
作成中のParser インスタンスと context パラメータの両方が Parser が作成された行から parse_context 関数の終わりまで存続しているが、両方とも関数の全`lifetimeで存続する必要があるとエラーは述べている。
言い換えれば、コードの全referenceが常に有効であるようにするために、 Parser と context は関数全体より長く存続し、関数の終了後はもちろん開始前にも有効でなければならない。
parse_context は context のownershipを持つので、作成した Parser も引数の context も関数の終わりでスコープ外になる。
再びリスト19-13の定義、特に parse メソッドのシグネチャを見てみよう。
fn parse(&self) -> Result<(), &str> {elision ruleを覚えているだろうか?referenceのlifetimeをアノテーションするときはシグネチャは次のようになる:
fn parse<'a>(&'a self) -> Result<(), &'a str> {つまり、 parse の戻り値のエラー部分は Parser のインスタンスのlifetime( parse メソッドのシグネチャの &self )に結びついたlifetimeを持っている。戻り値のstring sliceが Parser が保持する Context インスタンス内のstring sliceを参照しているのでこれは意味があり、そして構造体 Parser の定義中で Parser が保持する Context へのreferenceのlifetimeと Context が保持するstring sliceのlifetimeが同じでなければないことを指定している。
問題は、 parse_context 関数が parse から返される値を返すことであり、よって parse_context の戻り値のlifetimeは Parser のlifetimeに結びつけられる。しかし parse_context 関数内で作られた Parser インスタンスは今のままでは関数の終わりを過ぎても存続せず、 context は関数の最後でスコープから外れる( parse_context がそのownershipを持つ)。
関数の最後にスコープ外になる値へのreferenceを返すことはできない。我々が同じlifetimeパラメータで全lifetimeをアノテーションしたので、Rustはこれが我々のしたいことだと考える。 アノテーションは、Context の保持するstring sliceのlifetimeは Parser が保持する Context へのreferenceのlifetimeと同じだとRustに言っている。
parse_context 関数は、返されるstring sliceは Context と Parser よりも長く存続し、 parse_context の返すreferenceが Context や Parser ではなくstring sliceを指すという parse 関数内を見ることはできない。
parse の実装が何をするのかを知ることにより、 parse の戻り値が Parser に結びついている唯一の理由が、それが Parser の Context を参照していて、これはstring sliceへ参照していることであると知り、そしてそれは parse_context が気にしなければならないstring sliceのlifetimeである。 Context のstring sliceと Parser の Context へのreferenceが異なるlifetimeを持ち、 parse_context の戻り値が Context のstring sliceのlifetimeと結びついていることをRustに伝える手段が必要である。
リスト19-15に示すように、 Parser と Context に異なるlifetimeパラメータを与えることができる。ここでは Context のstring sliceと Parser の Context へのreferenceのlifetimeを明確にするために、 's と 'c という名前のlifetimeパラメータを選んだ。これでは問題は完全には解決されないが、これが始まりであり、コンパイルしようとするときに十分でない理由を見ていく。
struct Context<'s>(&'s str);
struct Parser<'c, 's> {
context: &'c Context<'s>,
}
impl<'c, 's> Parser<'c, 's> {
fn parse(&self) -> Result<(), &'s str> {
Err(&self.context.0[1..])
}
}
fn parse_context(context: Context) -> Result<(), &str> {
Parser { context: &context }.parse()
}リスト19-15: string sliceと Context へのreferenceに異なるlifetimeパラメータを指定する
リスト19-13でアノテーションしたところと同じ場所にreferenceのlifetimeをアノテーションしたが、string sliceと Context どちらへのreferenceかに応じて異なるパラメータを用いた。また、 Context のstring sliceのlifetimeと同じであることを示すため、 parse の戻り値のstring slice部分にアノテーションを追加した。
次のエラーが得られる:
error[E0491]: in type `&'c Context<'s>`, reference has a longer lifetime than the data it references
--> src/main.rs:4:5
|
4 | context: &'c Context<'s>,
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
note: the pointer is valid for the lifetime 'c as defined on the struct at 3:0
--> src/main.rs:3:1
|
3 | / struct Parser<'c, 's> {
4 | | context: &'c Context<'s>,
5 | | }
| |_^
note: but the referenced data is only valid for the lifetime 's as defined on the struct at 3:0
--> src/main.rs:3:1
|
3 | / struct Parser<'c, 's> {
4 | | context: &'c Context<'s>,
5 | | }
| |_^
Rustは 'c と 's の間の関係を何も知らない。有効であるためには、lifetime 's の Context 中のreferenceデータが、lifetime 'c を持つ Context へのreferenceより長く存続することを保証するための制約である必要がある。 's が 'c よりも長くないと、 Context のreferenceが有効でないかもしれない。
これは本節のポイントに至る: Rustには lifetime subtypingと呼ばれる機能があり、これはあるlifetimeパラメータを少なくとも別のlifetimeパラメータと同じだけ存続させる指定をする方法である。lifetimeパラメータを宣言する山括弧の中でいつも通りlifetime 'a を宣言し、 'b: 'a 構文で 'b を宣言することにより、少なくとも 'a と同じ長さだけ存続するlifetime 'b を宣言する。
Parser の定義では、 's (string sliceのlifetime) が少なくとも 'c ( Context へのreferenceのlifetime)と同じくらい長く存続することを保証するため、lifetimeの宣言を次のように変更する。
struct Parser<'c, 's: 'c> {
context: &'c Context<'s>,
}Parser の Context への参照と Context のstring sliceへのreferenceは異なるlifetimeを持ち、string sliceのlifetimeは Context へのreferenceよりも長くなっている。
これは非常に長くに渡る例だったが、章の冒頭で述べたように、これらの機能はかなりニッチである。この構文はしばしば不要だが、このような状況の際にはreferenceがあるものを参照する必要がある。
10章では総称型に対してtrait boundsを使用する方法を説明した。また、総称型への制約としてlifetimeパラメータを追加することもできる。例えばreferenceにラッパーを作りたいとしよう。15章の RefCell<T> を覚えているだろうか?これは borrow と borrow_mut メソッドがどう働くかである。実行時にborrowing rulesを追跡するためにreferenceのラッパーを返す。今のところ、構造体にはlifetimeパラメータはないが、リスト19-16のようになる。
struct Ref<T>(&T);リスト19-16: はじめにlifetimeパラメータなしで総称型へのreferenceのラッパーをする構造体を定義する
しかし、Rustは総称型 T がどのくらい存続するか知らないので、lifetime boundsを全く使用しないとエラーになる。
error[E0309]: the parameter type `T` may not live long enough
--> <anon>:2:19
|
2 | struct Ref<'a, T>(&'a T);
| ^^^^^^
|
= help: consider adding an explicit lifetime bound `T: 'a`...
note: ...so that the reference type `&'a T` does not outlive the data it points at
--> <anon>:2:19
|
2 | struct Ref<'a, T>(&'a T);
| ^^^^^^
struct Ref(&i32) のような具体的な型で T を埋めても同じエラーとなる。構造体定義内の全referenceにはlifetimeパラメータが必要である。しかし総称型パラメータがあるため、同じ方法でlifetimeパラメータを追加することはできない。 Ref を struct Ref<'a>(&'a T) として定義すると、Rustは T が十分に長く存続していると判断できないためにエラーとなる。 T は任意の型になれるので、 T 自身がreferenceであるか、独自のlifetimeを持つ1つ以上のreferenceを保持する型である可能性がある。
この場合にlifetimeパラメータをどう指定するかはRustがアドバイスを与えてくれた。
consider adding an explicit lifetime bound `T: 'a` so that the reference type
`&'a T` does not outlive the data it points to.
T: 'a 構文は T が任意の型になれることを指定しているのでリスト19-17のコードは動くが、もしそれが任意のreferenceを持っている場合、 T は 'a と同じ期間存続する必要がある。
struct Ref<'a, T: 'a>(&'a T);リスト19-17: T 内の任意のreferenceが 'a と少なくとも同じだけ存続することを指定するために T にlifetime boundsを追加した
リスト19-18に示すように、 'static の上に T をboundingするという別の方法で解決する方法をとることもできる。これは、 T に任意の参照が含まれるなら 'static lifetimeを持たなければならないことを意味する。
struct StaticRef<T: 'static>(&'static T);リスト19-18 T に 'static referenceを持つ型かreferenceを持たない型の制約をするために T にlifetime bound 'static を追加する
referenceのない型は T: 'static として数える。 'static とはreferenceがプログラム全体と同じだけ存続する必要があることを意味するので、referenceを持たない型はreferenceがないのでプログラム全体と同じだけ存続する全referenceの基準を満たす。このように考えよう: もしborrowチェッカーが十分に長く存続するreferenceを心配している場合、referenceのない型と永久に存続するreferenceを持つ型の間に実際の区別はない。referenceが参照するものより短いlifetimeを持つか持たないかを判断する目的ではどちらも同じである。
17章では動的ディスパッチを使用するためにreferenceの後ろへのtraitの設置からなるtrait objectについて学んだ。しかし、もしtrait object中で使用するtraitを実装する型がlifetimeを持っている場合にどうなるかは議論しなかった。リスト19-19について考えてみよう。ここではtrait Foo と、trait Foo を実装するreferenceを持つ構造体 Bar (従ってこれはlifetimeパラメータを持つ)があり、trait object Box<Foo> として Bar のインスタンスを使いたい:
trait Foo { }
struct Bar<'a> {
x: &'a i32,
}
impl<'a> Foo for Bar<'a> { }
let num = 5;
let obj = Box::new(Bar { x: &num }) as Box<Foo>;リスト19-19: traint objectとともにlifetimeパラメータを持つ型を使う Listing 19-19: Using a type that has a lifetime parameter with a trait object
obj に含まれるlifetimeに関して何も言及していないのにこのコードはエラーなしにコンパイルされる。これはlifetimeとtrait objectと関係のある規則があるために動作する。
- trait objectの初期lifetimeは
'staticである。 - もし
&'a Xか&'a mut Xがあるなら初期値は'aである。 - もし
T: 'a節を1つ持つなら初期値は'aである。 - もし
T: 'aのような節を複数持つなら初期値はない。明示する必要がある。
明示的にする必要があるなら、 Box<Foo + 'a> あるいは Box<Foo + 'static> 構文とともに Box<Foo> のようなtrait objectに必要に応じて boundを追加することができる。
他のboundと同様、内部に任意のreferenceを持つ Foo traitの実装は、trait object bounds内でreferenceとして指定されてlifetimeを所有する必要がある。
他の境界と同様に、これは、内部に参照を持つ Foo特性の実装者は、それらの参照として特性オブジェクトの境界で指定された有効期間を持たなければならないことを意味します。
次はtraitを扱う高度な機能を見てみよう。