🦍

Rustのライフタイム推論入門

2022/12/18に公開約2,900字2件のコメント

はじめに

「ライフタイムなんもわからん」ので、勉強した内容を整理しました。
Rust初心者なので、間違いがあったらコメントにて指摘頂けるとうれぴっぴです。

ライフタイムのおさらい

Rustは全ての参照にライフタイムを持ちます。
明示的にライフタイムを指定する場合は'aのような、ライフタイムパラメータを使用する必要があります。

しかし、全ての参照にライフタイムパラメータを書くのは手間がかかるので、
一定のルールに沿ってライフタイムを推論し、ルール外のものはコンパイルエラー、という仕様になっています。

ライフタイムパラメータを書ける場所は「ライフタイムポジション」と呼ばれ、以下の場所で書くことができます。

&'a T
&'a mut T
T<'a>

それぞれ簡単なコードで示すと次のような感じになります。

// 変数の束縛
let a = &'a b;
let a = &'a mut b;

// 引数と戻り値
fn foo<'a>(arg: &'a str) -> &'a str

関数の場合、引数を入力、戻り値を出力と考えることができます。

省略ルール

rust-nomicon-jaによると、以下のルールがある。

  1. 入力ポジションの省略されたライフタイムは、それぞれ別のライフタイムパラメータになります。
  2. 入力ポジションのライフタイム(省略されているかどうかに関わらず)が一つしか無い場合、 省略された出力ライフタイム全てにそのライフタイムが割り当てられます。
  3. 入力ポジションに複数のライフタイムがあって、そのうちの一つが &self または &mut self の場合、 省略された出力ライフタイム全てに self のライフタイムが割り当てられます。

それぞれ、コードとともにもう少し噛み砕いていきます。

  1. 入力ポジションの省略されたライフタイムは、それぞれ別のライフタイムパラメータになります。

要は複数の引数がある場合、それぞれの引数のライフタイムが異なる、ということです。

fn hoge(a: &i32, b: &i32)
// ↑は↓に推論される
fn hoge<'a, 'b>(a: &'a i32, b: &'b i32)
  1. 入力ポジションのライフタイム(省略されているかどうかに関わらず)が一つしか無い場合、 省略された出力ライフタイム全てにそのライフタイムが割り当てられます。

要は引数が1つのみで、戻り値がある場合は、引数のライフタイムが戻り値のライフタイムが同じになる、とうことです。

fn get_ref(data: &u32) -> &u32 { data }
// ↑は↓に推論される
fn get_ref<'a>(data: &'a u32) -> &'a u32 { data }
  1. 入力ポジションに複数のライフタイムがあって、そのうちの一つが &self または &mut self の場合、 省略された出力ライフタイム全てに self のライフタイムが割り当てられます。

要は&self、つまり構造体などと同じライフタイムになる、ということですね。

fn do_some(&mut self, arg: &str) -> &mut T;
// ↑が↓に推論される
fn do_some<'a>(&'a mut self, arg: &str) -> &'a mut T; 

なぜその省略ルールなのか

省略ルールについて分かったので、次はそれぞれ「なぜそのルールなのか」について考えてみます。
RFCを読んでも、解説されていなかった(英語わからんので多分そう)ので、考察内容になります。
間違った解釈をしている可能性が高いので、もしおかしなところがあれば教えて頂けると喜びの歌を歌います。

  1. 入力ポジションの省略されたライフタイムは、それぞれ別のライフタイムパラメータになります。

関数呼び出しをインライン化するとわかりやすいですが、ブロックスコープ内のabはそれぞれ宣言しています。
変数の束縛するタイミングが異なるので、自ずとライフタイムが異なります。
このように「自然とそうなる」ということかと思います。

let arg = "a";
hoge(&arg, &arg);

// 以下のコードと同等
let arg = "a";
{
    let a: &str = &arg;
    let b: &str = &arg;
};
  1. 入力ポジションのライフタイム(省略されているかどうかに関わらず)が一つしか無い場合、 省略された出力ライフタイム全てにそのライフタイムが割り当てられます。
  • 関数が参照を受け取る = 関数が引数の生存期間に依存する = 引数は関数よりも長生きする
  • 関数が引数の生存期間に依存する = 関数の出力も引数の生存期間に依存するということになる

と考えると自然のような気がします。
「入力を受け取って何かしらを出力する関数」は「関数とその出力は入力に依存している」ことになるので、
依存先の生存期間に合わせるのが自然、ということかと思います。

余談ですが、複数個の参照を受け取る関数だと、ライフタイムパラメータを明示しない限りコンパイルエラーになるのも、
上記の思考をすると、「どちらの入力に依存すればいいのかわからないから明示してね」ということかなと思います。

fn foo(a: &str, b: &str) -> &str { .. }
  1. 入力ポジションに複数のライフタイムがあって、そのうちの一つが &self または &mut self の場合、 省略された出力ライフタイム全てに self のライフタイムが割り当てられます。

これは2の考え方と同じかと思います。
&selfがあるということは、構造体に依存した関数ということになります。

構造体に依存した関数は、構造体と同じ生存期間になります。
なので関数の出力も同様に構造体の生存期間に依存する、ということかと思います。

実際には、次のような入力や構造体に依存しない、値への参照を出力することも可能ですが、
そこまでコンパイラに頑張ってもらうのはコストに見合わないというトレードオフも考えてのルールではないかなって想像しています。(多分)

fn foo(&self, arg: &str) -> &str {
  "foo"
}

最後に

ライフタイム、ナニモワカラナイ。

Discussion

そこまでコンパイラに頑張ってもらうのはコストに見合わないというトレードオフも考えてのルールではないかな

コンパイルの負担が増えるのもあるかもしれませんが、戻り値のライフタイムが関数の実装に依存するのではなくシグネチャのみから決定されないと、関数を使う人が不便だからという理由もあるかと思います。

関数を使う人が不便だからという理由もあるかと思います

なるほど、
使う側がシグネチャから生存期間を確認できないので、不便ということでしょうか?
確かに不便そうですね

ログインするとコメントできます