1. ホーム

[解決済み】ある値とその値への参照を同じ構造体に格納できないのはなぜですか?

2022-03-28 05:42:40

質問

ある値があって、その値と、その値への参照を保存したいのです。 その値の中にあるものを、自分自身の型に入れる。

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つが"doesn'tというエラーが表示されます。 not long enough". このエラーは何を意味するのでしょうか?

解決方法を教えてください。

を見てみましょう。 の簡単な実装です。 :

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が防止するエラーの主なカテゴリーです。

これはまさに ライフタイム を防ぐことができます。ライフタイムとは というメタデータがあります。 の値はその時点で有効です。 現在のメモリ位置 . これは これはRust初心者が犯しがちなミスなので、重要な違いです。 Rustのライフタイムは ではなく オブジェクトが生成されるまでの期間 作成され、破棄されます。

例えて言えば、こんな風に考えてみてください。人は一生の間に それぞれ住所の異なるさまざまな場所に住んでいます。A ラストライフは、あなたの住所に関するものです。 現在の居住地 , 将来いつ死ぬかではなく(死ぬこともありますが は住所を変更します)。引っ越しのたびに住所が変更されます。 の住所が有効でなくなる。

また、ライフタイムが はしません。 コードを変更します。 コードが寿命をコントロールし、寿命がコードをコントロールするのではありません。つまり ライフタイムは記述的であり、規定的ではない」という格言があります。

アノテーションをつけよう Combined::new には行番号を付け、それを使って を使い、ライフタイムを強調する。

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

具体的な寿命 parent は、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 { /* ... */ }
}

これは ライフタイムエリジョン を明示的に書かないようにするため 汎用 寿命パラメータ . と同等である。

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;

に対して強制的に発生させるのではなく すべての を実行すると、プログラマは 選ぶ を作成し、呼び出したときだけ適切な参照を取得するようにします。

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

具体的なケースとして できる 自分自身への参照を持つ型を作成します。次のようなものを使う必要があります。 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> { /* ... */ }

これと同じコードをメソッドで実行しようとすると、魅力的だが結局は役に立たない &'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 ?

Pin Rust 1.33 で安定化した、この モジュールドキュメント内の :

このようなシナリオの典型例は、自己参照構造体を構築することです。自分自身へのポインタを持つオブジェクトを移動すると、それらが無効となり、未定義の動作が発生する可能性があるからです。

ここで重要なのは、quot;self-referential"は、必ずしもquot;self-referential"を使うことを意味しないことです。 参照 . 確かに 自己言及的な構造体の例 は具体的にこう言っている(強調)。

それを通常のリファレンスでコンパイラに知らせることはできない。 このパターンは通常の借用規則では記述できないからです。 その代わり 生のポインタを使用します。 ただし、NULLでないことが分かっているものです。 文字列を指していることが分かっているからです。

この動作に生ポインタを使用する機能は、Rust 1.0から存在していました。実際、owning-refとrentalは、フードの下で生のポインタを使用しています。

唯一 Pin は、ある値が動かないことが保証されていることを示す一般的な方法を追加しています。

こちらもご覧ください。