rustのライフタイムについて理解する

rust

この記事について

この記事ではrustのライフタイムについて、学んだことをまとめています。

ライフタイムを理解するには、「参照」と「ジェネリクス」について理解している必要があります。「参照」については別に記事を書いていますので、こちらをご参照ください。「ジェネリクス」についても、別に記事を書いていますので、こちらをご参照ください。

ライフタイムとは

ライフタイムとは、「参照が有効な期間」のことです。rustでは参照元の値がなくなってしまうこと(ダングリング)をコンパイル時に発見してくれます。この時に、参照元の変数がどこまで生きているかが重要になってきます。参照元変数が生きている=参照が有効と判断します。rustでは、全参照がライフタイムを持っています。参照が持っているライフタイムはライフタイムパラメータで表されます。

ライフタイムパラメータが必要なとき

基本的にはライフタイムパラメータは使う必要がありません。なぜならrustのコンパイラが判定してくれるからです。ライフパラメータが必要になるのは、参照から新たな参照を作る時です。

例えば、以下のように2つの参照から、新たに参照を返す関数を考えてみます。このコードはコンパイルできません。

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

longest関数は2つの参照を受け取ります。これらの参照はライフタイムを持っています。そして返す参照もライフタイムを持っています。問題は「新しく作り出した参照のライフタイムは何にするか」ということです。

それぞれの変数のライフタイムについて考えるため、以下のコードを例にしてみます。

let a = "hogehoge".to_string();
let mut c = "";
{
    let b = "hoge".to_string();
    c = longest(&a, &b);
    println!("{}", c);
}

このコードでは、longest関数に入ってくる参照のライフタイムが異なっています。図にしてみると以下のようになっています。

aのライフタイムを’aで、bのライフタイムを’bで表しています。新たに作り出す参照cのライフタイムは「どちらか短い方」にする必要があります。そうしなければダングリングが発生してしまいます。このような時、関数にライフタイムパラメータを追加して、ライフタイムを指定してあげる必要があります。ライフタイムパラメータを追加した関数は以下のようになります。

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

‘aがライフタイムパラメータです。ライフタイムパラメータは一種のジェネリクスです。ジェネリクスなので、’aは「任意のライフパラメータ」の意味です。しかし、2つの引数それぞれのライフタイムは同じになっています。つまり、この関数は「同じライフタイムを持つ2つの変数」を引数とする関数です。このように指定した場合、引数のライフタムパラメータは短い方に合わせられます(そうでないとダングリングが発生するので)。これで、cのライフタイムはb変数が持つものと同じになります。

例えば、println!をbを定義したスコープの外にしてみます。するとこれはコンパイルエラーになります。

let a = "hogehoge".to_string();
let mut c = "";
{
    let b = "hoge".to_string();
    c = longest(&a, &b);
}
println!("{}", c);

cのライフタイムはbと同じなので、bを定義したスコープ内でしか生きられません。

ライフタイム省略

基本的に参照は全て

省略できるパターンは以下の2つです。

  • 引数が1つの場合
  • ライフタイムを持つ構造体のself引数を含むメソッドの場合

引数が1つの場合

引数が1つのみの場合、戻り値も引数と同じライフタイムにします。

fn first_str(a: &str) -> &str {
    &a[0..1]
}
fn main(){
    let first_str = first_str("hoge");
    println!("{}",first_str);
}

実行結果

h

ライフタイムを持つ構造体のself引数を含むメソッドの場合

ライフタイムを持つ構造体で、selfを含むメソッドの場合、selfと戻り値のライフタイムを省略することができます。selfと戻り値のライフタイムは構造体が持つライフタイムに合わせられます。

// Ok
struct Hoge<'a> {
    s: &'a str,
}

impl<'a> Hoge<'a> {
    fn first_str(&self, b: &str) -> &str {
        &self.s[0..1]
    }
}

以下の例だとコンパイルエラーになります。上との違いはaの参照から戻り値を作ったことです。bの参照から作成したので、ライフタイムはaと同じになりますが、戻り値を省略しているため、戻り値は構造体と同じものを期待しています。よって、コンパイルさせてくれません。

// Err
struct Hoge<'a> {
    s: &'a str,
}

impl<'a> Hoge<'a> {
    fn first_str(&self, b: &str) -> &str {
        &b[0..1]
    }
}

このコードをコンパイルできるようにするためには、ライフタイムパラメータを指定する必要があります。

例えば、戻り値のライフタイムをbのものにしたい場合、以下のようになります。

// Ok
struct Hoge<'a> {
    s: &'a str,
}

impl<'a,'b> Hoge<'a> {
    fn first_str(&self, b: &'b str) -> &'b str {
        &b[0..1]
    }
}

構造体のライフタイムに合わせたい場合は以下のようになります。b変数のライフタイムを構造体のライフタイムに合わせることで、全ての引数、戻り値のライフタイムが’aになります。

// Ok
struct Hoge<'a> {
    s: &'a str,
}

impl<'a> Hoge<'a> {
    fn first_str(&self, b: &'a str) -> &str {
        &b[0..1]
    }
}

静的ライフパラメータ

ライフパラメータに’staticと書いた場合、静的ライフパラメータになります。これは、プログラムの全期間で生存できることを示しています。以下に例を示します。

fn main(){
    let mut d = "";
    {
        let a = "foo";
        d = &a;
    }
    println!("{}", d);
}

実行結果

foo

rustは変数に直接、文字のリテラルを束縛するときは’staticライフタイムを与えます。{}の中でaに”foo”を束縛しています。その参照をdに代入します。ここで、dはaのアドレスを持ちます。この{}を抜けると、aはスコープから外れるので、dropされます。この時、”foo”は静的ライフパラメータを持つので、値は破棄されません。

aに&str型ではなく、String型を束縛してみると、{}を出る際にaとともに”foo”も破棄されてしまいます。よって、以下のコードはコンパイルできません。

fn main(){
    let mut d = "";
    {
        let a = "foo".to_string();
        d = &a;
    }
    println!("{}", d); // not allowed
}

コメント

タイトルとURLをコピーしました