ライフタイム

Rust は今まで説明してきたルールをライフタイムを使って強制します。 ライフタイムとは、要するにプログラム中のスコープの名前です。 参照と、参照を含むものとは、有効なスコープを示すライフタイムでタグ付けられています。

通常、関数本体では、関係するライフタイムの名前を明示することは求められません。 一般に、ローカルコンテキストにおいてライフタイムを気にする必要はまずないからです。 Rust はすべての情報を持っていて、可能な限りすべてを最適にできます。 省略可能な無名スコープや一時変数は、コードがきちんと動くように自動的に導入されます。

しかし関数の境界を超えると、ライフタイムについて気にしなくてはいけなくなります。 ライフタイムは、'a'static などアポストロフィーつきの名前を持ちます。 ライフタイムの世界に足を踏み入れるために、 スコープにライフタイムのラベルをつけられるとして、この章の最初のサンプルコードを 「脱糖 (desugar)」してみましょう。

もともとのサンプルコードは、スコープとライフタイムについて、 果糖がたくさん含まれたコーンシロップのように強烈に甘い書き方でした。 (訳注:ライフタイムを省略できることは syntax sugar で、元のコードは大量の syntax sugar を使っているので、「甘い」と言っている) なぜなら、すべてを明示的に書くのは極めて煩わしいからです。 Rust のコードは、積極的な推論と「明らかな」ことの省略とを当てにしています。

let 文が、スコープを暗黙的に導入するというのは、興味深いシンタックスシュガーでしょう。 ほとんどの場合、これは問題になりません。 しかし、複数の変数がお互いを参照している場合は問題になります。 簡単な例として、この単純な Rust コードを脱糖してみましょう。


#![allow(unused)]
fn main() {
let x = 0;
let y = &x;
let z = &y;
}

借用チェッカは、ライフタイムの長さを最小にしようとするので、 これは次のように脱糖されるでしょう。

// `'a: {` と `&'b x` は正当な構文ではないことに注意してください!
'a: {
    let x: i32 = 0;
    'b: {
        // ここで使用されるライフタイムは 'b です。なぜなら 'b で十分だからです。
        let y: &'b i32 = &'b x;
        'c: {
            // 'c も同様
            let z: &'c &'b i32 = &'c y;
        }
    }
}

おっと。こんなふうに書かなければいけないとしたら・・・これはひどいですね。 ここでしばらく時間をとって、簡単な構文を許してくれる Rust に感謝を意を表しましょう。

参照を外のスコープに返す場合は、Rust はより大きいライフタイムを推論することになります。


#![allow(unused)]
fn main() {
let x = 0;
let z;
let y = &x;
z = y;
}
'a: {
    let x: i32 = 0;
    'b: {
        let z: &'b i32;
        'c: {
            // ここでは 'b を使う必要があります。なぜならこの参照は
            // スコープ `b に渡されるからです。
            let y: &'b i32 = &'b x;
            z = y;
        }
    }
}

例:参照先より長く生きる参照

それでは、以前に出した例を見てみましょう。

fn as_str(data: &u32) -> &str {
    let s = format!("{}", data);
    &s
}

は次のように脱糖されます。

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s;
    }
}

as_str のシグネチャは、あるライフタイムを持つ u32 への参照をとり、 その参照と同じ長さだけ生きる str への参照を生成することを約束します。 このシグネチャが問題になるかもしれないと、すでに話しました。 このシグネチャは、引数の u32 を指す参照が生成されたスコープか、もしくはそれより以前のスコープで、str を探すことを意味します。これはなかなか難しい注文です。

それから文字列 s を計算し、その参照を返します。 この関数は、返される参照が 'a より長生きすることを約束しているので、この参照のライフタイムとして 'a を使うことを推論します。 残念なことに、s はスコープ 'b の中で定義されているので、 この推論が妥当になるためには、'b'a を含んでいなくてはなりません。 ところがこれは明らかに成立しません。'a はこの関数呼び出しそものを含んでいるからです。 結局、この関数は参照先より長生きする参照を生成してしまいました。 そしてこれは文字通り、参照がやってはいけないことの一番目でした。 コンパイラは正当に怒りだします。

よりわかりやすくするために、この例を拡張してみます。

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s
    }
}

fn main() {
    'c: {
        let x: u32 = 0;
        'd: {
            // この x の借用は、x が有効な全期間より短くて良いので、無名スコープが導入されます。
            // as_str は、この呼び出しより前のどこかにある str を見つけなければいけませんが、
            // そのような str が無いのはあきらかです。
            println!("{}", as_str::<'d>(&'d x));
        }
    }
}

ちくしょう!

この関数を正しく書くと、当然次のようになります。


#![allow(unused)]
fn main() {
fn to_string(data: &u32) -> String {
    format!("{}", data)
}
}

この関数が所有する値を関数内で生成し、それを返さなくてはいけません! str が &'a u32 のフィールドだとしたら、&'a str を返せるのですが、 もちろんそれはありえません。

(そういえば、単に文字列リテラルを返すこともできたかもしれません。 文字列リテラルはグローバルで、スタックの底に存在すると解釈できますから。 ただこれはこの関数の実装をほんの少しだけ制限してしまいますね。)

例:可変参照の別名付け

もう一つの例はどうでしょう。

let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);
'a: {
    let mut data: Vec<i32> = vec![1, 2, 3];
    'b: {
        // スコープ 'b は次の貸し出しに必要なだけ大きくなります。
        // (`println!` を含むまで大きくなります)
        let x: &'b i32 = Index::index::<'b>(&'b data, 0);
        'c: {
            // &mut は長生きする必要が無いので、一時的なスコープ 'c が作られます。
            Vec::push(&'c mut data, 4);
        }
        println!("{}", x);
    }
}

これは、すこし分かりにくいですが面白い問題です。 私たちは、Rust が次のような理由で、このプログラムを拒否するだろうと思っています。 つまり、push するために data への可変参照を取ろうとするとき、 data の子孫への共有参照 x が生存中です。 これは可変参照の別名となり、参照の二番目のルールに違反します。

ところが、Rust がこのプログラムを悪いと推論するやり方は全く違うのです。 Rust は、xdata の部分パスへの参照であることは理解しません。 Rust は Vec のことなど何も知らないのです。 Rust に見えているのは、x は println! のためにスコープ 'b の中で生存しなくてはならないことです。 さらに、Index::index のシグネチャは、data を参照する参照が スコープ 'b の中で生存することを要求します。 push を呼び出すときに、&'c mut data を取ろうとすることを Rust は理解します。 Rust はスコープ 'c が スコープ 'b に含まれていることを知っているので、 このプログラムを拒否します。 なぜなら、&'b data はまだ生きているからです。

ここでは、ライフタイムをチェックするシステムは、私たちが維持したい参照の意味論に比べて とても荒いことを見てきました。 ほとんどの場合、これで全く大丈夫です。 私たちが書いたコードをコンパイラに説明するために丸一日費やさなくてもいいからです。 しかし、ライフタイムのチェックがとてもバカなために、Rust の真の意味論的には全く正しいプログラムでも拒否されることがあるのです。

関連キーワード:  参照, let, data, スコープ, ライフタイム, 関数, コード, プログラム, mut, 推論