値があり、その値とその値内の何かへの参照を自分のタイプで保存したい:
struct Thing {
count: u32,
}
struct Combined<'a>(Thing, &'a u32);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing { count: 42 };
Combined(thing, &thing.count)
}
時々、値があり、その値とその値への参照を同じ構造に保存したいことがあります。
struct Combined<'a>(Thing, &'a Thing);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing::new();
Combined(thing, &thing)
}
時々、私は値の参照さえも取らず、同じエラーを受け取ります:
struct Combined<'a>(Parent, Child<'a>);
fn make_combined<'a>() -> Combined<'a> {
let parent = Parent::new();
let child = parent.child();
Combined(parent, child)
}
これらの各ケースで、値の1つが「十分に長く生きていない」というエラーを受け取ります。このエラーはどういう意味ですか?
これの簡単な実装 を見てみましょう:
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`
このエラーを完全に理解するには、メモリ内で値がどのように表されるか、およびそれらの値をmoveするとどうなるかを考える必要があります。 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つです。
これはまさにlifetimesが防ぐ問題です。ライフタイムとは、ユーザーとコンパイラーが現在のメモリー位置で値が有効になる期間を知ることができるメタデータの一部です。 Rust初心者がよくある間違いであるため、これは重要な違いです。 Rustライフタイムはnotオブジェクトが作成されてから破棄されるまでの時間です!
類推として、このように考えてください。人の生活の中で、彼らは多くの異なる場所に住み、それぞれが異なる住所を持ちます。 Rustライフタイムは、あなたが現在居住しているアドレスに関係します。将来あなたが死ぬときではありません(死ぬことであなたのアドレスも変わります)。住所が無効になるため、移動するたびに関係があります。
ライフタイムはコードを変更しないことに注意することも重要です。コードはライフタイムを制御しますが、ライフタイムはコードを制御しません。簡潔な格言は「寿命は記述的であり、規範的ではない」です。
ライフタイムを強調するために使用するいくつかの行番号でCombined::new
に注釈を付けましょう:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
parent
のconcrete lifetimeは1から4までです(これを[1,4]
と表します)。 child
の具体的なライフタイムは[2,4]
であり、戻り値の具体的なライフタイムは[4,5]
です。ゼロから始まる具体的なライフタイムを持つことができます-これは、関数のパラメータのライフタイムまたはブロック外に存在する何かを表します。
child
のライフタイム自体は[2,4]
ですが、はライフタイムが[1,4]
の値を参照することに注意してください。参照先の値が無効になる前に参照値が無効になる限り、これは問題ありません。この問題は、ブロックからchild
を返そうとすると発生します。これは、自然な長さを超えて寿命を「延長」します。
この新しい知識は、最初の2つの例を説明するはずです。 3番目の方法では、Parent::child
の実装を確認する必要があります。チャンスは、次のようになります。
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
これはlifetime elisionを使用して、明示的なgeneric lifetimeパラメーターの書き込みを回避します。以下と同等です:
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>
を使用する場合に発生します。この場合、移動される構造にはヒープへのポインターが含まれます。ポイントされた値は安定したままですが、ポインター自体のアドレスは移動します。実際には、常にポインタに従うため、これは重要ではありません。
rental crate または owning_ref crate は、このケースを表す方法ですが、ベースアドレスが絶対に移動しないようにする必要があります。これにより、ベクトルの変更が排除され、ヒープに割り当てられた値の再割り当てと移動が発生する可能性があります。
レンタルで解決した問題の例:
それ以外の場合は、 Rc
または Arc
を使用するなど、ある種の参照カウントに移行することをお勧めします。
parent
を構造体に移動した後、コンパイラがparent
への新しい参照を取得して、それを構造体のchild
に割り当てることができないのはなぜですか?
これを行うことは理論的には可能ですが、これを行うと、大量の複雑さとオーバーヘッドが発生します。オブジェクトが移動されるたびに、コンパイラは参照を「修正」するコードを挿入する必要があります。これは、構造体のコピーが、ビットを移動するだけの非常に安価な操作ではなくなったことを意味します。仮想オプティマイザーがどれだけ優れているかによって、このようなコードは高価になることさえ意味します。
let a = Object::new();
let b = a;
let c = b;
everymoveでこれを強制する代わりに、プログラマーはchooseに到達します。それらを呼び出すときにのみ適切な参照。
canがそれ自体への参照を持つ型を作成できる特定のケースが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);
}
これは何らかの意味で機能しますが、作成された値は非常に制限されています-neverは移動できません。特に、これは関数から返されたり、値ごとに何かに渡されたりできないことを意味します。コンストラクター関数は、上記のライフタイムと同じ問題を示します。
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Pin
はどうですか?Pin
、Rust 1.33で安定化、これがあります モジュールのドキュメント :
このようなシナリオの主な例は、自己参照構造体の構築です。これは、ポインターを使用してオブジェクトを移動すると、オブジェクトが無効になり、未定義の動作が発生する可能性があるためです。
「自己参照」は必ずしも参照を使用することを意味するわけではないことに注意することが重要です参照。確かに、 自己参照構造体の例 は具体的に(強調マイン)と言います:
このパターンは通常の借用規則では記述できないため、通常の参照ではコンパイラーに通知できません。代わりに生のポインターを使用しますが、文字列を指していることがわかっているため、nullではないことがわかっています。
Rust 1.0以降、この動作に生のポインタを使用する機能が存在します。実際、owning-refとレンタルでは、内部で生のポインタを使用します。
Pin
がテーブルに追加する唯一のものは、特定の値が移動しないことが保証されていることを述べる一般的な方法です。
こちらもご覧ください:
非常に類似したコンパイラメッセージを引き起こすわずかに異なる問題は、明示的な参照を保存するのではなく、オブジェクトの有効期間の依存関係です。その例は ssh2 ライブラリです。テストプロジェクトよりも大きなものを開発する場合、そのセッションから取得したSession
とChannel
を構造体に入れて、実装の詳細をユーザーから隠そうとするのは魅力的です。ただし、 Channel
定義の型注釈には'sess
ライフタイムがありますが、 Session
にはないことに注意してください。
これにより、有効期間に関連する同様のコンパイラエラーが発生します。
非常に簡単な方法でそれを解決する1つの方法は、呼び出し元の外部でSession
を宣言し、構造体内の参照に、 this Rustの答えと同様のライフタイムで注釈を付けることです。ユーザーのフォーラム投稿 SFTPのカプセル化中に同じ問題について話している。これはエレガントに見えず、常に適用されるとは限りません-必要なエンティティではなく、2つのエンティティを処理できるようになったためです!
他の答えから rental crate または owning_ref crate がこの問題の解決策であることが判明しました。 owning_refを考えてみましょう。owning_refには、まさにこの目的のための特別なオブジェクト OwningHandle
があります。基礎となるオブジェクトの移動を回避するために、Box
を使用してヒープに割り当てます。これにより、次の解決策が得られます。
use ssh2::{Channel, Error, Session};
use std::net::TcpStream;
use owning_ref::OwningHandle;
struct DeviceSSHConnection {
tcp: TcpStream,
channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}
impl DeviceSSHConnection {
fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
use std::net::TcpStream;
let mut session = Session::new().unwrap();
let mut tcp = TcpStream::connect(targ).unwrap();
session.handshake(&tcp).unwrap();
session.set_timeout(5000);
session.userauth_password(c_user, c_pass).unwrap();
let mut sess = Box::new(session);
let mut oref = OwningHandle::new_with_fn(
sess,
unsafe { |x| Box::new((*x).channel_session().unwrap()) },
);
oref.Shell().unwrap();
let ret = DeviceSSHConnection {
tcp: tcp,
channel: oref,
};
ret
}
}
このコードの結果、Session
は使用できなくなりますが、使用するChannel
と共に保存されます。 OwningHandle
オブジェクトは、Box
を逆参照するChannel
を逆参照するため、構造体に格納するときにそのように名前を付けます。 注:これは私の理解です。 OwningHandle
unsafetyの議論 にかなり近いように見えるので、これは正しくないかもしれないという疑いがあります。
ここで興味深いのは、Session
はTcpStream
がChannel
と関係があるので、Session
と論理的に同様の関係にありますが、その所有権は取得されず、そうすることに関する型注釈はありません。代わりに、 handshake methodのドキュメントにあるように、これはユーザーの責任です。
このセッションは提供されたソケットの所有権を取得しません。通信が正しく実行されることを保証するために、ソケットがこのセッションの存続期間を保持することを確認することをお勧めします。
また、プロトコルを妨害する可能性があるため、提供されたストリームは、このセッションの間、他の場所で同時に使用しないことを強くお勧めします。
したがって、TcpStream
の使用法では、コードの正確性を保証するのは完全にプログラマー次第です。 OwningHandle
を使用すると、unsafe {}
ブロックを使用して、「危険な魔法」が発生する場所に注意を引くことができます。
この問題のさらに詳細な説明は、この Rust User'sフォーラムスレッド にあります。これには、安全でないブロックを含まないレンタルクレートを使用した別の例とそのソリューションが含まれています。