web-dev-qa-db-ja.com

同じ構造体に値とその値への参照を保存できないのはなぜですか?

値があり、その値とその値内の何かへの参照を自分のタイプで保存したい:

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つが「十分に長く生きていない」というエラーを受け取ります。このエラーはどういう意味ですか?

185
Shepmaster

これの簡単な実装 を見てみましょう:

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

parentconcrete 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がテーブルに追加する唯一のものは、特定の値が移動しないことが保証されていることを述べる一般的な方法です。

こちらもご覧ください:

198
Shepmaster

非常に類似したコンパイラメッセージを引き起こすわずかに異なる問題は、明示的な参照を保存するのではなく、オブジェクトの有効期間の依存関係です。その例は ssh2 ライブラリです。テストプロジェクトよりも大きなものを開発する場合、そのセッションから取得したSessionChannelを構造体に入れて、実装の詳細をユーザーから隠そうとするのは魅力的です。ただし、 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の議論 にかなり近いように見えるので、これは正しくないかもしれないという疑いがあります。

ここで興味深いのは、SessionTcpStreamChannelと関係があるので、Sessionと論理的に同様の関係にありますが、その所有権は取得されず、そうすることに関する型注釈はありません。代わりに、 handshake methodのドキュメントにあるように、これはユーザーの責任です。

このセッションは提供されたソケットの所有権を取得しません。通信が正しく実行されることを保証するために、ソケットがこのセッションの存続期間を保持することを確認することをお勧めします。

また、プロトコルを妨害する可能性があるため、提供されたストリームは、このセッションの間、他の場所で同時に使用しないことを強くお勧めします。

したがって、TcpStreamの使用法では、コードの正確性を保証するのは完全にプログラマー次第です。 OwningHandleを使用すると、unsafe {}ブロックを使用して、「危険な魔法」が発生する場所に注意を引くことができます。

この問題のさらに詳細な説明は、この Rust User'sフォーラムスレッド にあります。これには、安全でないブロックを含まないレンタルクレートを使用した別の例とそのソリューションが含まれています。

3
Andrew Y