Charaken 技術系ブログ

技術系に関して学んだことや、気になったことを記事にしていく。

【Rust】がばがばRust独学 - 10. Generic Types/Traits/Lifetimes - 3 Lifetimes

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回も、Generic Typesについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


Lifetimes

Rustの特徴的なownershipのLifetimesに関してになります。

Validating References with Lifetimes - The Rust Programming Language

ぶら下がり参照の防止

公式ドキュメントのコードより、下記を実行すると、 {} ブロック内部で x のスコープが外れます。

そのため、x が開放されて参照先を無くした r はエラーとなるべきですので、エラーとして

x dropped here while still borrowed

となります。

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

実際のスコープは Validating References with Lifetimes - The Rust Programming Language

公式ドキュメントのボローチェッカー(Validating References with Lifetimes - The Rust Programming Language)がとても分かりやすいです。

関数の一般的な寿命

関数も同様に式(ブロック)内部でのみのLifetimeとなります。

そのため、公式ドキュメントのデモコードのように、longest(x: &str, y: &str) -> &str とした場合、longest(string1.as_str(), string2) における string1.as_str() および string2

  • どちらが選択されるか返り値(-> &str)から分からない
  • &x&y の具体的なLifetimeが longest 関数から判断が付かない

であるため、コンパイラからの判断が出来ず、エラーとなります。

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

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
error[E0106]: missing lifetime specifier
 --> src/demo.rs:8:33
  |
8 | fn longest(x: &str, y: &str) -> &str {
  |                                 ^ expected 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`

Lifetime Annotation Syntax

参照の有効期間を変更させないために、アノテーション ' を利用することが出来ます。

デモコードより、関数で利用する場合、 longest 関数において、 xystr 全てのライフタイムが同一でなければならない状態とした場合、 xy どちらが来たとしても、コンパイラから -> &str のLifetimeは必ず xy と同様であることが分かるため、Lifetimeによるエラーはなくなります。

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

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

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

また、下記のように異なるLifetime( longest(string1.as_str(), string2.as_str()) )を渡そうとした場合、 string1string2 よりの長いLifetimeであり、 result のLifetimeは string2 と同一であるためエラーとはなりません。

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);
    }
}

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

しかし、下記のように、 result のLifetimeを string1 と同様にした場合、resultstring2 のLifetime外となってしまうパターンがあるため、エラーとなります。

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);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
error[E0597]: `string2` does not live long enough
 --> src/demo.rs:6:38
  |
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

Lifetime Elision

全てのリファレンスにはLifetimeが存在しますが、アノテーション無しでコンパイルできるパターンが存在します。

fn main() {
    let s = "Hello Rust!";
    let fw = first_word(&s);
    println!("first word -> {}", fw);
}

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[..]
}
first word -> Hello

公式ドキュメント(Validating References with Lifetimes - The Rust Programming Language)で背景がしっかりと記載されていますが、ざっくりまとめると以下になります。

  • Rustの初期バージョン(1.0以前)は対応していなかった
  • 同一パターンでアノテーションしていたので、特定パターンを省略可能にした

    • 省略できるが、完全な推論ではない
  • 関数におけるLifetimeパラメータ名

    • 引数: input lifetime parameter
    • 返り値: output lifetime parameter
  • 3つルールで推論できる場合、省略可能

    • ルール1:input lifetime parameter が独自のLifetimeを持つ
      • 例1:fn foo<'a>(x: &'a i32)
      • 例2:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
    • ルール2:input lifetime parameter が1つだけの場合は output lifetime parameter は同一Lifetimeになる
      • 例: fn foo<'a>(x: &'a i32) -> &'a i32
    • ルール3:input lifetime parameter が複数の場合はそのうちの一つが &self&mut self 、 関数のどれかである場合

上記のルールより、 first_word(s: &str) -> &str はルール1、2を満たし、ルール3は適応外のため省略可能となります。

また、 impl に対してもルール1、3を満たし、ルール2は適応外のため省略可能となります。

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

The Static Lifetime

'static は特別なLifetimeであり、Lifetimeはプログラムの全期間で存続可能です。

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

上記を利用を意識した場合はLifetimeが不要なのか確認する必要があります。長期間保持する必要があるのかどうか・・・

最後に

Lifetimeは省略系を除けば、Ownershipの深堀り的な内容ですね!

charaken.hatenablog.com

Stringusize のLifetimeに気をつけたりすれば、かなり有効的に使えますね。

体に染み込ませるまでに時間がかかりそうではありますが、使いこなせるようになって、Rustのメリットを活かしたいですね。

次はテスト。DevOpsでも重要になってくるテストですね。