寿命を持つ参照の検証

RustRustBeginner
オンラインで実践に進む

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

寿命を持つ参照の検証へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。

この実験では、寿命と、必要な限り参照が有効であることをどのように保証するかについて説明します。寿命は慣れ親しんだものではないかもしれませんが、寿命構文に遭遇する一般的な方法をカバーして、この概念に慣れるのを助けます。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100414{{"寿命を持つ参照の検証"}} rust/integer_types -.-> lab-100414{{"寿命を持つ参照の検証"}} rust/string_type -.-> lab-100414{{"寿命を持つ参照の検証"}} rust/function_syntax -.-> lab-100414{{"寿命を持つ参照の検証"}} rust/expressions_statements -.-> lab-100414{{"寿命を持つ参照の検証"}} rust/method_syntax -.-> lab-100414{{"寿命を持つ参照の検証"}} end

寿命を持つ参照の検証

寿命は、すでに使っていた別の種類のジェネリックです。型が望む動作を持つことを保証するのではなく、寿命は参照が必要な限り有効であることを保証します。

「参照と借用」では触れていない 1 つの詳細は、Rust のすべての参照には「寿命」があり、それはその参照が有効なスコープです。ほとんどの場合、寿命は暗黙的に推論されます。ほとんどの場合、型も推論されます。複数の型が考えられる場合にのみ型を明示的に指定する必要があります。同様に、参照の寿命がいくつかの異なる方法で関連付けられる場合、寿命を明示的に指定する必要があります。Rust では、実行時に使用される実際の参照が必ず有効であることを保証するために、ジェネリックな寿命パラメータを使用して関係を明示的に指定する必要があります。

寿命を明示的に指定することは、他の多くのプログラミング言語にもあまりありません。だから、これは慣れ親しんだものではないでしょう。この章で寿命についてすべてを網羅するわけではありませんが、寿命構文に遭遇する一般的な方法を説明して、この概念に慣れるのを助けます。

寿命を使ったダングリング参照の防止

寿命の主な目的は、プログラムが参照するべきデータ以外のデータを参照するようにする「ダングリング参照」を防止することです。リスト 10-16 のプログラムを見てみましょう。このプログラムには外側のスコープと内側のスコープがあります。

fn main() {
  1 let r;

    {
      2 let x = 5;
      3 r = &x;
  4 }

  5 println!("r: {r}");
}

リスト 10-16:値のスコープが終了した参照を使用しようとするコード

注:リスト 10-16、10-17、および 10-23 の例では、変数を宣言して初期値を与えていません。したがって、変数名は外側のスコープに存在します。一見すると、これは Rust には null 値がないことと矛盾しているように見えるかもしれません。しかし、値を与える前に変数を使用しようとすると、コンパイル時エラーが発生します。これは、Rust が実際に null 値を許可していないことを示しています。

外側のスコープでは、初期値を持たないrという名前の変数が宣言されています[1]。内側のスコープでは、初期値が5xという名前の変数が宣言されています[2]。内側のスコープ内では、rの値をxへの参照として設定しようとしています[3]。そして内側のスコープが終了します[4]。その後、rの値を表示しようとします[5]。このコードはコンパイルされません。なぜなら、rが参照する値は、使用しようとする前にスコープが終了してしまっているからです。エラーメッセージは次の通りです。

error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

エラーメッセージには、変数xが「十分に長く生存していません」と表示されています。その理由は、xは 7 行目で内側のスコープが終了するときにスコープ外になってしまうからです。一方、rは外側のスコープでは依然として有効です。そのスコープが大きいため、「より長く生存している」と言います。もし Rust がこのコードを動作させるように許可した場合、rxがスコープ外になったときに解放されたメモリを参照してしまい、rを使って行おうとした何らかの操作が正しく機能しなくなります。では、Rust はどのようにしてこのコードが無効であることを判断するのでしょうか?それは借用チェッカーを使っています。

借用チェッカー

Rust コンパイラには、すべての借用が有効であるかどうかを判断するためにスコープを比較する「借用チェッカー」があります。リスト 10-17 は、リスト 10-16 と同じコードですが、変数の寿命を示す注釈付きです。

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

リスト 10-17:それぞれ'a'bと名付けられたrxの寿命の注釈

ここでは、rの寿命を'aで、xの寿命を'bで注釈付けしています。ご覧の通り、内側の'bブロックは外側の'a寿命ブロックよりもはるかに小さいです。コンパイル時に、Rust は 2 つの寿命のサイズを比較し、r'aの寿命を持っていることを確認しますが、それが寿命'bのメモリを参照していることも確認します。このプログラムは拒否されます。なぜなら、'b'aよりも短いからです。つまり、参照の対象は参照自体よりも長く生存しません。

リスト 10-18 では、コードを修正してダングリング参照がなく、コンパイルエラーなくコンパイルできるようにしました。

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

リスト 10-18:データの寿命が参照よりも長いため、有効な参照

ここでは、xが寿命'bを持ち、この場合、'b'aよりも大きいです。これは、rxを参照できることを意味します。なぜなら、Rust はrの参照がxが有効な限り常に有効であることを知っているからです。

これで、参照の寿命がどこにあるか、および Rust が寿命をどのように分析して参照が常に有効であることを保証するかをご理解いただけたので、関数のコンテキストでのパラメータと戻り値のジェネリックな寿命を探ってみましょう。

関数におけるジェネリックな寿命

2 つの文字列スライスのうち、長い方を返す関数を書きます。この関数は 2 つの文字列スライスを受け取り、1 つの文字列スライスを返します。longest関数を実装した後、リスト 10-19 のコードはThe longest string is abcdと表示するはずです。

ファイル名:src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

リスト 10-19:2 つの文字列スライスのうち、長い方を見つけるためにlongest関数を呼び出すmain関数

longest関数には、所有権を持たないように、参照である文字列スライスを引数として渡したいことに注意してください。リスト 10-19 で使用している引数がなぜ必要なのかについては、「引数としての文字列スライス」を参照してください。

リスト 10-20 に示すようにlongest関数を実装しようとすると、コンパイルされません。

ファイル名:src/main.rs

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

リスト 10-20:2 つの文字列スライスのうち、長い方を返すlongest関数の実装例ですが、まだコンパイルできません

代わりに、寿命に関する次のエラーが表示されます。

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

ヘルプメッセージによると、返り値にはジェネリックな寿命パラメータが必要です。なぜなら、Rust は返される参照がxまたはyを参照しているかどうかを判断できないからです。実際、この関数の本体のifブロックはxへの参照を返し、elseブロックはyへの参照を返すため、私たち自身もわかりません!

この関数を定義するとき、この関数に渡される具体的な値はわかりません。したがって、ifの場合とelseの場合のどちらが実行されるかわかりません。また、渡される参照の具体的な寿命もわかりません。したがって、リスト 10-17 と 10-18 のようにスコープを見て、返す参照が常に有効であるかどうかを判断することはできません。借用チェッカーもこれを判断できません。なぜなら、xyの寿命が返り値の寿命とどのように関係しているかを知らないからです。このエラーを修正するには、参照間の関係を定義するジェネリックな寿命パラメータを追加して、借用チェッカーが分析を行えるようにします。

寿命注釈の構文

寿命注釈は、参照の生存期間を変更するものではありません。むしろ、それらは寿命に影響を与えることなく、複数の参照の寿命の相互関係を記述します。関数のシグネチャがジェネリック型パラメータを指定する場合、関数は任意の型を受け付けることができるように、関数はジェネリックな寿命パラメータを指定することにより、任意の寿命の参照を受け付けることができます。

寿命注釈には少し異なる構文があります。寿命パラメータの名前はアポストロフィ(')で始まる必要があり、通常はすべて小文字で、ジェネリック型と同じくらい短いものになります。ほとんどの人は、最初の寿命注釈に'aという名前を使っています。寿命パラメータの注釈は、参照の&の後に配置し、注釈と参照の型を区切るためにスペースを使います。

以下はいくつかの例です。寿命パラメータのないi32への参照、'aという名前の寿命パラメータを持つi32への参照、および同じく'aの寿命を持つi32への可変参照です。

&i32        // 参照
&'a i32     // 明示的な寿命を持つ参照
&'a mut i32 // 明示的な寿命を持つ可変参照

寿命注釈自体だけではあまり意味がありません。なぜなら、注釈は Rust に複数の参照のジェネリックな寿命パラメータがどのように相互関係するかを伝えるためのものだからです。longest関数のコンテキストで、寿命注釈がどのように相互関係するかを見てみましょう。

関数シグネチャにおける寿命注釈

関数シグネチャで寿命注釈を使用するには、関数名とパラメータリストの間の角括弧の中にジェネリックな「寿命」パラメータを宣言する必要があります。これは、ジェネリックな「型」パラメータを宣言する場合と同じです。

シグネチャには次の制約を表現してほしいです。返される参照は、両方のパラメータが有効な限り有効です。これがパラメータの寿命と返り値の寿命の関係です。寿命を'aと名付け、それを各参照に追加します。リスト 10-21 を参照してください。

ファイル名:src/main.rs

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

リスト 10-21:シグネチャ内のすべての参照が同じ寿命'aを持つことを指定するlongest関数の定義

このコードはコンパイルされ、リスト 10-19 のmain関数と一緒に使用したときに期待する結果を生成するはずです。

関数シグネチャは現在、Rust に対して、ある寿命'aに対して、関数は 2 つのパラメータを受け取り、その両方が少なくとも寿命'aと同じ期間生存する文字列スライスであることを伝えています。関数シグネチャはまた、関数から返される文字列スライスが少なくとも寿命'aと同じ期間生存することも Rust に伝えています。実際のところ、これはlongest関数が返す参照の寿命が、関数引数が参照する値の寿命の小さい方と同じであることを意味します。これらの関係は、Rust がこのコードを分析する際に使用するものです。

覚えておいてください。この関数シグネチャで寿命パラメータを指定するとき、渡される値や返される値の寿命を変更するわけではありません。むしろ、借用チェッカーがこれらの制約に準拠しない値を拒否するように指定しているのです。longest関数は、xyがどれくらいの期間生存するかを正確に知る必要はありません。ただ、このシグネチャを満たす'aに置き換えることができるあるスコープがあることだけを知っていればよいのです。

関数の寿命を注釈する場合、注釈は関数シグネチャに入り、関数本体には入りません。寿命注釈は、シグネチャ内の型と同じように、関数の契約の一部になります。関数シグネチャに寿命契約を含めることで、Rust コンパイラが行う分析を簡単にすることができます。関数の注釈方法や呼び出し方法に問題がある場合、コンパイラエラーはコードの部分と制約をより正確に指摘することができます。逆に、Rust コンパイラが寿命の関係をどのように推論するかをもっと行う場合、コンパイラは問題の原因から何段階も離れたコードの使用箇所を指摘できるだけかもしれません。

longestに具体的な参照を渡すとき、'aに置き換えられる具体的な寿命は、xのスコープのうちyのスコープと重なる部分です。言い換えると、ジェネリックな寿命'aは、xyの寿命の小さい方に等しい具体的な寿命を取得します。返される参照に同じ寿命パラメータ'aを付けたので、返される参照もxyの寿命の小さい方の期間だけ有効になります。

次に、異なる具体的な寿命を持つ参照を渡すことで、寿命注釈がlongest関数をどのように制限するかを見てみましょう。リスト 10-22 は簡単な例です。

ファイル名:src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

リスト 10-22:異なる具体的な寿命を持つString値への参照を使ってlongest関数を使用する

この例では、string1は外側のスコープの終了まで有効で、string2は内側のスコープの終了まで有効で、resultは内側のスコープの終了まで有効なものを参照しています。このコードを実行すると、借用チェッカーが承認することがわかります。コンパイルされ、The longest string is long string is longと表示されます。

次に、resultの参照の寿命が 2 つの引数のうち小さい方の寿命でなければならないことを示す例を試してみましょう。result変数の宣言を内側のスコープの外に移動しますが、result変数への値の代入はstring2があるスコープ内に残します。そして、resultを使用するprintln!を内側のスコープの外に、内側のスコープが終了した後に移動します。リスト 10-23 のコードはコンパイルされません。

ファイル名:src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

リスト 10-23:string2がスコープ外になった後にresultを使用しようとする

このコードをコンパイルしようとすると、次のエラーが表示されます。

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value
does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

このエラーは、println!文に対してresultが有効であるためには、string2が外側のスコープの終了まで有効である必要があることを示しています。Rust はこれを知っています。なぜなら、関数のパラメータと返り値の寿命を同じ寿命パラメータ'aを使って注釈付けしたからです。

人間としては、このコードを見るとstring1string2より長いことがわかり、したがって、resultstring1への参照を含むことになります。string1がまだスコープ外になっていないので、string1への参照はprintln!文に対してまだ有効です。しかし、コンパイラはこの場合に参照が有効であることを見ることができません。longest関数が返す参照の寿命が、渡された参照の寿命の小さい方と同じであることを私たちは Rust に伝えています。したがって、借用チェッカーは、リスト 10-23 のコードが無効な参照を持つ可能性があるために許可しません。

longest関数に渡される参照の値や寿命、および返される参照の使用方法を変えるような、さらに多くの実験を試してみてください。コンパイルする前に、実験が借用チェッカーを通過するかどうかについて仮説を立ててみてください。そして、あなたが正しいかどうかを確認してみてください!

寿命について考える

寿命パラメータを指定する必要がある方法は、関数が何を行っているかに依存します。たとえば、longest関数の実装を変更して、常に最初のパラメータを返すようにして、最長の文字列スライスではなくする場合、yパラメータに寿命を指定する必要はありません。次のコードはコンパイルされます。

ファイル名:src/main.rs

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

パラメータxと返り値には寿命パラメータ'aを指定しましたが、パラメータyには指定していません。なぜなら、yの寿命はxの寿命や返り値の寿命とは何の関係もありませんからです。

関数から参照を返す場合、返り値の寿命パラメータは、パラメータのうちの 1 つの寿命パラメータと一致する必要があります。返される参照がパラメータのいずれかを参照しない場合、それはこの関数内で作成された値を参照する必要があります。しかし、これはダングリング参照になります。なぜなら、値は関数の終了時にスコープ外になりますからです。コンパイルされないlongest関数のこのような試みた実装を考えてみましょう。

ファイル名:src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

ここでは、返り値の型に寿命パラメータ'aを指定しましたが、この実装はコンパイルに失敗します。なぜなら、返り値の寿命はパラメータの寿命とまったく関係がないからです。得られるエラーメッセージは次の通りです。

error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the
current function

問題は、resultがスコープ外になり、longest関数の終了時にクリーンアップされることです。また、関数からresultへの参照を返そうとしています。ダングリング参照を変更する寿命パラメータを指定する方法はありません。そして、Rust はダングリング参照を作成させません。この場合、最善の解決策は、返される型を所有型にすることで、呼び出し元の関数に値のクリーンアップの責任を持たせることです。

結局のところ、寿命構文は、関数のさまざまなパラメータと返り値の寿命を結び付けるものです。それらが結び付けられると、Rust はメモリセーフな操作を許可し、ダングリングポインタを作成する操作やそれ以外のメモリセーフ違反を許可しないための十分な情報を持つようになります。

構造体定義における寿命注釈

これまでに定義した構造体はすべて、所有型を保持していました。構造体を参照を保持するように定義することもできますが、その場合、構造体定義内の各参照に寿命注釈を付ける必要があります。リスト 10-24 には、文字列スライスを保持するImportantExcerptという名前の構造体があります。

ファイル名:src/main.rs

1 struct ImportantExcerpt<'a> {
  2 part: &'a str,
}

fn main() {
  3 let novel = String::from(
        "Call me Ishmael. Some years ago..."
    );
  4 let first_sentence = novel
       .split('.')
       .next()
       .expect("Could not find a '.'");
  5 let i = ImportantExcerpt {
        part: first_sentence,
    };
}

リスト 10-24:参照を保持する構造体で、寿命注釈が必要

この構造体には、文字列スライス(参照)を保持する単一のフィールドpartがあります [2]。ジェネリックデータ型と同様に、構造体名の後に角括弧の中にジェネリック寿命パラメータの名前を宣言します。これにより、構造体定義の本体で寿命パラメータを使用できるようになります [1]。この注釈は、ImportantExcerptのインスタンスが、そのpartフィールドに保持する参照よりも長生きすることはできないことを意味します。

ここのmain関数は、ImportantExcerpt構造体のインスタンスを作成します [5]。このインスタンスは、変数novel [3]が所有するStringの最初の文への参照を保持しています [4]。novelのデータは、ImportantExcerptインスタンスが作成される前に存在します。また、novelImportantExcerptがスコープ外になるまでスコープ外になりません。したがって、ImportantExcerptインスタンス内の参照は有効です。

寿命省略

あなたは、すべての参照には寿命があり、参照を使用する関数や構造体には寿命パラメータを指定する必要があることを学びました。しかし、リスト 4-9 にある関数を見てみましょう。これは、リスト 10-25 に再掲されていますが、寿命注釈なしでコンパイルされました。

ファイル名:src/lib.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

リスト 10-25:寿命注釈なしでコンパイルされた、リスト 4-9 で定義した関数。パラメータと返り値の型は参照であるにもかかわらず

この関数が寿命注釈なしでコンパイルされる理由は、歴史的なものです。Rust の初期バージョン(1.0 以前)では、このコードはコンパイルされませんでした。なぜなら、すべての参照に明示的な寿命が必要だったからです。当時、関数のシグネチャはこのように書かれていました。

fn first_word<'a>(s: &'a str) -> &'a str {

多くの Rust コードを書いた後、Rust の開発チームは、Rust のプログラマが特定の状況で同じ寿命注釈を何度も入力していることに気付きました。これらの状況は予測可能で、いくつかの決定論的なパターンに従っていました。開発者たちは、これらのパターンをコンパイラのコードにプログラムしました。その結果、借用チェッカーはこれらの状況での寿命を推論することができ、明示的な注釈が必要なくなりました。

この Rust の歴史は重要です。なぜなら、より多くの決定論的なパターンが現れ、コンパイラに追加される可能性があるからです。将来的には、さらに少ない寿命注釈が必要になるかもしれません。

Rust の参照分析にプログラムされたパターンは、「寿命省略規則」と呼ばれます。これは、プログラマが従う規則ではありません。コンパイラが考慮する特定のケースのセットであり、あなたのコードがこれらのケースに適合する場合、明示的に寿命を書く必要はありません。

省略規則は完全な推論を提供しません。Rust が規則を決定論的に適用しても、参照の寿命が何であるかに関して曖昧さが残る場合、コンパイラは残りの参照の寿命が何であるべきかを推測することはありません。推測する代わりに、コンパイラは寿命注釈を追加することで解決できるエラーを表示します。

関数またはメソッドのパラメータの寿命は「入力寿命」と呼ばれ、返り値の寿命は「出力寿命」と呼ばれます。

明示的な注釈がない場合、コンパイラは 3 つの規則を使って参照の寿命を判断します。最初の規則は入力寿命に適用され、2 番目と 3 番目の規則は出力寿命に適用されます。コンパイラが 3 つの規則の最後まで到達しても、寿命を判断できない参照がまだある場合、コンパイラはエラーで停止します。これらの規則は、fn定義だけでなく、implブロックにも適用されます。

最初の規則は、コンパイラが参照である各パラメータに寿命パラメータを割り当てることです。言い換えると、1 つのパラメータを持つ関数は 1 つの寿命パラメータを取得します。fn foo<'a>(x: &'a i32); 2 つのパラメータを持つ関数は 2 つの別々の寿命パラメータを取得します。fn foo<'a, 'b>(x: &'a i32, y: &'b i32); などです。

2 番目の規則は、入力寿命パラメータが正確に 1 つの場合、その寿命がすべての出力寿命パラメータに割り当てられることです。fn foo<'a>(x: &'a i32) -> &'a i32

3 番目の規則は、入力寿命パラメータが複数ある場合でも、そのうちの 1 つが&selfまたは&mut selfである場合(これはメソッドであるため)、selfの寿命がすべての出力寿命パラメータに割り当てられることです。この 3 番目の規則により、メソッドは読みやすく書きやすくなります。なぜなら、必要なシンボルが少なくなるからです。

コンパイラになってみましょう。リスト 10-25 のfirst_word関数のシグネチャにおける参照の寿命を判断するために、これらの規則を適用します。シグネチャは、参照に関連付けられた寿命がない状態から始まります。

fn first_word(s: &str) -> &str {

そして、コンパイラは最初の規則を適用します。この規則によれば、各パラメータに独自の寿命が割り当てられます。いつものように'aと呼びます。すると、現在のシグネチャはこうなります。

fn first_word<'a>(s: &'a str) -> &str {

2 番目の規則が適用されます。なぜなら、入力寿命が正確に 1 つだからです。2 番目の規則によれば、1 つの入力パラメータの寿命が出力寿命に割り当てられます。したがって、現在のシグネチャはこうなります。

fn first_word<'a>(s: &'a str) -> &'a str {

これで、この関数のシグネチャ内のすべての参照に寿命があり、コンパイラはこの関数のシグネチャ内の寿命を注釈する必要がなく、分析を続けることができます。

もう 1 つの例を見てみましょう。今回は、リスト 10-20 で最初に取り扱ったときに寿命パラメータがなかったlongest関数を使います。

fn longest(x: &str, y: &str) -> &str {

最初の規則を適用しましょう。各パラメータに独自の寿命が割り当てられます。今回は 1 つではなく 2 つのパラメータがあるので、2 つの寿命があります。

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

2 番目の規則は適用されません。なぜなら、入力寿命が 1 つ以上あるからです。3 番目の規則も適用されません。なぜなら、longestは関数であり、メソッドではないからです。したがって、どのパラメータもselfではありません。3 つの規則すべてを適用しても、まだ返り値の寿命が何であるかを判断できません。これが、リスト 10-20 のコードをコンパイルしようとしたときにエラーが発生した理由です。コンパイラは寿命省略規則を適用しましたが、シグネチャ内の参照のすべての寿命を判断することができませんでした。

3 番目の規則は実際には主にメソッドのシグネチャにのみ適用されます。次に、このコンテキストで寿命を見てみましょう。なぜ 3 番目の規則が、メソッドのシグネチャで寿命を頻繁に注釈する必要がないことを意味するのかを理解するためです。

メソッド定義における寿命注釈

寿命を持つ構造体にメソッドを実装する場合、リスト 10-11 に示すジェネリック型パラメータと同じ構文を使用します。寿命パラメータを宣言して使用する場所は、それらが構造体フィールド、またはメソッドパラメータと返り値に関係するかどうかに依存します。

構造体フィールドの寿命名は常に、implキーワードの後に宣言し、その後、構造体名の後で使用する必要があります。なぜなら、それらの寿命は構造体の型の一部だからです。

implブロック内のメソッドシグネチャでは、参照は構造体フィールド内の参照の寿命に関連付けられる場合もあれば、独立している場合もあります。また、寿命省略規則により、メソッドシグネチャに寿命注釈が必要ない場合が多いです。リスト 10-24 で定義したImportantExcerptという名前の構造体を使ったいくつかの例を見てみましょう。

まず、levelという名前のメソッドを使います。このメソッドの唯一のパラメータはselfへの参照で、返り値はi32で、何かへの参照ではありません。

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

implの後の寿命パラメータ宣言と、型名の後でのその使用は必要ですが、最初の省略規則のため、selfへの参照の寿命を注釈する必要はありません。

ここに、3 番目の寿命省略規則が適用される例があります。

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

入力寿命が 2 つあるため、Rust は最初の寿命省略規則を適用し、&selfannouncementの両方に独自の寿命を与えます。その後、パラメータの 1 つが&selfであるため、返り値の型に&selfの寿命が与えられ、すべての寿命が考慮されます。

静的寿命

議論する必要のある 1 つの特別な寿命は'staticです。これは、影響を受ける参照がプログラムの生存期間全体にわたって「生きることができる」ことを示します。すべての文字列リテラルは'static寿命を持ち、次のように注釈することができます。

let s: &'static str = "I have a static lifetime.";

この文字列のテキストは、常に利用可能なプログラムのバイナリに直接格納されます。したがって、すべての文字列リテラルの寿命は'staticです。

エラーメッセージで'static寿命を使用するような提案を見ることがあるかもしれません。しかし、参照の寿命として'staticを指定する前に、実際に持っている参照がプログラムの生存期間全体にわたって生きているかどうか、そしてそれが望ましいかどうかを考えてみてください。ほとんどの場合、'static寿命を示唆するエラーメッセージは、ダングリング参照を作成しようとしたり、利用可能な寿命が一致しなかったりすることから生じます。そのような場合、解決策はそれらの問題を修正することであり、'static寿命を指定することではありません。

ジェネリック型パラメータ、トレイト境界、および寿命の同時使用

1 つの関数でジェネリック型パラメータ、トレイト境界、および寿命を指定する構文を簡単に見てみましょう!

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

これは、2 つの文字列スライスのうち長い方を返す、リスト 10-21 のlongest関数です。しかし今、where句で指定されたDisplayトレイトを実装する任意の型で埋めることができるジェネリック型Tの追加のパラメータannがあります。この追加のパラメータは{}を使って表示されます。これがDisplayトレイト境界が必要な理由です。寿命は一種のジェネリックなので、寿命パラメータ'aとジェネリック型パラメータTの宣言は、関数名の後の角括弧の中の同じリストに入ります。

まとめ

おめでとうございます!寿命を持つ参照の検証の実験を完了しました。LabEx でさらに多くの実験を行って、スキルを向上させることができます。