読者です 読者をやめる 読者になる 読者になる

冥冥乃志

ソフトウェア開発会社でチームマネージャをしているエンジニアの雑記。アウトプットは少なめです。

follow us in feedly

Rustの勉強(その11)

Programming Rust

結局前回の部分は完全に理解できておりませんので、続き。

mao-instantlife.hatenablog.com

Move Semanticsの詳細

子ブロックであろうと、関数のコールであろうとバインディングのオーナーシップが移動します。オーナーシップの移動はブロックを抜けても有効で、オーナーシップの移動後のバインディングは基本的に使えません。

ここで、Rustのバインディングに関するメモリの使い方についての説明を大まかに整理してみます。変数が宣言されると、実データ(型と値)はヒープに、変数名とヒープのアドレス(データポインタと呼ぶ)はスタックに積まれます。同じ値を指す変数が宣言されるとヒープのアドレスをコピーしてスタックに積みます。上記のように分けて格納することに価値があるっぽいとのこと。長さやキャパシティについては、両方で共有されています。

所有権が移った時、Rustはヒープのメモリアドレスのビット単位のコピーを作ります。これは、一つのスタック領域へのポインタが同じヒープの中に二つあることを意味し、Rustのメモリ安全性の保証(一つのスタック領域に同時にアクセスできるポインタが二つ以上あってはならない)に違反します。

例えば二つのVectorv1v2 があり、それらのヒープが同じものを指している状態を考えます。複数のポインタを許可すると、何がしかの操作で v2 の 三つ目の要素を削除しても v1 にはすぐに通知されず、 v1 は楽観的に要素にアクセスするけど、なかったら実行時エラーになるという状況が起こります。これを防ぎたいという安全性の保証に沿って、以前のものの参照がなくなる仕様にしているようです。

Copyトレイト

で、どう言い訳してもこの仕様は不便さをもたらしてしまうので、その不便さを解消するための言語機能が幾つかあるようです。その一つがCopyトレイト。

let v = 1;

let v2 = v;

println!("v is: {}", v);

上記はコンパイルエラーになりません。これは i32Copy トレイトを実装している *1 ためです(そのままだとv2未使用の警告は出る)。おそらく上記のようなコードがあった時に、RustはCopyトレイトを実装しているかどうか確認して、実装していればコピーを作成、実装してなければ所有権を移す、という挙動をしているものと思われます。

というか関数呼び出しで引数を渡しただけでも所有権移るのでちょっとトラップ多い感じですね。

手動(?)でのオーナーシップ返却

もう一つが手動でのオーナーシップ返却です。さっきの関数の話ではないですが、

fn hoge(v: UserObj) -> UserObj {
v // 引数をリターンする
}

という感じで引数をそのままリターンすれば所有権は一応返ります。引数リストが複数で戻り値もある、という場合はタプルで返せば良いようです。

fn fuga(v1: UserObj, v2: UserObj) -> (UserObj, UserObj, i32) {
(v1, v2, 42)
}

で、これでもまだ複雑ですね。根本的な解決にはなってない気がします。というわけで、 Borrowing というコンパイル機能があります。

Borrowing

前項で出てきた引数の話しですが、Rustの慣用的には以下のようにコールします。

fn fuga(v1: &UserObj, v2: &UserObj) -> i32 {
42
}

let v1 = UserObj.new();
let v2 = UserObj.new();

let ret = fuga(&v1, &v2);

&T(型) という型を reference と呼び、実際にコールするときも & をつけます。オーナーシップを一時的に借りて(borrowing)いる機能です。 fuga のコールが終わったら、所有権が自動的に戻るようになっています。

なお、referenceは不変なので、上記のfuga関数内でv1なりv2なりを操作しようとするとエラーになります。

mutable reference

具体的な利用シーンがまだ思い浮かばないんですが、ミュータブルな変数の参照は &mut T で行うみたいです。

let mut x = 5;
{
let y = &mut x;
*y += 1; // アクセスするにはアスタリスクをつける
}
println!("{}", x); // 6が出力される

しかも出力を見ると、元データの値も変わるとか。ますますどんな利用シーンがあるのかわからなくなってきました。

なお、このブロックを外すとコンパイルエラーになる。

何となくはわかってきたけど

段々とRustの仕様が私をいじめにかかってる気がしてきました。かなり勉強のスピード的には鈍化していて、一体いつ終わるんだ感がひどいですが、この辺りを乗り越えればスピードが出てくると信じて頑張ってみるですよ、はい。

というわけで、以下次号。

*1:プリミティブ型はすべて