0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustのライフタイムを理解したい

Last updated at Posted at 2024-12-14

はじめに

超個人的な感想だけど、Rustでちょっと凝ったことをしようとするとぶつかる壁がライフタイム。
公式のライフタイムで参照を検証するを熟読すればそれだけで済むことを記事にする。

重ねて書くけど、ライフタイムで参照を検証するを熟読している方は本記事から得るものはない。

個人的には、コンパイラに頼らなくても何とかなるところまで持っていきたい。

本記事

基本の関数シグネチャ

公式に記載のある通り

  • ライフタイムとは、参照が有効になるスコープ
  • 参照は全てライフタイムを保持する

なので、コンパイラがライフタイムをどうだこうだ言ってくる = 参照のスコープがよく分からんことになっている 状態。

公式にもジャストの例が載っていて、

fn main() {
    let str1 = String::from("エリザベス");
    let str2 = "モリザベス";
    let result = longest(str1.as_str(), str2);
    println!("{}", result);
}

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

↑を実行しようとするとちゃんとライフタイムのエラーが出る。引数が参照で複数なのでどっちのライフタイムに合わせればいいかわからん状態。

  |
8 | 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
  |
8 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

エラーの通り修正すると実行可能になる

fn main() {
    let str1 = String::from("エリザベス");
    let str2 = "モリザベス";
    let result = longest(str1.as_str(), str2);
    println!("{}", result);
}

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

ここで使ってる<'a>とか'aとかがライフタイム注釈記法と呼ばれるもの。

// <'a>はライフタイムの宣言
// パラメタとか戻り値についてる'aがライフタイム指定
// パラメタのライフタイムは入力ライフタイム、戻り値のライフタイムが出力ライフタイムと呼ばれる
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

↑の意味は、引数のx, yのどちらか小さい方のスコープを'aとして関数のスコープとすると言うもの。ちなみに、ライフタイム注釈は小文字の連続なら何でもよい。frierenでも大丈夫。仕事で使ったら怒られるだろうけどRust的には問題ない。まず実装でお目にかかることはないだろうけど、アンダースコアも使える。frieren_and_fernとかでも問題ない(はず)。

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

ライフタイム注釈には3つの規則がある

1 - 各引数は、独自のライフタイム引数を得る
2 - 入力ライフタイム引数が一つなら出力ライフタイム引数も同じになる
3 - &selfとか&mut self使ったら&selfのライフタイムになる(構造体のメソッドのやつ)

以下が例

 fn main() {
    let a = 50;
    let b = 51;
    let c = 49;
    let z1 = process01(&a, &b, &c);
    println!("z1: {}", z1);
    let z2 = process02(&a, &b, &c);
    println!("z2: {}", z2);
    let z3 = process03(&a);
    println!("z3: {}", z3);
    let z4 = process04(&a);
    println!("z4: {}", z4);
 }

// 各引数は独自のライフタイムを得る(戻り値を&i32へすると、ライフタイム指定が必要になる)
 fn process01<'izen, 'himmel, 'frieren>(x: &'izen i32, y: &'himmel i32, z: &'frieren i32) -> i32 {
    *x + *y + *z 
 }

 fn process02<'frieren>(x: &'frieren i32, y: &'frieren i32, z: &'frieren i32) -> &'frieren i32 {
    x
 }

 // 入力ライフタイム引数が一つなら出力ライフタイム引数も同じになる
 fn process03(x: &i32) -> &i32 {
    x
 }

 fn process04<'frieren>(x: &'frieren i32) -> &'frieren i32 {
    x
 }

構造体シグネチャ

先にあった通り、参照は全てライフタイムを保持するなので参照を持つ構造体はライフタイムの指定が必要。

特に参照を持たない場合はライフタイム関係なく使える。平和な世界。

#[derive(Debug)]
struct Easygoing {
    feel: bool,
}

impl Easygoing {
    fn feeling() -> bool {
        true
    }

    fn feeling_self(&self) -> bool {
        self.feel || false
    }
}

fn main() {
    let easygoing = Easygoing {
        feel: true,
    };

    println!("{:?}", &easygoing);
    println!("feeling: {}", Easygoing::feeling());
    println!("feeling self: {}", easygoing.feeling_self());
}

参照が入ってくると少しややこしくなるが、こちらもジャストな例が公式に載ってる。

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

#[derive(Debug)]
struct ImportantExcerpt<'a> {
    part: &'a str,
}

ちなみにライフタイム注釈を外すと以下のようになる

error[E0106]: missing lifetime specifier
  --> src/main.rs:30:11
   |
30 |     part: &str,
   |           ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
29 ~ struct ImportantExcerpt<'a> {
30 ~     part: &'a str,
   |

怒られている内容としては構造体に保持している参照のライフタイムが謎やぞ。なので、構造体のライフタイムと合わせる必要がある(*1)。
ちなみにstruct ImportantExcerpt<'a><'a>はジェネリックなライフタイム指定。次で出てくるimplに関連する。

ライフタイムを指定したImportantExcerptにimplを単純に指定すると

#[derive(Debug)]
struct ImportantExcerpt<'a> {
    part: &'a str,
}

// ↓追加
impl ImportantExcerpt {
    fn level() -> i32 {
        3
    }
    fn level_self(&self) -> i32 {
        3
    }
    fn get_part(&self) -> &str {
        self.part
    }
}

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

下記エラーが出る

  --> src/main.rs:26:6
   |
26 | impl ImportantExcerpt {
   |      ^^^^^^^^^^^^^^^^ expected lifetime parameter
   |
help: indicate the anonymous lifetime
   |
26 | impl ImportantExcerpt<'_> {
   |                      ++++

これは、参照を持つ構造体に合わせたライフタイム指定が必要なため。匿名ライフタイムの指定でも回避可能だし、ライフタイムを明示的に指定しても回避可能。

#[derive(Debug)]
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'frieren> ImportantExcerpt<'frieren> {
    fn level() -> i32 {
        3
    }
    fn level_self(&self) -> i32 {
        3
    }
    fn get_part(&self) -> &str {
        self.part
    }
}

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

implのライフタイム指定がややこしい。impl<'frieren> ImportantExcerpt<'frieren>に関して軽く補足すると
1回目の<'frieren> -> ライフタイムの定義
2回目の<'frieren> -> ImportantExceprtのジェネリックに'freirenを割り当て

みたいな感じと理解している。定義と割り当てで同じ感じだからややこしいけど意味合いが別(*2)。

*1: 個人的には構造体の範囲を超えて使うことないからいいじゃんって思ってる
*2: 巷の例でもだいたいが'aで書いてあるから余計に見分けつきづらい

ライフタイム境界

いまだに理解できてるのかが怪しい項目。使わなきゃならないユースケースにぶつかった事がない。
公式のRust By Example 日本語版 - ライフタイム境界を読んですぐ何のことか理解できる人は多くないと思いたい。

ジェネリック型に境界(bound)を与え、特定のトレイトを実装していることを保証できるのと同様、ライフタイム(それ自身ジェネリック型)にも境界を与えることができます。

自分なりの理解として、↑を順序立てて説明してみる。

まず、大前提として構造体はジェネリックが使える。

#[derive(Debug)]
struct Buffer<T> {
    value: T,
}

pub fn run_lifetime_bound() {
    let value = 100;
    let buffer = Buffer{
        value: value,
    };
    println!("{:?}", buffer);
}

構造体のジェネリックも参照型が使える。勿論ライフタイムの指定が必要。

#[derive(Debug)]
struct Buffer<'a, T> {
    value: &'a T,
}

pub fn run_lifetime_bound() {
    let value = 100;
    let buffer = Buffer { value: &value };
    println!("{:?}", buffer);
}

構造体なのでトレイトを実装できる。(printとかで利用する場合はジェネリックにDebugトレイトが実装されていることを明示する必要があることに注意)

use std::fmt::Debug;

trait BufferBehavior<T> {
    fn buffer(&self)
    where
        T: Debug,
    {
    }
}

#[derive(Debug)]
struct Buffer<'a, T> {
    value: &'a T,
}

impl<'buffer_life, T> BufferBehavior<T> for Buffer<'buffer_life, T> {
    fn buffer(&self)
    where
        T: Debug,
    {
        println!("BufferBehavior: {:?}", self.value);
    }
}

pub fn run_lifetime_bound() {
    let value = 100;
    let buffer = Buffer { value: &value };
    println!("{:?}", buffer);
    buffer.buffer();
}

ここで、トレイト自体にジェネリックの参照を返す処理を定義する。

use std::fmt::Debug;

trait BufferBehavior<T> {
    fn buffer(&self)
    where
        T: Debug,
    {
    }
    fn get_value(&self) -> &T;
}

#[derive(Debug)]
struct Buffer<'a, T> {
    value: &'a T,
}

impl<'buffer_life, T> BufferBehavior<T> for Buffer<'buffer_life, T> {
    fn buffer(&self)
    where
        T: Debug,
    {
        println!("BufferBehavior: {:?}", self.value);
    }

    fn get_value(&self) -> &T {
        self.value
    }
}

pub fn run_lifetime_bound() {
    let value = 100;
    let buffer = Buffer { value: &value };
    println!("{:?}", buffer);
    buffer.buffer();
    println!("buffer value: {:?}", buffer.get_value());
}

トレイト内の参照にライフタイムをつけられる! <- ライフタイム境界

use std::fmt::Debug;

trait BufferBehavior<'a, T: 'a> {
    fn buffer(&self)
    where
        T: Debug,
    {
    }
    fn get_value(&self) -> &'a T;
}

#[derive(Debug)]
struct Buffer<'a, T> {
    value: &'a T,
}

impl<'buffer_life, T> BufferBehavior<'buffer_life, T> for Buffer<'buffer_life, T> {
    fn buffer(&self)
    where
        T: Debug,
    {
        println!("BufferBehavior: {:?}", self.value);
    }

    fn get_value(&self) -> &'buffer_life T {
        self.value
    }
}

pub fn run_lifetime_bound() {
    let value = 100;
    let buffer = Buffer { value: &value };
    println!("{:?}", buffer);
    buffer.buffer();
    println!("buffer value: {:?}", buffer.get_value());
}

これなんに使うんだか

まとめ

ライフタイムの学習にいまいち身が入らないのは、ジェネリックとかとごっちゃになりやすいからと思う。
ライフタイム境界は未だに謎。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?