Rustをはじめよう その8(参照・仕組み編)

三菱総研DCS デジタル企画推進部の加藤です。 この記事では、プログラミング言語Rust(ラスト)の参照の性質について解説します。

前書き

記事概要

  • この記事では所有権との関わりが深い機能である参照について、前回の記事より細かい性質について確認していきます。
  • 本記事で「参照」と表記した場合は、「共有参照」と「可変参照」の両方を含めるものとします。

参照外し

参照は、値(実データ)のありか(アドレス)を表すポインタ型[注]です。そのため、プログラム中で参照(アドレス)そのものを使用する状況は比較的まれで、参照が指し示す値(実データ)を使用する場合がほとんどです。

[注] Rustにおける参照は、値(実データ)の型によっては、単なるポインタではなくファットポインタ(アドレス以外の情報を含むポインタ)となる場合があります。ただし、本記事では説明を簡略化するため、すべての参照を単なるポインタとして扱います。

以下のサンプルコードで、「値」と「参照」を区別する必要があるプログラムの例を見ていきます。以下のコードを実行すると、ifの条件式でString型(値)と&String型(参照)を比較していることが原因でエラーが発生します。

main.rs

fn main() {
    let str = String::from("abc");
    let str_ref = &String::from("def");
    if str == str_ref {
        println!("equal");
    } else {
        println!("not equal");
    }
}


出力結果

error[E0277]: can't compare `std::string::String` with `&std::string::String`
 --> src\main.rs:4:12
  |
4 |     if str == str_ref {
  |            ^^ no implementation for `std::string::String == &std::string::String`
  |
  = help: the trait `std::cmp::PartialEq<&std::string::String>` is not implemented for `std::string::String`


error: aborting due to previous error


エラーメッセージにcan't compare `std::string::String` with `&std::string::String`とある通り、String型と&String型は型が異なるため比較ができません。上記のコードの場合、参照(変数str_ref)が指し示す値(実データ)はString型のため、この値とString型の値(変数str)を比較すれば正しく動作します。

参照から実データにアクセスするためには、参照の先頭に*(アスタリスク)を付けます。参照に*を付けて実データにアクセスすることを 参照外し(dereference) といい、*参照外し演算子(dereference operator)と呼ばれます。

参照外し演算子*を使用して書き換えたサンプルコードを、以下に示します。

main.rs

fn main() {
    let str = String::from("abc");
    let str_ref = &String::from("def");
    if str == *str_ref {
        println!("equal");
    } else {
        println!("not equal");
    }
}


出力結果

not equal


このように、変数同士を比較する場合などは、変数に格納されている値の型を意識する必要があります。Rustの仕組みに慣れるまでは、参照を格納する変数名には_refといった接尾辞を付けるなど、変数名や引数名に工夫をするのも一案かと思います。

暗黙的な参照外し

所有権とその移動という仕組みがある関係で、Rustでは参照が多用されます。その一方、参照が指し示す値を使用する際にいちいち参照外しをしていると、実装が煩雑になってしまいます。

Rustでは、参照外しが必要な一部の実装において、暗黙的な参照外しが発生します。つまり、プログラムに参照外し演算子*を明示的に記述することなく、参照外しを用いた値の使用ができます。

以下で、暗黙的な参照外しが発生するパターンを見ていきます。

まずは、タプル(tuple)を使用して暗黙的な参照外しの挙動を確認します。タプルはデータ構造の一種で、複数の異なる型の値を含むことができるコレクションです。
以下のサンプルコードは、タプルを生成し、そのタプルに対する共有参照を生成します。その後、タプルの要素をコンソール出力します。

main.rs

fn main() {
    let tuple = (1, "One");
    let tuple_ref = &tuple;

    // タプルの要素には(タプル.番号)でアクセス
    println!("{}", tuple.1);

    // 参照外しをしているが実装が煩雑
    println!("{}", (*tuple_ref).1);

    // tuple_refは暗黙的な参照外しで(*tuple_ref)として扱われる
    println!("{}", tuple_ref.1);

}


出力結果

One
One
One


コード中の変数tuple_refにはタプルの共有参照が格納されています。そのため、この変数には実データそのものではなく、実データのアドレスが格納されているはずです。一方で、12行目のprintln!("{}", tuple_ref.1);では、tuple_refの参照外しをしていないにも関わらず、あたかも参照外しを行ったかのようにタプルの要素にアクセスできています。

以上のサンプルコードのように、Rustでは .(ドット)演算子を使用した場合、その左辺の値は暗黙的に参照外しされます。 これにより、コードの見た目がすっきりするだけでなく、あたかも参照が実データそのもののように振る舞うため、処理の流れが理解しやすくなるというメリットもあります。

.演算子による暗黙的な参照外しは、メソッドの実行時にも発生します。以下のサンプルコードでは、Vec型の持つsortメソッド(各要素を昇順に並び替え)を実行した後にreverseメソッド(各要素を降順に並び替え)を実行しています。この場合もタプルのサンプルコードと同様、.演算子の左辺は暗黙的に参照外しされます。

main.rs

fn main() {
    let mut vector = Vec::new();
    vector.push(String::from("c"));
    vector.push(String::from("b"));
    vector.push(String::from("a"));
    let vector_ref = &mut vector;

    // 明示的に参照外しをする場合
    (*vector_ref).sort();

    // vector_refは可変参照だが、メソッド実行時に.演算子の左辺は暗黙的に参照外しされる
    vector_ref.reverse();

    for str in vector_ref {
        println!("{}", str);
    }
}


出力結果

c
b
a


暗黙的な参照外しは、上記の.演算子を使用する場合のほか、参照同士を比較演算子で比較した場合にも発生します。
以下のサンプルコードでは、ifで&Vec型の値同士を比較していますが、正しく動作します。

main.rs

fn main() {
    let mut vector1 = Vec::new();
    vector1.push(String::from("a"));
    vector1.push(String::from("b"));
    vector1.push(String::from("c"));
    let vector1_ref = &vector1;
    let mut vector2 = Vec::new();
    vector2.push(String::from("a"));
    vector2.push(String::from("b"));
    vector2.push(String::from("c"));
    let vector2_ref = &vector2;

    if vector1_ref == vector2_ref {
        println!("equal");
    } else {
        println!("not equal");
    }
}


出力結果

equal


ダングリングポインタが発生しない

参照は存在する値(実データ)に対して生成されることを前提としているため、参照を持つ値が参照よりも早くスコープを抜けて破棄される場合、参照がダングリングポインタとなるように思えます。しかし、Rustにおける参照では一般にダングリングポインタは発生せず、もし発生するような実装をした場合にはコンパイルの時点でエラーとなります。

以下でいくつかのサンプルコードを示し、ダングリングポインタが発生しないことを確認します。

まずは単純な例です。以下のサンプルコードでは、{}内のスコープで生成した値に対する参照を{}の外のスコープの変数へ格納して、その変数をスコープ外で使用しています。この場合、{}内のスコープを抜けるタイミングで値も破棄されるので、一見するとダングリングポインタが発生しそうです。しかし、コンパイラはこの実装をエラーとして指摘します。

main.rs

fn main() {
    let reference;

    {
        let mut vector = Vec::new();
        vector.push(String::from("a"));
        vector.push(String::from("b"));
        vector.push(String::from("c"));
        reference = &vector;
    } // ここでvectorはスコープを抜けるので、ダングリングポインタが発生するはず

    for str in reference {
        println!("{}", str);
    }
}


出力結果

error[E0597]: `vector` does not live long enough
  --> src\main.rs:9:21
   |
9  |         reference = &vector;
   |                     ^^^^^^^ borrowed value does not live long enough
10 |     }
   |     - `vector` dropped here while still borrowed
11 | 
12 |     for str in reference {
   |                --------- borrow later used here


error: aborting due to previous error


エラーメッセージはダングリングポインタに直接言及していませんが、`vector` does not live long enough(vectorの生存期間が十分ではない)ことを指摘しています。さらに、コードの9行目でvectorが借用されているにもかかわらず{}のスコープを抜けた時点でvectorはドロップ(所有権が破棄)されており、12行目で借用された値を使用していると述べています。

以下のサンプルコードでは、関数funcから参照を返却しています。しかし、戻り値の参照が指し示す値は関数内部で生成されるため、この値は関数の実行が終わるタイミングで破棄されます。このため、戻り値の参照はダングリングポインタとなってしまいます。
Rustのコンパイラは、この実装もエラーとして指摘します。

fn main() {
    let str_ref = func();
}

fn func() -> &String {
    let str = String::from("String");
    &str
}


出力結果

error[E0106]: missing lifetime specifier
 --> src\main.rs:5:14
  |
5 | fn func() -> &String {
  |              ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from


error: aborting due to previous error


エラーメッセージは先ほどのサンプルコードとは異なっており、missing lifetime specifier(ライフタイム指定子が存在しない)と指摘しています。ライフタイムとは、ある参照がプログラムの実行中に安全に使用できるスコープを表現したもので、通常は暗黙的に推論されます。今回のサンプルコードのように、コンパイラがライフタイムを推論できない場合にはエラーとなり、適切なライフタイム指定子をコードに記述しない限りコンパイルができません。
ライフタイムに関する詳しい説明は割愛しますが、ここではRustにおけるメモリ安全性を担保する仕組みの一つである、と理解してください。

まとめ

本記事と前回の記事では、Rust の参照の仕組みをご紹介しました。
参照と所有権は、Rustによるプログラミングを習得するにあたって特に理解が難しい概念だと思います。これらを理解するためには、ご自身でコードを書いて挙動を確認するほか、複数の入門記事や参考書を読むことをおすすめします。1つの概念について様々な角度から検討することができるため、1つの情報源のみにあたるよりも、理解が進みやすくなると思います。

本連載「Rustをはじめよう」は、今回で最終回となります。連載で紹介したRustの文法事項や機能はごく一部です。より深くRustを知りたい方は、本連載を足がかりとして、以下に示す参考文献や入門記事、技術書をお読みいただければと思います。

記事一覧

参考文献

  • 「The Rust Programming Language: 2nd Edition」, https://doc.rust-jp.rs/book-ja-pdf/book.pdf 2020 年 2 月 26 日アクセス
  • Jim Blandy, Jason Orendorff 著, 中田 秀基 訳(2018)『プログラミングRust』 オライリー・ジャパン
  • κeen,河野 達也,小松礼人(2019)『実践 Rust 入門[言語仕様から開発手法まで]』 技術評論社
その7 |  その8