これの簡単な実装を見てみましょう:
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
これはエラーで失敗します:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
このエラーを完全に理解するには、値がメモリ内でどのように表されるか、および
それらの値を移動するとどうなるかについて考える必要があります。Combined::new
値が配置されている場所を示すいくつかの仮想メモリアドレスで注釈を付けましょう。
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
どうなりchild
ますか?値がparent
そのまま移動された場合、有効な値を持つことが保証されなくなったメモリを参照します。他のコードでは、メモリアドレス0x1000に値を格納できます。整数であると想定してそのメモリにアクセスすると、クラッシュやセキュリティバグにつながる可能性があり、Rustが防止するエラーの主なカテゴリの1つです。
これはまさに寿命が妨げる問題です。ライフタイムとは、現在のメモリ位置で値が有効になる期間をユーザーとコンパイラが知ることができる、ちょっとしたメタデータです。これは、Rustの初心者が犯す一般的な間違いであるため、重要な違いです。錆の寿命は、オブジェクトが作成されてから破棄されるまでの期間ではありません。
類推として、次のように考えてください。人の人生の間に、彼らは多くの異なる場所に居住し、それぞれに異なる住所があります。Rustのライフタイムは、現在住んでいる住所に関係し、将来死ぬことはありません(死んでも住所が変更されます)。あなたの住所はもはや有効ではないので、あなたが移動するたびにそれは関連しています。
また、存続期間によってコードが変更されることはありません。コードはライフタイムを制御し、ライフタイムはコードを制御しません。簡潔なことわざは、「寿命は説明的なものではなく説明的なものです」です。
Combined::new
寿命を強調するために使用するいくつかの行番号で注釈を付けましょう:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
コンクリートの寿命のは、parent
1〜4であり、(Iとして表すうれる包括的[1,4]
)。の具体的な寿命child
は[2,4]
であり、戻り値の具体的な寿命は[4,5]
です。ゼロから始まる具体的な有効期間を持つことが可能です。これは、ブロックの外部に存在する関数または何かに対するパラメーターの有効期間を表します。
child
自身の存続期間はですが[2,4]
、存続期間がの値を参照することに注意してください[1,4]
。これは、参照先の値が無効になる前に、参照先の値が無効になる限り問題ありません。この問題はchild
、ブロックから戻るときに発生します。これは、本来の長さを超えて寿命を「超過」します。
この新しい知識は、最初の2つの例を説明するはずです。3つ目は、の実装を確認することですParent::child
。おそらく、次のようになります。
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
これは、ライフタイム省略を使用して、明示的なジェネリックライフタイムパラメータの記述を回避します。これは次と同等です。
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
どちらの場合も、メソッドはChild
、具体的な存続期間でパラメーター化された構造が返されることを示しています
self
。別の言い方をすると、Child
インスタンスにはParent
それを作成したへの参照が含まれているため、そのParent
インスタンスよりも長く存続することはできません
。
これにより、作成機能に本当に問題があることがわかります。
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
これは別の形式で書かれている可能性が高くなりますが、
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
どちらの場合も、引数を介して提供される存続期間パラメーターはありません。これは、Combined
パラメーター化される存続期間が何によっても制約されないことを意味します-呼び出し元が望むとおりにすることができます。呼び出し側が'static
有効期間を指定でき、その条件を満たす方法がないため、これは無意味です。
どうすれば修正できますか?
最も簡単で推奨される解決策は、これらのアイテムを同じ構造にまとめようとしないことです。これを行うことにより、構造のネストはコードの存続期間を模倣します。データを所有する型を一緒に構造体に配置し、必要に応じて参照または参照を含むオブジェクトを取得できるメソッドを提供します。
ライフタイムトラッキングが熱狂的である特別なケースがあります。ヒープに何かを配置したときです。これは、Box<T>
たとえばを使用した場合に発生します
。この場合、移動される構造には、ヒープへのポインターが含まれます。指摘された値は安定したままですが、ポインター自体のアドレスは移動します。実際には、常にポインタに従っているため、これは重要ではありません。
レンタルクレート(もはや維持されるか、またはサポートされている)またはowning_refクレートは、このケースを表現する方法ですが、彼らはベースアドレスがいる必要が動くことはありません。これにより、変化するベクトルが除外され、ヒープに割り当てられた値の再割り当てと移動が発生する可能性があります。
レンタルで解決した問題の例:
他の場合では、Rc
またはを使用するなどして、あるタイプの参照カウントに移行することもできますArc
。
詳しくは
parent
構造体に移動した後、コンパイラが構造体で新しい参照を取得しparent
て割り当てることができないのはなぜchild
ですか?
これを行うことは理論的には可能ですが、これを行うと大量の複雑さとオーバーヘッドが発生します。オブジェクトが移動されるたびに、コンパイラは参照を「修正」するコードを挿入する必要があります。これは、構造体のコピーが、ビットを移動するだけの安価な操作ではなくなったことを意味します。架空のオプティマイザがどの程度優れているかによっては、このようなコードが高価になることさえあります。
let a = Object::new();
let b = a;
let c = b;
すべての移動でこれを強制的に実行する代わりに、プログラマは、呼び出し時にのみ適切な参照を取得するメソッドを作成することにより、これがいつ発生するかを選択できます。
自身への参照を持つ型
それ自体への参照を使用してタイプを作成できる特定のケースが1つあります。Option
ただし、次のような2つのステップで作成する必要があります。
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
これはある意味では機能しますが、作成された値は非常に制限されており、移動することはできません。特に、これは、関数から返すことも、値渡しで何かに渡すこともできないことを意味します。コンストラクター関数は、上記と同じ存続期間の問題を示します。
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
どうPin
ですか?
Pin
Rust 1.33で安定化され、モジュールのドキュメントにこれがあります:
このようなシナリオの主な例は、自己参照構造体の構築です。ポインタを含むオブジェクトをそれ自体に移動すると、それらが無効になり、未定義の動作が発生する可能性があるためです。
「自己参照」は必ずしも参照を使用することを意味するものではないことに注意することが重要です。確かに、自己参照構造体の例は具体的に次のように述べています(私の強調):
このパターンは通常の借用規則では記述できないため、通常の参照ではコンパイラに通知できません。代わりに、生のポインタを使用しますが、文字列を指していることがわかっているため、nullでないことがわかっています。
この動作に生のポインタを使用する機能は、Rust 1.0以降に存在します。実際、owning-refとrentalは、内部で生のポインタを使用します。
Pin
テーブルに追加される唯一のものは、特定の値が移動しないことが保証されていることを示す一般的な方法です。
以下も参照してください。
Parent
とChild
役立つ可能性があります...