nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

【Rust】コードで理解する生存期間

はじめに。

Rustの言語機能の核に、所有権、参照、そして生存期間とあります。
個人的に生存期間が特に理解が難しかったので、復習がてらブログを書きます。

生存期間とは?

Rustコンパイラは全ての参照型に対して、その参照の使われ方によって生じた制約を反映した値として生存期間(lifetime)を割り当てる。
参照型というのは、"&" から始まる変数で、任意の変数の所有権移動を伴わずに借用できる変数の型のことを言う。

生存期間の基本

fn main() {
    let r;
    {
        let x = 1;
        r = &x;
    }
    println!("{}", *r);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=12e83a7fae65088e21b06a2a5df29447

例えば上記のコードはコンパイルに失敗する。

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

エラーメッセージを見ると「xは十分長く生存出来ない」「xはまだ借用しているのにここでdropしている」と意訳できる。
このように、

  • 変数は宣言したスコープから外れるとdropしてしまう
  • 借用している最中に借用者が先にdropするとダングリングポインタとなってしまう

と言う2つのルールにより、コンパイルエラーが起きる。
以下のサイトによるとダングリングポインタは、

無効なメモリ領域を指すポインタはダングリングポインタ(dangling pointer)と呼ばれる。とりわけ、本来有効だったメモリ領域が解放処理などによって無効化されたにもかかわらず、そのメモリ領域を参照し続けているポインタのことを、ダングリングポインタと呼ぶ。

ダングリングポインタとは|dangling pointerの危険性と回避 | MaryCore

だそうだ。
この2つのルールを見れば、ここはそこまで難しくなさそうだ。

つまり上記のコードは、xがドロップする前にrを参照すればいい

fn main() {
    let r;
    {
        let x = 1;
        r = &x;
        println!("{}", *r);
    }
}
1

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=20d91c94bf1aae47ce374fdaf47a5e9b

ベクタ型の生存期間

さ、ここからが本番だ。

ベクタの要素が定数

fn main() {
    let v = vec![1, 2, 3];
    {
        let r = &v[0];
        println!("{}", *r);
    }
}
1

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=20d91c94bf1aae47ce374fdaf47a5e9b

参考書によると、「vの生存期間は参照型&v[0]の生存期間を包含してなければならない」、そうだ。要は、rがvより長く生きるのはダメだと言うことだと思われる。

fn main() {
    let r;
    {
        let v = vec![1, 2, 3];
        r = &v[0];
    }
    println!("{}", *r);
}
error[E0597]: `v` does not live long enough
 --> src/main.rs:5:14
  |
5 |         r = &v[0];
  |              ^ borrowed value does not live long enough
6 |     }
  |     - `v` dropped here while still borrowed
7 |     println!("{}", *r);
  |                    -- borrow later used here

error: aborting due to previous error

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9556547d3603510eb055b6778569ae56

vは内側のスコープでライフが切れるので、それを参照しようとするとダングリングポインタ扱いにある。まぁ普通の参照系と変わらなそうだ。

ベクタ内が変数

例えば、ベクタ内が全部参照型とした時、当然ながらベクタ越しに参照しても要素達にも生存期間のルールは適用される。

fn main() {
    let r;
    let v;
    {
        let a = 1;
        let b = 2;
        let c = 3;
        
        v = vec![&a, &b, &c];
        r = &v[0];
    }// ここでa, b, cだけライフが切れる。
    println!("{}", *r);
}
error[E0597]: `a` does not live long enough
  --> src/main.rs:9:18
   |
9  |         v = vec![&a, &b, &c];
   |                  ^^ borrowed value does not live long enough
10 |         r = &v[0];
11 |     }
   |     - `a` dropped here while still borrowed
12 |     println!("{}", *r);
   |                    -- borrow later used here

error[E0597]: `b` does not live long enough
  --> src/main.rs:9:22
   |
9  |         v = vec![&a, &b, &c];
   |                      ^^ borrowed value does not live long enough
10 |         r = &v[0];
11 |     }
   |     - `b` dropped here while still borrowed
12 |     println!("{}", *r);
   |                    -- borrow later used here

error[E0597]: `c` does not live long enough
  --> src/main.rs:9:26
   |
9  |         v = vec![&a, &b, &c];
   |                          ^^ borrowed value does not live long enough
10 |         r = &v[0];
11 |     }
   |     - `c` dropped here while still borrowed
12 |     println!("{}", *r);
   |                    -- borrow later used here

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9da20612ba114da51507d12819a47da4

では、例えばベクタvの方が先にa, b, cより先に生存期間が切れたらどうなるだろう?

fn main() {
    let r;
    let a = 1;
    let b = 2;
    let c = 3;
    {
        let v;
        
        v = vec![&a, &b, &c];
        r = &v[0];
    }// ここでvだけライフが切れる。
    println!("{}", *r);
}
error[E0597]: `v` does not live long enough
  --> src/main.rs:10:14
   |
10 |         r = &v[0];
   |              ^ borrowed value does not live long enough
11 |     }
   |     - `v` dropped here while still borrowed
12 |     println!("{}", *r);
   |                    -- borrow later used here

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=aeaf81a22db484ac7bb32ff9a1472b48

まぁそりゃそうだと言う感じにエラーを吐く。
もちろん、生存期間が切れたものは参照出来ない。

関数が引数として参照型を受け取る場合

ここがつらみ。

普通に引数として

同じようなコードで恐縮だ。
この場合でも、しっかり生存期間は普通に参照型を使った時と変わらない。

fn hoge(r: &usize) {
    println!("{}", *r);
}

fn main() {
    let r;
    {
        let x = 1;
        r = &x;
    }
    hoge(r)
}
error[E0597]: `x` does not live long enough
  --> src/main.rs:9:9
   |
9  |         r = &x;
   |         ^^^^^^ borrowed value does not live long enough
10 |     }
   |     - `x` dropped here while still borrowed
11 |     hoge(r)
   |          - borrow later used here

任意の引数を生存期間とする。

fn hoge<'a>(r: &'a i32) {
    println!("{}", r);
}

fn main() {
    let r;
    {
        let x = 1;
        r = &x;
        hoge(&r);
    }
}
1

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=75d338872efe687f342bfc3055c254fa

関数が引数として渡されたrが、関数呼び出しを超えて生き残る必要が ない 事を示す。
・・・冷静に考えて見れば当然だ。Rustの言語仕様として我々が「関数が受け取った引数はstaticなスコープを保たない限りは関数が閉じた瞬間、呼び出し先からは消される(が、これはつまり暗黙的な関数の挙動なわけだが・・・・)」と学んだはずだ。つまり、この<' a>(tick aと言うらしい)と言うなんとも魔術じみた記号はただの暗黙的約束を明記しただけとなる。

'staticを生存期間に設定する

この場合、いきなりコンパイルが通らなくなる。

fn hoge(r: &'static i32) {
    println!("{}", r);
}

fn main() {
    let r;
    {
        let x = 1;
        r = &x;
        hoge(&r);
    }
}
error[E0597]: `x` does not live long enough
  --> src/main.rs:10:9
   |
10 |         r = &x;
   |         ^^^^^^ borrowed value does not live long enough
11 |         hoge(&r);
   |         -------- argument requires that `x` is borrowed for `'static`
12 |     }
13 | }
   | - `x` dropped here while still borrowed

前段で述べたルールに照らし合わせれば理解できる。この関数hogeは引数に対して、'static、つまりスレッドの終了までの変数の生存期間を要求している。もちろんxはそのような生存期間は持ち合わせていないわけだ。

返り値としての参照

関数に対して生存期間表記がデフォルトで省略されている事がわかった。
しかし返り値はどうだろう?

fn hoge(h: &i32) -> &i32 {
    h
}

fn main() {
    let r;
    {
        let x = 1;
        r = hoge(&x);
    }
    println!("{}", *r);
}
error[E0597]: `x` does not live long enough
  --> src/main.rs:9:18
   |
9  |         r = hoge(&x);
   |                  ^^ borrowed value does not live long enough
10 |     }
   |     - `x` dropped here while still borrowed
11 |     println!("{}", *r);
   |                    -- borrow later used here

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=75d338872efe687f342bfc3055c254fa

また同じようなコード???と思われるだろうが少しまたれたい。
今回の場合はxの参照型を関数hogeに渡しているだけだ・・・まぁいつものごとくコンパイルは通らないわけだが。

生存期間の話から振り返ってみる。
実は関数hogeの暗黙的に宣言されている関数全文はこう書ける。

fn hoge<'a>(h: &'a i32) -> &'a i32 {
    h
}

ここはしっかり頭に入れておきたいのだが、 引数と返り値は同じ生存期間を保たなければいけない と言う事になる。

つまりrと参照元であるxは同じ生存期間でなければならないが、xが先にライフ切れになるからアウト、と言うわけである。

構造体

構造体は、暗黙的に生存期間の解決をやってくれない、と覚える。

構造体内の値の生存期間

構造体の中に参照型を埋め込むと、生存期間をいきなり考慮しなければならない。

struct S {
    r: &i32
}

fn main() {
    let s;
    {
        let x = 10;
        s = S {
            r : &x,
        };
        println!("{:?}", s.r);
    }
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:2:8
  |
2 |     r: &i32
  |        ^ expected lifetime parameter

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=0fdc8f2ec5f09f3d0d111e517c4aca45

なんと、「生存期間を明示しろ」といってくるではないか・・・!あれだけ関数では暗黙的に宣言していたのに・・・😭

つまり以下のように明記してやれば通る。

struct S<'moemoe> { 
    r: &'moemoe i32
}

fn main() {
    let s;
    {
        let x = 10;
        s = S {
            r : &x,
        };
        println!("{:?}", s.r);
    }
}
10

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c0441d2ced1dea5572d63eeddb70db91

ちなみに、生存期間を明記するtickの後の英字は変数のように、任意の英字でいい。
このように構造体の場合は、参照型を埋め込む時は明記しないといけないから注意だ。なんで暗黙的に宣言されないかは僕はよくわかっていない・・・・。

構造体内の構造体の値の生存期間

構造体内に構造体を埋め込む場合は、もちろん宣言する必要がでてくるし、以下のように書く。

struct T<'toretore> {
    t: S<'toretore>
}

struct S<'moemoe> { 
    r: &'moemoe i32
}

fn main() {
    let s;
    {
        let x = 10;
        s = T {
            t : S {
              r : &x  
            },
        };
        println!("{:?}", s.t.r);
    }
}
10

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=801109880ca7bf2303375fd21c0c5206

ネストしたとしてもTの値の生存期間をSから受け継いでるだけで、基本的なルールは変わらない。

構造体内の生存期間の共有

こんな事すんのかって思うが、片方の引数の生存期間を両方の引数に制約をかけれたりもできる。

struct S<'ponyo> {
    x: &'ponyo i32,
    y: &'ponyo i32,
}

fn main() {
    let x = 10;
    let r1;
    {
        let y = 20;
        {
            let s = S {
                x: &x,
                y: &y,
            };
            r1 = s.x;
        }
    }
    println!("x:{:?}", r1);
}
error[E0597]: `y` does not live long enough
  --> src/main.rs:14:20
   |
14 |                 y: &y,
   |                    ^^ borrowed value does not live long enough
...
18 |     }
   |     - `y` dropped here while still borrowed
19 |     println!("x:{:?}", r1);
   |                        -- borrow later used here

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c05d48270c77ea77bde17ccd0a601bc0

一見良さそうだ。yはsでしか参照されていないし、xの生存期間はmain()が終わるまでだし、フォーマットの呼び出しを与えているのはxだけ。しかしyについて生存期間で怒られる。なぜだろう?

1つは「r= s.xの代入により、 'aはrの生存期間を含んでなければならない。」
2つは「r=s.yは&yで初期化されているため、'aはyの生存期間より長い時間をとってはいけない」

となるので、yのライフより短い上に、rより長い生存期間の変数をRustは探すのだが、選択肢にはないのでコンパイルエラーとなる。まぁつまり、先ほど確認した通り変数上では生存期間に問題はなかったように、例に習った引数通りに構造体の生存期間を満たしてやれば良いと言う話になりそうだ。

struct S<'ponyo, 'sousuke> {
    x: &'ponyo i32,
    y: &'sousuke i32,
}

fn main() {
    let x = 10;
    let r1;
    {
        let y = 20;
        {
            let s = S {
                x: &x,
                y: &y,
            };
            r1 = s.x;
        }
    }
    println!("x:{:?}", r1);
}
10

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5830439037bb09e247c28ec6a4dfc164

さて万事これでOKだ。
実際にコードを書く時は新たに構造体を色々な場所で宣言していくと思うのだが、実際には引数に生存期間パラメータを合わせると言うよりかは、徐々に構造体の生存期間の制約を緩めていく方向に書いていくと上手くいき、省エネなコードになりそうだ。

感想

Rustは難しいけど、コンパイラがマジで懇切丁寧に教えてくれるので学習しやすい。

参考文献

www.amazon.co.jp