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


222

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

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つが「十分に長く存続しない」というエラーが発生します。このエラーはどういう意味ですか?


1
後者の例では、定義ParentChild役立つ可能性があります...
Matthieu M.

1
@MatthieuM。私はそれについて議論しましたが、2つのリンクされた質問に基づいて反対することにしました。これらの質問はどちらも、問題の構造体またはメソッドの定義を検討していなかったため、この質問を自分の状況に簡単に一致させることができるように模倣するのが最善だと思いました。私がいることを注記答えにメソッドシグネチャを示しています。
Shepmaster 2015

回答:


245

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

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

コンクリートの寿命のは、parent1〜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ですか?

PinRust 1.33で安定化され、モジュールのドキュメントにこれがあります

このようなシナリオの主な例は、自己参照構造体の構築です。ポインタを含むオブジェクトをそれ自体に移動すると、それらが無効になり、未定義の動作が発生する可能性があるためです。

「自己参照」は必ずしも参照を使用することを意味するものではないことに注意することが重要です。確かに、自己参照構造体例は具体的に次のように述べています(私の強調):

このパターンは通常の借用規則では記述できないため、通常の参照ではコンパイラに通知できません。代わりに、生のポインタを使用しますが、文字列を指していることがわかっているため、nullでないことがわかっています。

この動作に生のポインタを使用する機能は、Rust 1.0以降に存在します。実際、owning-refとrentalは、内部で生のポインタを使用します。

Pinテーブルに追加される唯一のものは、特定の値が移動しないことが保証されていることを示す一般的な方法です。

以下も参照してください。


1
このようなもの(is.gd/wl2IAt)は慣用的と見なされますか?つまり、生データの代わりにメソッドを介してデータを公開します。
Peter Hall

2
@PeterHall確かに、それはをCombined所有するChildを所有することを意味しますParent。それはあなたが持っている実際のタイプに応じて意味があるかもしれませんし、意味がないかもしれません。独自の内部データへの参照を返すことは、かなり典型的です。
シェプマスター、2016年

ヒープ問題の解決策は何ですか?
derekdreery 2016年

@derekdreeryコメントを拡張できますか?パラグラフ全体がowning_refクレートについて話しているのはなぜ不十分なのですか?
Shepmaster 2016年

1
@FynnBeckerでは、参照とその参照への値を保存することはまだ不可能です。Pin主に、自己参照ポインタを含む構造体の安全性を知る方法です。Rust 1.0以降、同じ目的で生のポインタを使用する機能が存在します。
シェプマスター

4

非常によく似たコンパイラメッセージを引き起こすわずかに異なる問題は、明示的な参照を格納するのではなく、オブジェクトの有効期間の依存関係です。その一例がssh2ライブラリです。テストプロジェクトよりも大きな何かを開発するとき、入れて試してみたくなるSessionChannel、ユーザから実装の詳細を隠し、構造体の中に互いに並んでそのセッションから取得しました。ただし、Channel定義には'sess型注釈に存続期間がありますが、存続期間はありSessionません。

これにより、寿命に関連する同様のコンパイラエラーが発生します。

非常に簡単な方法でそれを解決する1つの方法は、宣言することであるSession呼び出し側で外側をした後、注釈のためにある答えに似た寿命を持つ構造体内の参照、この錆ユーザーズフォーラムの投稿は、同じ問題について話してSFTPをカプセル化しながら、 。これはエレガントに見えず、常に適用されるとは限りません。これで、処理するエンティティが2つになりました。

判明レンタルクレートまたは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安全でないという議論に非常に近いように思われるため、これは正しくない可能性があるという疑いがあります。

ここでは一つの好奇心詳細は、ということであるSession論理と同様の関係があるTcpStreamようChannelに持っているがSession、まだその所有権が取られておらず、そう周囲に型注釈が存在しません。代わりに、ハンドシェイクメソッドのドキュメントが言うように、これを処理するかどうかはユーザー次第です。

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

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

したがって、TcpStream使用方法については、コードの正確さを保証するプログラマーに完全に委ねられています。を使用するOwningHandleと、「危険な魔法」が発生する場所への注意がunsafe {}ブロックを使用して描画されます。

この問題に関するさらに高度な議論は、このRustユーザーのフォーラムスレッドにあります。これには、危険なブロックを含まないレンタルクレートを使用した別の例とそのソリューションが含まれています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.