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

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

前書き

記事概要

  • この記事では所有権との関わりが深い機能である参照について、基本的な考え方と文法事項をご紹介します。
  • 所有権の仕組みをご存じでない方は、先にRustをはじめよう その5(所有権・仕組み編)をお読みになることをおすすめします。

所有権の移動による問題

このセクションでは、所有権が移動する際に発生する問題について振り返ります。

復習となりますが、所有権が移動するタイミングは以下の3つのパターンに分けられます。

  • 変数への値の代入
  • 関数に対する引数(値)の受け渡し
  • 関数からの戻り値の受け取り

このうち、移動による問題が特に発生しやすいのが 関数に対する引数(値)の受け渡しのパターンです。

以下のサンプルコードで、関数に対する引数(値)の受け渡しを見ていきます(前回の記事より再掲)。このコードでは関数mainから関数funcを実行しており、この際にVec型の値の所有権が変数vectorからfuncの引数vecに移動し、vectorは未初期化状態となります。そのため、funcの実行後はmainでvectorを再び使用することができません。さらに、引数vecは関数funcの実行後にスコープを抜けるため、もともと変数vectorに格納していたVec型の実データは消えてしまいます。

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

    // vectorの所有権は関数funcの引数vecに移動する
    func(vector);

    // vectorは未初期化のため使用できない!
    for str in vector {
        println!("{}", str);
    }
}

fn func(vec: Vec<string>) {
    // 任意の処理
}


このような事態を避ける方法として、関数funcから戻り値としてVec型の値を返却することで、変数vectorに再度Vec型の値を格納することが考えられます。以下のサンプルコードは、この方法で書き換えたもので、問題なく実行することができます。
しかし、このような実装を毎回行うのは面倒なので、何かしらの代替手段がほしいところです。

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

    // vectorへ戻り値を再代入
    vector = func(vector);

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

fn func(vec: Vec<String>) -> Vec<String> {
    // 任意の処理
    vec
}


次に、関数以外の処理で所有権が移動する事例を見ていきます。

forループを使用してVec型の実データから各要素を順番に取り出すと、各要素から変数への代入が発生します。変数の代入が発生すると、所有権が移動します。したがって、forループを実行した後はVec型の値の所有権を持っていた変数から実データの所有権がすべて移動してしまうため、変数は未初期化状態となってしまいます。

以下のサンプルコードで挙動を確認しましょう。以下のコードでは、1回目のforループ実行時にvectorが持っている値の所有権がすべて変数strへ移動してしまうため、2回目のforループは実行できず、エラーが発生します。

main.rs

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

    // 各要素("a", "b", "c")はそれぞれ変数strへ代入されるため、所有権も移動する
    for str in vector {
        println!("{}", str);
    }

    // vectorは未初期化状態のため、エラー
    for str in vector {
        println!("{}", str);
    }
}


出力結果

error[E0382]: use of moved value: `vector`
  --> src\main.rs:13:16
   |
2  |     let mut vector = Vec::new();
   |         ---------- move occurs because `vector` has type `std::vec::Vec<std::string::String>`, which does not implement the `Copy` trait
...
8  |     for str in vector {
   |                ------
   |                |
   |                value moved here
   |                help: consider borrowing to avoid moving into the for loop: `&vector`
...
13 |     for str in vector {
   |                ^^^^^^ value used here after move


error: aborting due to previous error


ここまでに紹介したサンプルコードを意図通りに実行するためには、所有権を移動せずに実データにアクセスする手段があればよいことになります。これを実現する機能が、Rustにおける 参照です。

参照の使い方と仕組み

共有参照

では、さっそく参照を使ってサンプルコードを書き換えます。ここではどのようにコードが書き換わるのかを確認するに留めて、参照の仕組みについては後ほど解説します。

まずは、関数を実行する場合を見ていきます。

main.rs

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

    func(&vector);

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

// 引数のシグニチャ(型名)をVecから&Vecに変更
fn func(vec: &Vec<String>) {
    // 任意の処理
}


出力結果

a
b
c


上記のコードを実行すると、関数funcの実行後であっても問題なくforループが実行されます。元のコードとの違いは、関数の引数vecのシグニチャ(型名)の 先頭に& が付き、関数mainからfuncを実行する際に引数として与えている変数vectorの先頭にも、同様に&が付いています。

次に、forループのサンプルコードを参照を使用する形に書き換えます。

main.rs

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

    // inの後ろのvectorの先頭に&を付けた
    for str in &vector {
        println!("{}", str);
    }

    // 同上
    for str in &vector {
        println!("{}", str);
    }
}


出力結果

a
b
c
a
b
c


こちらも、問題なく実行できます。元のコードとの違いは、forループで使用する変数vectorの先頭に&が付いている点のみです。

関数のサンプルコードのシグニチャ(&Vec<String>)のように、Rustでは型名の先頭に&を付けることで、その値に対する 共有参照(shared reference)を表現できます。
また、変数や値の先頭に&を付けると、その変数や値に対する共有参照が返却されます。各サンプルコードの&vectorは、変数vectorの共有参照が返却されていることを表しています。
共有参照と後述の可変参照は、ともに型の一種で、ヒープ上の実データの位置を指し示すポインタ型として機能します。参照はあくまで実データがヒープ上のどこに置かれているかを表す型であり、実データそのものは扱いません。そのため、所有権の移動は発生しません。

共有参照は、その名の通り一つの値(実データ)を他の関数などと共有するために使用します。そのため、ある値に対する 共有参照は、同時に複数存在することができます。 ただし、共有参照を通じて、値を書き換えることはできません(値の読み出しのみ可能)。さらに、値の所有権を持つ所有者自身であっても、共有参照が存在する場合は値そのものを変更することができません。

以下のサンプルコードは、共有参照を通じて参照元の値を書き換えようとする例です。関数funcが、引数として受け取った&Vec型の値に対して新たな要素を追加しようとしますが、&Vec型は共有参照のためエラーとなります。

main.rs

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

    func(&vector);

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

fn func(vec: &Vec<String>) {
    vec.push(String::from("d"));
}


出力結果

error[E0596]: cannot borrow `*vec` as mutable, as it is behind a `&` reference
  --> src\main.rs:15:5
   |
14 | fn func(vec: &Vec<String>) {
   |              ------------ help: consider changing this to be a mutable reference: `&mut std::vec::Vec`
15 |     vec.push(String::from("d"));
   |     ^^^ `vec` is a `&` reference, so the data it refers to cannot be borrowed as mutable


error: aborting due to previous error


可変参照

可変参照(mutable reference)を使用すると、所有権を移動せずに参照元の値の書き換え(変更)ができます。可変参照は、値の書き換えはもちろんのこと、値の読み出しも共有参照と同様に行うことができます。
先ほどのサンプルコードを、共有参照から可変参照を使用する形に書き換えると、以下のようになります。

main.rs

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

    func(&mut vector);

    // 以下のforループの中では値の読み出しのみを実施するため、本来は共有参照を使用するべきです。
    // 今回は可変参照を使用した値の読み出し例を確認するため、あえて可変参照を使用しています。
    for str in &mut vector {
        println!("{}", str);
    }
}

fn func(vec: &mut Vec<String>) {
    vec.push(String::from("d"));
}


出力結果

a
b
c
d


上記のコードのように、型名の先頭に&mutを付けることで(関数funcの引数シグネチャの&mut Vec<String>)、その値に対する可変参照を表現できます。また、変数や値の先頭に&mutを付けると、その変数や値に対する可変参照が返却されます。

可変参照を使用する際は、以下の2点に注意が必要です。

  • ある値に対する可変参照が存在するとき、その値に対して他の参照(共有参照、可変参照ともに)を生成することはできません。
  • ある値に対する可変参照が存在するとき、値の所有権を持つ所有者であっても値を読み書きできません。

上記の2点をまとめると、可変参照が存在する間は、可変参照を通じて値に対して排他的にアクセスできるということです。
このような仕組みとなっている理由は、参照元の値にアクセスする別のコードに意図しない影響が発生することを防ぐためです。仮に可変参照以外から値にアクセスできてしまうと、アクセス時に値がどのようになっているのか、保証ができません。なぜなら、可変参照を通じて値が書き換わっている可能性があるからです。
可変参照を使用して排他的に値へアクセスできる仕組みは、特に並列処理時に発生しうるデータの競合を排除できるという点で有用です。そのため、Rustでは並列処理を他のプログラミング言語よりも安全かつ容易に実装することができます。並列処理については本記事では扱いませんが、Rustの有用性が特に発揮される分野ですので、ご興味のある方は本記事末尾の参考文献などでお調べになることをおすすめします。

なお、値の参照を作成することを、Rustでは値への参照の 借用(borrowing) と呼びます(他者が所有しているものを一時的に借りるという意味合い)。
実は、上で紹介したサンプルコードのエラーメッセージにも「借用」という単語が登場していました。以下に再掲するエラーメッセージの、コードの8行目(for str in vector { ...)に対する指摘を確認してください。

出力結果

error[E0382]: use of moved value: `vector`
  --> src\main.rs:13:16
   |
2  |     let mut vector = Vec::new();
   |         ---------- move occurs because `vector` has type `std::vec::Vec<std::string::String>`, which does not implement the `Copy` trait
...
8  |     for str in vector {
   |                ------
   |                |
   |                value moved here
   |                help: consider borrowing to avoid moving into the for loop: `&vector`
...
13 |     for str in vector {
   |                ^^^^^^ value used here after move


error: aborting due to previous error


エラーメッセージにhelp: consider borrowing to avoid moving into the for loop: `&vector`とあり、ここでborrowing(借用)という単語が使われています。メッセージ全体の意味としては、「forループへの(所有権の)移動を避けるため、借用(を使用すること)を検討してください」となっています。
Rustのコンパイラが生成するエラーメッセージは親切で、上記例のようにエラーの解決策を提案してくれることがあります。Rustの学習初期には、所有権や参照に関連したエラーが多く発生すると思いますが、その場合はエラーメッセージに問題解決に役立つヒントが書かれているかもしれません。

まとめ

参照の性質について、ここまでに解説してきた内容をまとめます。

  • 共有参照(shared reference)
    • &変数名、&型名で宣言(例: &variable&Vec<String>)
    • 値の読み出しが可能
    • 複数の共有参照が同時に存在できる
    • 共有参照が存在する場合は、所有権の所有者であっても値の書き込みは不可(読み出しは可能)
  • 可変参照(mutable reference)
    • &mut 変数名、&mut 型名で宣言(例: &mut variable&mut Vec<String>)
    • 値の書き込みと読み出しが可能(=書き込みが必要な場合に使用)
    • 同時に存在できる可変参照は1つのみ(可変参照が存在する場合、共有参照、可変参照ともに作成不可)
    • 可変参照が存在する場合は、所有権の所有者であっても値の読み出し・書き込みは不可

次回は、参照の性質について詳しく解説する予定です。

参考文献

  • 「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 入門[言語仕様から開発手法まで]』 技術評論社
その6 |  その7 | その8