Chapter 14

参照とライフタイム

参照

変数は,メモのようなものだと言いました.

たとえば

fn main() {
    let hoge: i8 = 100;
    println!("{}", hoge);
}

というコードを見てみましょう.変数 hoge の型は i8,そこに代入された値は 100 です. i8 のバイト長は 1 なので, hoge のために 1 バイト分のメモリが使われています.

プログラムが使用するメモリは, 1 バイト分のメモがたくさん並んだようなものです. let hoge: i8 = 100; と書くと,たくさんのメモの中のどれか一つに hoge という名前が付けられて, 100 という値が書き込まれます.

実は,これらのメモには全て番号が振られています.これを変数のアドレスといいます.

どのメモが使われるかは,プログラム実行時に決められます.プログラマーが変数のアドレスを決定する普通の手段は存在しません.

しかし,変数のアドレスを知る手段は存在します.

fn main() {
    let hoge: i8 = 100;
    let reference = &hoge;
    println!("{:p}", reference);
}

hoge の前に & を付けたものは,変数 hoge のアドレスを表します.これを変数 reference に代入したので, reference の値は hoge のアドレスになります.

println! マクロのフォーマット文字列で {:p} とすると,アドレスが 16 進数で出力されます.よって,出力は(たとえば) 0x7ffce01ce3af (=140720556394719)のようになります(先頭の 0x は 16 進法を意味します).この値は実行するたびに変わり得ます.

& を付けることで得られる変数 hoge のアドレスを,hoge への参照といいます.& を付けて hoge への参照をとることを, hoge借用するといいます.

参照 &hoge&i8 という型をもちます.一般に,T 型の変数への参照は &T 型になります.同じアドレスでも, &i8&u8 のように T が異なるものは別の型です.

fn main() {
    let hoge: i8 = 100;
    let reference: &i8 = &hoge;
    println!("{:p}", reference);
}

また,未初期化の変数を借用することはできません.

fn main() {
    let hoge: i8;
    let reference = &hoge;
}
error[E0381]: borrow of possibly-uninitialized variable: `hoge`
 --> src/main.rs:3:21
  |
3 |     let reference = &hoge;
  |                     ^^^^^ use of possibly-uninitialized `hoge`

borrow of possibly-uninitialized variable: `hoge` は「未初期化の可能性のある変数 hoge を借用しようとしている」という意味です.

ここまでの例で,変数 hogei8 という 1 バイトの型でした.もし hogei32 (4 バイト)や [i32; 10] (40 バイト)のように 1 バイトより大きい型だったら,図のようにメモリ上の連続した領域が使われ, & を付けて得られるのはその先頭のアドレスになります.

参照外し

reference には,変数 hoge のアドレスが入っています.であれば, reference に入っている値さえ分かっていれば,メモリ上でそのアドレスを見にゆくことで変数 hoge の値を間接的に知ることもできるでしょう.これを参照外しといい, reference の前に * 演算子を付けることでできます.

fn main() {
    let hoge: i8 = 100;
    let reference = &hoge;
    assert_eq!(*reference, 100_i8);
}

*reference は,メモリ上でアドレスが reference の場所に入っている値です.今回は reference が変数 hoge のアドレスなので, *referencehoge の値,すなわち 100 になります.

型推論

型推論は,参照/参照外しを越えて行われます.上のコードで, hoge の型注釈を外して

fn main() {
    let hoge = 100;
    let reference = &hoge;
    assert_eq!(*reference, 100_i8);
}

とすると,「*reference100_i8 が比較されているので reference の型は &i8 でなくてはならない」「&hogereference に代入しているので hoge の型は i8 でなくてはならない」という推論が働いて,変数 hoge と整数リテラル 100i8 型に推論されます.

演算子の参照外し

+ - * / % のような算術演算子は,参照外しをしなくても使うことができます[1].次のコードを見てください.

fn main() {
    let hoge: i8 = 100;
    let reference = &hoge;
    assert_eq!(reference + 1_i8, 101_i8);
}

reference + 1_i8 という式の中で, reference の型は &i8 である一方, 1_i8 の型は i8 です. + 演算子は,左辺や右辺に &i8 のような参照型の変数があると,参照外しをしてから和を計算します.すなわちこれは,

fn main() {
    let hoge: i8 = 100;
    let reference = &hoge;
    assert_eq!(*reference + 1_i8, 101_i8);
}

と書いているのと同じです.

println! による出力

println! マクロのフォーマット文字列では, { } の中身を変えることで様々な出力形式を選択できました.しかし,参照型でない i32f64{:p} で出力することはできません.

fn main() {
    println!("{:p}", 100); // エラー
}

一方,参照型を {:p} 以外で出力することは可能です.

fn main() {
    let hoge = 100;
    let reference = &hoge;
    println!("{}", reference);
}

reference の参照が外されて, *reference の値が出力されます.よって出力は 100 となります.

今まで, x.sin()x.rem_euclid(y) のように,変数 x の後にピリオド . を置いてそのあとに sin()rem_euclid(y) などと続けるような書き方がいくつか登場しました.これは後の章で説明する関数呼び出しの形の一つで,このときも x は自動的に参照外しされることがあります.たとえば, x の型が &f64 のとき, x.sin()(*x).sin() という意味になります.これを暗黙の参照外しといいます.

パターンマッチ

参照もパターンマッチが可能です.

fn main() {
    let hoge = 10;
    let reference = &hoge;
    let &copied = reference;
    assert_eq!(copied, 10);
    println!("hoge:   {:p}", &hoge);
    println!("copied: {:p}", &copied);
}

let &copied = reference; の行で,参照形式のパターンを使っています.= の右辺が &i32 型なので, copiedi32 型になり,そこには hoge の値が代入されます.

copied の値は hoge と同じ 10 ですが, hogecopied のアドレスをそれぞれ出力してみると異なる値になっていることが分かるはずです.

ライフタイム

あるブロックの中で宣言された変数のスコープは,そのブロックが終わるまででした.

fn main() {
    {
        let hoge = 100;
    }
    // ここで hoge は存在しない
}

では,この変数への参照を,ブロックから返したらどうなるでしょうか.

fn main() {
    let reference = {
        let hoge = 100;
        &hoge
    };
    println!("{}", reference);
    // 存在しない変数 hoge の値が出力されるのか?
}

これは,次のようなエラーとなります.

error[E0597]: `hoge` does not live long enough
 --> src/main.rs:4:9
  |
2 |     let reference = {
  |         --------- borrow later stored here
3 |         let hoge = 100;
4 |         &hoge
  |         ^^^^^ borrowed value does not live long enough
5 |     };
  |     - `hoge` dropped here while still borrowed

このエラーは,以下で説明する重要なルールの存在によって生じています.

参照には,ライフタイムというものがあります.ある参照のライフタイムは,変数が借用されてから,最後にその参照が使用されるまでをいいます.

fn main() {
    let hoge = 100;
    let reference = &hoge; // 借用:ライフタイムの開始
    println!("{}", reference); // 最後の使用:ライフタイムの終了
}

&hoge と書いて変数 hoge が借用されたときに,この参照 &hoge のライフタイムが開始します.そして reference に代入された &hoge の値が最後に使用されるのは println!("{}", reference); の箇所なので,ここで &hoge のライフタイムが終了します.

ここで,次のようなルールが存在します:参照のライフタイムは,元の変数のスコープをはみ出してはなりません

この例では,ライフタイムの開始がスコープの開始より遅く,かつライフタイムの終了がスコープの終了より早いので,問題なく動きます.

fn main() {
    let hoge = 100; // hoge のスコープの開始
    let reference = &hoge; // &hoge のライフタイムの開始
    println!("{}", reference); // &hoge のライフタイムの終了
} // hoge のスコープの終了

一方,先ほどの例では,

fn main() {
    let reference = {
        let hoge = 100;
        &hoge
    }; // hoge のスコープの終了
    println!("{}", reference); // &hoge のライフタイムをここまで続けようとしている
}

reference に代入された &hoge のライフタイムの終了点を,ブロックの外側まで引き延ばそうとしています.よってこのコードはエラーとなります.

ここで問題です.次のコードはエラーになるでしょうか?

fn main() {
    let reference;
    {
        let hoge = 100;
        reference = &hoge;
        println!("{}", reference);
    }
}
クリックで答えを表示

エラーにはなりません.

fn main() {
    let reference;
    {
        let hoge = 100; // hoge のスコープの開始
        reference = &hoge; // &hoge のライフタイムの開始
        println!("{}", reference); // &hoge のライフタイムの終了
    } // hoge のスコープの終了
}

hoge のスコープは,宣言されてからブロックが終了するまでです.一方, &hoge のライフタイムは,借用されてから最後に使用されるまでです.最後に使用されるのがブロックの内側なので, &hoge のライフタイムは hoge のスコープをはみ出していません.

また,同じ hoge を複数回借用したら,

fn main() {
    let hoge = 100;
    let reference1 = &hoge;
    let reference2 = &hoge;
    println!("{}", reference1);
    assert_eq!(*reference2, 100);
}

それぞれに対してライフタイムが存在します.

fn main() {
    let hoge = 100;
    let reference1 = &hoge; // 1 つめの &hoge のライフタイムの開始
    let reference2 = &hoge; // 2 つめの &hoge のライフタイムの開始
    println!("{}", reference1); // 1 つめの &hoge のライフタイムの終了
    assert_eq!(*reference2, 100); // 2 つめの &hoge のライフタイムの終了
}

静的なライフタイム

整数リテラルのようなリテラルも,借用することができます.

fn main() {
    let reference = &100;
    println!("{:p}", reference);
    assert_eq!(*reference, 100);
}

このとき, &100 は「プログラム開始からプログラム終了までずっと続く変数」への参照だと思うことができます.このような参照は,静的なライフタイムをもつといいます.

静的なライフタイムをもつ参照は,プログラム終了までずっと使うことができるので,たとえば次のコードは正常に動きます.

fn main() {
    let reference;
    {
        reference = &100;
    }
    assert_eq!(*reference, 100);
}
脚注
  1. 2021/01/10 までこれを「自動的な参照外し」(auto-dereferencing)と書いていましたが,間違いでした.申し訳ありません. ↩︎