LoginSignup
43
27

More than 5 years have passed since last update.

Rustの所有権システムについて

Last updated at Posted at 2017-12-21

はじめに

趣味でやってるRust第二弾!
前回の記事に書こうと思ったけど長くなりそうだったので、ここでまとめます。

Rustはガーベジコレクションなしにメモリの安全性を保障しています。
その手法「所有権システム」についての説明と、
前回紹介したコードで、所有権システムの制御に対処した箇所を紹介したいと思います。

僕なりの解釈でお話しすることになるので、
「何言ってんだ、てめえ!?」と思った際にはコメントよろしくお願いします。

所有権システムとは

C++をご存知の方はスマートポインタとムーブセマンティクス的な話だとご理解いただければ幸いです。
なんぞそれ?って方には以下のサンプルコードで説明致します。

サンプル
fn main(){
    let a = String::from("aaa");
    let b = xxx(a);

    println!("{}", a);
    println!("{}", b);
}

fn xxx(x: String) -> String{
    return x
}

変数aには"aaa"、変数bにはaを引数にした関数xxxの戻り値を格納しています。
関数xxxは受け取った文字列を返すだけの単純なものです。
そして最終的にコンソールに"aaa"が2行出力される想定です。

何の変哲もないシンプルなコードですね。

が、しかし!!!

こいつ、コンパイルすら通りません。

error[E0382]: use of moved value: `a`
 --> Main.exs:7:20
  |
5 |     let b = xxx(a);
  |                 - value moved here
6 | 
7 |     println!("{}", a);
  |                    ^ value used here after move
  |
  = note: move occurs because `a` has type `std::string::String`, which does not implement the `Copy` trait

error: aborting due to previous error

キーワードはずばり「use of moved value: `a`(ムーブされた変数aを使っています)」
一体どういうことなのでしょうか?

所有権という概念

初めてプログラミングを学ぶに際して、
変数は「値を入れておく箱」と説明されることが多いでしょう。

所有権システムについて考える時、
値の入った「箱」があり、
その箱の”所有権”を持っている「変数」という人物がいると仮定すると理解しやすいでしょう。

上記のサンプルコードを文章で書くとこんな感じになります。

・変数aさんは、"aaa"の入った箱を所有しています。
・変数bさんは、aさんの持っている箱を関数xxxさんに渡しました。
・bさんは、関数xxxさんがごにょごにょした箱を受け取りました。
・aさん、箱の中身を見せてください。
・bさん、箱の中身を見せてください。

なぜコンパイルエラーが出たか分かりましたね?
aさんは序盤でbさんに自分の箱を渡しています(所有権がムーブされた)。
もう箱なんて持っていないのに、aさんには「箱の中身を見せろ」という命令が出ます。

Rustではこのように値そのものを渡す(コピー)のではなく、
値の所有権を渡す(ムーブ)のがデフォルトになっております。

正しく値のやりとりをできているか?
それをチェックする機構が所有権システムなのです。

ちなみにデータ型がコピートレイトを実装していれば、
見慣れた値の代入を行なうこともできます。

そのあたりの詳しい話は以下のチューリアルにて
https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/ownership.html

C++の危険なコード

余談ですが、先ほどのサンプルコードをC++で書くとこんな感じになります。

サンプル.cpp
#include <iostream>
#include <string>

using namespace std;

string xxx(string* x){
    return *x;
}

int main(void)
{
    string* a = new string("aaa");
    string* b = new string(xxx(move(a)));

    cout << *a << endl;
    cout << *b << endl;

    return 0;
}

関数xxxを呼び出す箇所は、値がムーブされるようmove()を使用しました。
挙動はRustのサンプルと同じです。

当然コンパイルは通らない、、、かと思いきや

コンパイルできます。
そして実行すると落ちます。
恐ろしいです。

こういうミスを事前にコンパイル時に検出できるので、
所有権システム、えらい!

ちなみに上記のサンプルコード、
string*ではなくunique_ptr<string>を使えば、Rustと同様のコンパイルエラーが発生します。

誤解を恐れずに言うならば、Rustは所有権システムによって、
C++のスマートポインタをデフォルトで使用しているのと同様の振る舞いをしているということになります。

unique_ptrがあるんならshared_ptrもあるんじゃね?
その通りです。
所有権を渡すのではなく、貸し出す「借用」という機能が
所有権システムには存在します。

その例が前回投稿したコードにあるので、それをもとに解説します。

借用~値の参照

借用についての詳しい話は下記リンクをご参照ください。
https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/references-and-borrowing.html

リンク先からの引用になりますが、
Rustの借用(参照)には2つのルールがあります。

リソースに対する1つ以上の参照( &T )
ただ1つのミュータブルな参照( &mut T )

1つ目は「値と参照元の関係は1:多。値の変更はできない」
2つ目は「値と参照元の関係は1:1。値の変更は可能」

前回投稿「RustからWin32APIを呼び出す方法」では一つ目の方法を採用しております。

まずは該当箇所のコードを見てみましょう。

main.rs
fn main() {
    //ローカルの割り当て、ネットワーク上の資源
    let local_name = CString::new("z:").unwrap();
    let lpremote_name = CString::new("<資源名 or IPアドレス>").unwrap();
    //ファイル取得を行うディレクトリ
    let dir_path = String::from("Z:/sample");

    //接続に成功すれば、ファイル情報取得を実行
    if connect_resource(&local_name, &lpremote_name){
        get_fileinfo(dir_path);
    }
}

mainでネットワーク接続用関数connect_resource()に
「ネットワーク資源名」「ローカル割り当て時のドライブ名」を
"&変数名"で渡しています。
変数名の頭に&をつけることで値の所有権を貸出(参照)することができます。

そもそもなぜそんなことをしたかと言うと
connect_resource()内ではネットワーク資源の「切断」と「接続」を行なっているからです。

「切断先」と「接続先」は同じ資源を指すようにしたかったのですが、
前述の所有権の問題で、切断用関数に「ネットワーク資源名」を渡してしまうと、
接続の際に「ネットワーク資源名」を格納した変数を使えなくなってしまうのです。

手っ取り早く「接続用文字列」変数と「切断用文字列」変数を用意して、それぞれ同じ文字列を格納してもよかったのですが、
同じ値を入れるべき変数が2つもあるとかスマートではない。
じゃあラッパ関数としてconnect_resource()を作成し、引数に当該文字列を”借用”する形をとったわけです。

最後に余談

この記事作成中に、所有権がらみの面白いエラーが出たので紹介します。

let aaa = vec![1,2,3,4,5];
let bbb = vec![1,2,3,4,5];

for x in aaa{
    for y in bbb{
        //2つの配列の要素同士を比較するような処理
    }
}

配列を2つ用意し(a, b)、それぞれの要素数分繰り返すループをネストさせてます。
ソートのアルゴリズムっぽいことを行なっている思っていただければ幸いです。

さて、問題のエラーは2つ目のループで起こりました。

エラーログより抜粋
error[E0382]: use of moved value: `b`
 --> Main.exs:8:18
  |
8 |         for y in b{
  |                  ^ value moved here in previous iteration of loop

1つ目のループで繰り返す回数分2つ目のループが実行されます。
つまり、ループの実行回数分、配列bの所有権を渡す必要があるのです。

それは所有権システムのルール違反なので、
配列bは参照(&b)の形で記述する必要が生じます。

まとめ

このシステムはあるコストを持ちます。それは学習曲線です。 多くの新しいRustのユーザは「借用チェッカとの戦い」と好んで呼ばれるものを経験します。そこではRustコンパイラが開発者が正しいと考えるプログラムをコンパイルすることを拒絶します。 所有権がどのように機能するのかについてのプログラマのメンタルモデルがRustの実装する実際のルールにマッチしないため、これはしばしば起きます。 しかし、よいニュースがあります。より経験豊富なRustの開発者は次のことを報告します。一度彼らが所有権システムのルールとともにしばらく仕事をすれば、彼らが借用チェッカと戦うことは少なくなっていくということです。

以上、チュートリアルサイトからの引用です。

この所有権システム、意外と曲者で慣れるのに結構苦労します。
しかしコードを書いていくうちになんとなく分かるようになるのでご安心ください!

これを機にRustやってみようって方がいることを祈りつつ、
ご質問、ご意見などコメントでお待ちしております。

43
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
27