web-dev-qa-db-ja.com

Rustで明示的なライフタイムが必要なのはなぜですか?

Rust本の lifetimes章 を読んでいて、名前付き/明示的な存続期間でこの例を見つけました。

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                    // -+ x goes into scope
                              //  |
    {                         //  |
        let y = &5;           // ---+ y goes into scope
        let f = Foo { x: y }; // ---+ f goes into scope
        x = &f.x;             //  | | error here
    }                         // ---+ f and y go out of scope
                              //  |
    println!("{}", x);        //  |
}                             // -+ x goes out of scope

コンパイラによって防止されているエラーは、xに割り当てられた参照のse-after-freeであることが非常に明確です。内部スコープの実行後、fしたがって、&f.xは無効になり、xに割り当てられるべきではありません。

私の問題は、たとえば、より広い参照への参照の不正な割り当てを推測することにより、explicit'aライフタイムを使用して、問題を簡単に分析できることですwithoutスコープ(x = &f.x;)。

どのような場合、解放後使用(または他のクラス?)エラーを防ぐために明示的なライフタイムが実際に必要ですか?

175
corazza

他の回答にはすべて顕著な点があります( 明示的なライフタイムが必要なfjhの具体例 )が、1つの重要な事柄が欠落しています:コンパイラあなたはそれらを間違えたと教えてくれます

これは、実際には「コンパイラが推論できる場合に明示的な型が必要な理由」と同じ質問です。架空の例:

fn foo() -> _ {  
    ""
}

もちろん、コンパイラは&'static strを返していることを確認できますが、なぜプログラマはそれを入力する必要があるのでしょうか?

主な理由は、コンパイラーはコードが何をするのかを見ることができるが、意図が何であるかを知らないためです。

関数は、コード変更の影響をファイアウォールで保護するための自然な境界です。コードからライフタイムを完全に検査できるようにすると、無害な外観の変更がライフタイムに影響を与える可能性があり、その結果、遠く離れた関数でエラーが発生する可能性があります。これは架空の例ではありません。私が理解しているように、トップレベル関数の型推論に依存しているとき、Haskellにはこの問題があります。 Rustは特定の問題をつぼみに挟みました。

また、コンパイラには効率上の利点があります。型と有効期間を検証するために解析する必要があるのは関数シグネチャのみです。さらに重要なことは、プログラマーにとって効率的な利点があることです。明示的なライフタイムがなかった場合、この関数は何をしますか:

fn foo(a: &u8, b: &u8) -> &u8

ソースを検査せずに伝えることは不可能であり、膨大な数のコーディングのベストプラクティスに反することになります。

より広い範囲への参照の違法な割り当てを推測することにより

スコープareライフタイム、本質的に。もう少し明確に、ライフタイム'aジェネリックライフタイムパラメータで、コンパイル時に特定のスコープで特殊化できます。サイトを呼び出します。

[...]エラーを防ぐために、明示的な有効期間は実際に必要ですか?

どういたしまして。 Lifetimesはエラーを防止するために必要ですが、小さな健全性プログラマが持っているものを保護するために明示的なライフタイムが必要です。

190
Shepmaster

次の例を見てみましょう。

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
    x
}

fn main() {
    let x = 12;
    let z: &u32 = {
        let y = 42;
        foo(&x, &y)
    };
}

ここでは、明示的なライフタイムが重要です。 fooの結果の有効期間は最初の引数('a)と同じであるため、これはコンパイルされます。これは、fooの署名のライフタイム名で表されます。 fooの呼び出しで引数を切り替えた場合、コンパイラはyの寿命が十分でないと文句を言います。

error[E0597]: `y` does not live long enough
  --> src/main.rs:10:5
   |
9  |         foo(&y, &x)
   |              - borrow occurs here
10 |     };
   |     ^ `y` dropped here while still borrowed
11 | }
   | - borrowed value needs to live until here
86
fjh

次の構造のライフタイムアノテーション:

struct Foo<'a> {
    x: &'a i32,
}

Fooインスタンスは、それが含む参照(xフィールド)を超えないように指定します。

Rustの本で出会った例では、f変数とy変数が同時にスコープ外になるため、これを説明していません。

より良い例はこれでしょう:

fn main() {
    let f : Foo;
    {
        let n = 5;  // variable that is invalid outside this block
        let y = &n;
        f = Foo { x: y };
    };
    println!("{}", f.x);
}

これで、ff.xが指す変数よりも実際に有効です。

12
user3151599

構造定義を除き、そのコードには明示的な有効期間はありません。コンパイラは、main()の寿命を完全に推測できます。

ただし、型定義では、明示的な有効期間は避けられません。たとえば、ここにはあいまいさがあります。

struct RefPair(&u32, &u32);

これらは異なるライフタイムである必要がありますか、または同じである必要がありますか?使用の観点からは問題ありません。struct RefPair<'a, 'b>(&'a u32, &'b u32)struct RefPair<'a>(&'a u32, &'a u32)とは大きく異なります。

さて、あなたが提供したような単純なケースでは、コンパイラ[could理論的には 他の場所でのようにライフタイムを除外します が、そのようなケースは非常に限られていますコンパイラーの複雑さを増すだけの価値はありません。この明確さの向上は、少なくとも疑わしいものです。

9

本のケースは、設計上非常に単純です。寿命のトピックは複雑であると見なされます。

コンパイラは、複数の引数を持つ関数の寿命を簡単に推測することはできません。

また、独自の オプション crateには、実際に署名がas_sliceメソッドを持つOptionBoolタイプがあります。

fn as_slice(&self) -> &'static [bool] { ... }

コンパイラがそれを見つけ出す方法はまったくありません。

6
llogiq

関数が引数として2つの参照を受け取り、参照を返す場合、関数の実装は最初の参照と2番目の参照を返すことがあります。特定の呼び出しに対してどの参照が返されるかを予測することは不可能です。この場合、返される参照のライフタイムを推測することはできません。各引数リファレンスは、異なるライフタイムを持つ異なる変数バインディングを参照する可能性があるためです。明示的なライフタイムは、このような状況を回避または明確にするのに役立ちます。

同様に、構造体が2つの参照を(2つのメンバーフィールドとして)保持している場合、構造体のメンバー関数は最初の参照と2番目の参照を返すことがあります。ここでも、明示的なライフタイムにより、このようなあいまいさが回避されます。

いくつかの単純な状況では、 lifetime elision があり、コンパイラがライフタイムを推測できます。

4
MichaelMoser

ここで別の素晴らしい説明を見つけました: http://doc.Rust-lang.org/0.12.0/guide-lifetimes.html#returning-references

一般に、参照がパラメーターからプロシージャーに派生している場合にのみ参照を返すことができます。その場合、ポインターの結果の有効期間は常にパラメーターの1つと同じになります。名前付きライフタイムは、どのパラメーターであるかを示します。

3
corazza

Rustの新参者として、私の理解では、明示的なライフタイムは2つの目的を果たします。

  1. 関数に明示的な有効期間注釈を付けると、その関数内に表示されるコードの種類が制限されます。明示的な有効期間により、コンパイラは、プログラムが意図したとおりに動作していることを確認できます。

  2. コードの一部が有効であるかどうかをコンパイラーが確認したい場合、呼び出されたすべての関数を繰り返し見る必要はありません。そのコードによって直接呼び出される関数の注釈を見るだけで十分です。これにより、プログラム(コンパイラー)の推論がはるかに簡単になり、コンパイル時間を管理しやすくなります。

ポイント1.で、Pythonで記述された次のプログラムを検討します。

import pandas as pd
import numpy as np

def second_row(ar):
    return ar[0]

def work(second):
    df = pd.DataFrame(data=second)
    df.loc[0, 0] = 1

def main():
    # .. load data ..
    ar = np.array([[0, 0], [0, 0]])

    # .. do some work on second row ..
    second = second_row(ar)
    work(second)

    # .. much later ..
    print(repr(ar))

if __name__=="__main__":
    main()

印刷する

array([[1, 0],
       [0, 0]])

このタイプの行動はいつも私を驚かせます。 dfarとメモリを共有しているため、dfのコンテンツの一部がworkで変更されると、その変更もarに感染します。 。ただし、場合によっては、メモリ効率の理由(コピーなし)のために、これがまさに必要な場合があります。このコードの本当の問題は、関数second_rowが2番目ではなく最初の行を返すことです。それをデバッグしてください。

代わりに、Rustで書かれた同様のプログラムを検討してください。

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);

impl<'a, 'b> Array<'a, 'b> {
    fn second_row(&mut self) -> &mut &'b mut [i32] {
        &mut self.0
    }
}

fn work(second: &mut [i32]) {
    second[0] = 1;
}

fn main() {
    // .. load data ..
    let ar1 = &mut [0, 0][..];
    let ar2 = &mut [0, 0][..];
    let mut ar = Array(ar1, ar2);

    // .. do some work on second row ..
    {
        let second = ar.second_row();
        work(second);
    }

    // .. much later ..
    println!("{:?}", ar);
}

これをコンパイルすると、

error[E0308]: mismatched types
 --> src/main.rs:6:13
  |
6 |             &mut self.0
  |             ^^^^^^^^^^^ lifetime mismatch
  |
  = note: expected type `&mut &'b mut [i32]`
             found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

実際には2つのエラーが発生しますが、'a'bの役割を交換したものもあります。 second_rowの注釈を見ると、出力は&mut &'b mut [i32]である必要があることがわかります。つまり、出力はライフタイム'b(2番目のライフタイム) Array)の行。ただし、最初の行(ライフタイム'aを含む)を返すため、コンパイラはライフタイムの不一致について文句を言います。適切な場所で。適切な時に。デバッグは簡単です。

1
Jonas Dahlbæk

あなたの例がうまくいかない理由は、単にRustがローカルの寿命と型推論しか持っていないからです。あなたが提案していることは、グローバルな推論を要求します。有効期間を省略できない参照がある場合は、注釈を付ける必要があります。

0
Klas. S