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

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

値があり、その値と、その値内の何かへの参照を独自の型で保存したいと考えています。

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

を見ようよこれを簡単に実装すると:

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 のライフタイムは、オブジェクトが作成されてから破棄されるまでの期間ではありません。

例として、このように考えてみましょう。人は生涯を通じて、さまざまな場所に住むことになり、それぞれに異なる住所があります。Rust のライフタイムは、現在住んでいる住所に関係しており、将来いつ死ぬかには関係していません (死ぬと住所も変わりますが)。移動するたびに、住所は無効になるため、関係があります。

また、ライフタイムによってコードが変更されるわけではないことにも注意してください。ライフタイムはコードによって制御されるのであって、ライフタイムがコードを制御するのではありません。簡潔に言えば、「ライフタイムは記述的であり、規定的ではない」ということです。

Combined::new有効期間を強調表示するために使用する行番号をいくつか注釈付けしましょう。

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

の具体的な有効期間parent1 から 4 までです ( と表します[1,4])。 の具体的な有効期間child[2,4]で、戻り値の具体的な有効期間は です[4,5]。 0 から始まる具体的な有効期間を持つことも可能で、これは関数へのパラメータやブロックの外部に存在するものの有効期間を表します。

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>たとえば を使用すると発生します。この場合、移動される構造体にはヒープへのポインターが含まれます。指し示された値は安定したままですが、ポインター自体のアドレスは移動します。実際には、常にポインターを追跡するため、これは問題になりません。

いくつかのクレートは、このケースを表現する方法を提供していますが、ベース アドレスが決して移動しないことを要求します。これにより、ヒープに割り当てられた値の再割り当てと移動を引き起こす可能性のある変更ベクトルが排除されます。

レンタルで解決できる問題の例:

他の場合には、参照カウントの何らかのタイプに移行したいかもしれません。例えば、RcまたはArc

詳しくは

構造体に移動した後parent、コンパイラが構造体への新しい参照を取得しparentて割り当てることができないのはなぜですか?child

理論的には可能ですが、これを行うと、複雑さとオーバーヘッドが大幅に増加します。オブジェクトが移動するたびに、コンパイラは参照を「修正」するためのコードを挿入する必要があります。つまり、構造体のコピーは、単にビットを移動するだけの非常に安価な操作ではなくなります。仮想的な最適化の精度によっては、次のようなコードが高価になる可能性もあります。

let a = Object::new();
let b = a;
let c = b;

プログラマーは、移動ごとにこれを強制するのではなく、呼び出したときにのみ適切な参照を取得するメソッドを作成することで、これをいつ実行するかを選択できます。

自分自身への参照を持つ型

自分自身への参照を持つ型を作成できる特別なケースが 1 つあります。ただし、2 つの手順で作成するには、次のようなものを使用する必要がありますOption

#[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> { /* ... */ }

同じコードをメソッドで実行しようとすると、魅力的だが結局は役に立たない が必要になります&'a self。それが関係する場合、このコードはさらに制限され、最初のメソッド呼び出しの後に借用チェッカー エラーが発生します。

#[derive(Debug)]
struct WhatAboutThis<'a> {
    name: String,
    nickname: Option<&'a str>,
}

impl<'a> WhatAboutThis<'a> {
    fn tie_the_knot(&'a mut self) {
       self.nickname = Some(&self.name[..4]); 
    }
}

fn main() {
    let mut tricky = WhatAboutThis {
        name: "Annabelle".to_string(),
        nickname: None,
    };
    tricky.tie_the_knot();

    // cannot borrow `tricky` as immutable because it is also borrowed as mutable
    // println!("{:?}", tricky);
}

参照:

についてはどうですかPin?

PinRust 1.33で安定化されたモジュールのドキュメント:

このようなシナリオの代表的な例としては、自己参照構造体の構築が挙げられます。これは、ポインターを持つオブジェクトを自分自身に移動すると、そのポインターが無効になり、未定義の動作が発生する可能性があるためです。

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

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

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

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

参照:

おすすめ記事