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


199

Rustの本の寿命の章を読んでいて、名前付き/明示的な寿命のこの例に出くわしました。

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:内側のスコープが完了、された後、fそのため&f.x無効になり、そしてに割り当てられているべきではありませんx

私の問題は、たとえばより広いスコープへの参照の不正な割り当てを推測するなど、明示的な有効期間使用せずに問題を簡単に分析できたことです。 'ax = &f.x;

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


1
これはReddit
Shepmaster 2015

2
この質問の将来の読者のために、本の最初の版へのリンクであり、現在は第2版があることに注意してください:)
carols10cents

回答:


205

他の答えにはすべて顕著な点がありますが(明示的な有効期間が必要なfjhの具体例)、重要な点が1つありません

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

fn foo() -> _ {  
    ""
}

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

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

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

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

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

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

より広いスコープへの参照の不正な割り当てを推測する

スコープ本質的にライフタイムです。もう少し明確に言うと、ライフタイム'aは、呼び出しサイトに基づいて、コンパイル時に特定のスコープに特化できる一般的なライフタイムパラメーターです。

[...]エラーを防ぐために、明示的なライフタイムが実際に必要ですか?

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


18
@jco f x = x + 1別のモジュールで使用している型シグネチャのないトップレベルの関数があるとします。後で定義をに変更するとf x = sqrt $ x + 1、そのタイプがからNum a => a -> aに変更されます。これにより、が引数Floating a => a -> aなどfで呼び出されるすべての呼び出しサイトでタイプエラーが発生しIntます。型シグネチャがあると、エラーがローカルで発生します。
fjh 2015

11
「スコープは本質的にライフタイムです。もう少し明確に言えば、ライフタイム 'aは、呼び出し時に特定のスコープに特化できる一般的なライフタイムパラメーターです。」わあ、それは本当に素晴らしい、明快なポイントです。この本に明記されていればよかったのですが。
corazza

2
@fjhありがとう。私がそれを理解したかどうかを確認するだけです-重要なのは、追加する前に型が明示的に述べられていた場合sqrt $、変更後にローカルエラーのみが発生し、他の場所では多くのエラーが発生しなかったということです(そうしないと、はるかに優れています)実際のタイプを変更したくない)?
corazza

5
@jcoその通りです。タイプを指定しないと、関数のインターフェースを誤って変更してしまう可能性があります。これが、Haskellのすべてのトップレベルアイテムに注釈を付けることが強く推奨される理由の1つです。
fjh 2015

5
また、関数が2つの参照を受け取って参照を返す場合、最初の参照が返されることもあれば、2番目の参照が返されることもあります。この場合、返された参照の寿命を推測することは不可能です。明示的なライフタイムは、このような状況を回避/明確化するのに役立ちます。
MichaelMoser

93

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

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)と同じ存続期間であるため、これはコンパイルされ、2番目の引数よりも長く存続する可能性があります。これは、のシグニチャのライフタイム名によって表され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

16

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

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

Fooインスタンスが、それが含む参照(xフィールド)より長く存続しないように指定します。

Rustブックで出くわした例ではfy変数は同時にスコープ外になるため、これについては説明していません。

より良い例はこれです:

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);
}

さて、f実際にが指す変数よりも長く存続しますf.x


9

構造の定義を除いて、そのコードの一部には明示的な存続期間がないことに注意してください。コンパイラは、で寿命を完全に推測できmain()ます。

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

struct RefPair(&u32, &u32);

これらは異なる寿命である必要がありますか、それとも同じですか?使用方法の観点からは重要struct RefPair<'a, 'b>(&'a u32, &'b u32)ですが、とは大きく異なりstruct RefPair<'a>(&'a u32, &'a u32)ます。

今、あなたが提供したような単純なケースの場合、コンパイラ理論的には他の場所と同じように寿命を排除することができますが、そのようなケースは非常に限られており、コンパイラをさらに複雑にする価値はありません。非常に疑わしい。


2
それらが非常に異なる理由を説明できますか?
AB

@AB 2番目では、両方の参照が同じ存続期間を共有する必要があります。つまり、refpair.1はrefpair.2よりも長く存続することはできず、その逆も同様です。したがって、両方のrefは同じ所有者の何かを指す必要があります。ただし、1つ目は、RefPairがその両方の部分より長く存続することのみを要求します。
llogiq 2015

2
双方の寿命が統一されているので@ABは、それがコンパイル-地元の寿命はその小さいため'static'static地元の寿命を使用することができますどこので、あなたの例では、どこでも使用することができp、その寿命パラメータは、ローカル寿命と推測されていますy
Vladimir Matveev

5
@AB RefPair<'a>(&'a u32, &'a u32)は、それ'aが両方の入力ライフタイム、つまりこの場合はのライフタイムの交点になることを意味しますy
fjh 2015

1
@llogiqは「RefPairが両方の部分よりも長く存続することを要求します」?私はそれが反対だったのに...&u32はRefPairがなくても意味をなしますが、RefPairが無効なRefPairは奇妙です。
16

6

本のケースは、デザインが非常にシンプルです。寿命のトピックは複雑であると見なされます。

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

また、私自身のオプションのクレートには、シグニチャーが実際に次のようなメソッドを持つOptionBoolタイプがas_sliceあります。

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

コンパイラがそれを理解できたはずはありません。


IINM、2つの引数を持つ関数の戻り値の型の有効期間を推測することは、停止の問題と同じになります-IOW、有限時間で決定できません。
dstromberg 2018

4

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

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


4

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

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

いくつかの単純な状況では、コンパイラーが存続期間を推測できる存続期間省略があります。


1

あなたの例が機能しない理由は、Rustにはローカルの有効期間と型の推論しかないためです。あなたが提案しているものはグローバルな推論を要求します。存続期間を省略できない参照がある場合は常に、注釈を付ける必要があります。


1

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]])

このような行動はいつも私を驚かせます。何が起こっているかというdfと、とメモリを共有しているarため、の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)を返すため、コンパイラーはライフタイムの不一致について不平を言います。適切な場所で。正確な時に。デバッグは簡単です。


0

ライフタイムアノテーションは、与えられたrefに関するコントラクトとして、ソーススコープでは有効なままである間のみ、受信スコープで有効であると考えています。同じ存続期間の種類でより多くの参照を宣言すると、スコープがマージされます。つまり、すべてのソース参照がこのコントラクトを満たす必要があります。このような注釈により、コンパイラは契約の履行を確認できます。

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