W.I.S. Laboratory
menu-bar

Rust


Rustの借用、理論は分かるが書き方が分からなくなる

Rustは基本ユニークポインタを使うので、ポインタを他の変数に代入したり関数の引数として渡したりすると所有権が移動してしまう。
RefCellをRcやArcでラップすれば参照カウント式のスマートポインタとして使えるが、いろいろと面倒くさい。
なので通常は借用を使ってなんとかするわけだ。
Rustでは変数の前に「&」や「&mut」を付けることで変数の参照を関数に渡すことができ、これを「借用」と呼ぶ。
ただこの借用、長年CやC++を書いてきた私には「理論は分かる、けど記述の仕方が分からなくなる」が頻繁におきるのだ。
「借用=ポインタ渡し」だと思っていたのだが、どうもそうとは限らないらしい。
なので備忘録。

まずC。

なんということもない。いたって普通だ。
C++のスマートポインタ(ユニークポインタ)だと

これも特に言うことはないが、強いて言えばスマートポインタを参照渡しする。
続いてRust。

渡す側はCと同じなので、ほぼ混乱はない。
問題は受け取る側だ。
Cだと int *a としていたところを Rustでは a: &i32 と書く。
Cは「デリファレンスするとintになるアドレス値」という感じの書き方だが、Rustは「i32が格納されたアドレス値」という感じ。
変数名はプリフィックス無しのそのままで、型名に借用の意味である「&」を付ける。
Cと同じく、変数名の前に「&」は不要だ。
受け取った側でデリファレンスできるので、これはポインタ渡しだ。
ここで混乱が生じる原因のひとつに、println!マクロの引数の書き方がある。

デリファレンスしてもしなくても、結果が同じなのだ。
Cを書いてきた人が混乱すること請け合いだ。
アドレス値を出力するには、

こうするらしい。
初見殺しっぷりが凄まじい。

ややこしいのがミュータブル借用だ。
Cだと万年ミュータブルなので息を吐くようにミュータブル借用できるのだが、Rustはそうもいかない。

突然あちこちに「mut」が増える。
借用する変数自体をミュータブルにし、関数に渡すときにも「&mut」が必要だ。
「なんで?宣言時に let mut してるやんけ」と思うのだが、これは &(mut a) ではなく (&mut) a なのだ。
ミュータブル識別子には mut(値のミュータブル) と &mut(借用先のミュータブル) がある。
なのでここは「この引数は指し示す先の値を書き換えても良いアドレス値だよ」と関数に渡している。
受け取る側も同じで a: &mut i32 という具合に「&mut」を型名のプリフィックスとして付与すると「メモリ内容を書き換えても良いi32が格納されたアドレス値ですね」として受け取ることができる。
このとき、Rustではポインタ変数に格納されたアドレス値を書き換えることが御法度なので mut a: &mut i32 としてはいけない。
ミュータブルなのはあくまでデリファレンス時の値であって、アドレス値そのものではないからだ。
なので *a += 1 のデリファレンスを外すと「ポインタに対して演算はできないよ」と怒られる。
ということでこれもポインタ渡しだ。

ではBox化するとどうなるのか。
RustではBox化するとそのデータはヒープに置かれ、変数にはそのアドレス値が入る。
そしてこの変数こそスマートポインタ(ユニークポインタ)だ。
Cでいうところの malloc したもの、C++でいうところの std::make_unique したものと思って差し支えない。

なんと書き方が同じなのだ。
変数 a にはi32の10が入っているメモリのアドレスが入っている。(構造体なので他にもいろいろ入っているが)
にも関わらず、その変数 a に対して mut 宣言が必要だ。
Rustではポインタ変数の値(つまりアドレス値)を書き換えることは御法度なので、これは「変数 a が指し示す先にあるメモリ内容がミュータブルだよ」という意味になるようだ。
しかしCを書いてきた人の頭はこれを「アドレス値を書き換えても良いポインタ変数だよ」と認識するのだ。
長年Cを書くともうそういう頭になっている。
そしてその変数 a を &mut で、つまりミュータブル借用で渡しているのだから、「アドレス値が格納された変数が置かれたメモリアドレス、つまりユニークポインタへのポインタを渡している」と認識するわけだ、Cを書き続けてきた人の頭にはそうとしか読めないのだ。
なのだが、実際RustではユニークポインタがC++のような参照渡しで渡されているようだ。(もしこれがポインタ渡しなら2重デリファレンス(**a)できるはずなのだが、それをすると cannot be dereferenced エラーになる)
つまり、上のp関数で受け取っているポインタaは、main関数内でlet mutしたポインタaそのものを参照しているだけ(つまりエイリアス)なのだ。
ここが最大の混乱のもとになっている気がする。
Rustが「ポインタ渡し」とか「参照渡し」と呼ばず敢えて「借用」と呼んでいるのも、コード上の記述がまったく同じなのにポインタ渡しをする場合と参照渡しをする場合があるからではないだろうか。
そしてそれは記述上の「値」と「アドレス」の区別の緩さに繋がっている。
上に書いた println! マクロの引数をデリファレンスしてもしなくても同じ結果になるというのも、「値」と「アドレス」の区別を緩く扱っているからといえそうだ。

さらに借用の書き方の記憶が混乱する原因に、構造体にインプリメンテーションする関数の第一引数の書き方も関係しているかもしれない。

第一引数は &self なのだが、この書き方が他の引数と違う。
構造体のポインタを受け取っているにもかかわらず、変数名の前に「&」がついているかのように見える。
頻繁に書いているうちに「これがRustの借用の書き方なのかぁ」となんとなく思ってしまうようだ。
しかしこの書き方が特殊なのであって、一般的なRustの借用の書き方ではない。
これは self: &self のシンタックスシュガーらしい。

・・素直に丸暗記してしまったほうが近道のような気もするのだが、それが苦手な性格なので

  • 参照記号「&」は同じ記述なのにポインタ渡しをしたり参照渡しをしたりする
  • 記述上の値とアドレスの区別が緩い
  • 構造体にインプリメンテーションする関数の第一引数の書き方は特殊
このように理解しておこうと思う。


[ 戻る ]
saluteweb