概要
Rust学習するにあたり所有者を理解するため、色々なパターンを試してみた所、中々面白かったので、記事にしてみました。(おそらくまだまだパターンはあると思います。)
twitterでつぶやいた内容をわかりやすくまとめ、+αの解説を入れました。
Rustにて、string型だと変数はスタック領域、値がヒープ領域にあり、let v2 = v1のようにするとv1の所有権がv2にmoveするためv1にアクセスするとエラーになる。int型だとcopyトレイト実装しているため let i2 = i1するとi1を単純にi2へcopyされi1、i2の所有権も値もそれぞれスタック領域にできる— frusciante(フルシアンテ)☸? (@fruscianteee) December 17, 2022
経緯
Rustの型周りの学習していく際に、所有者周りの挙動を理解する必要があったので、所有者の動作確認を始めた。その流れを示します。
詳細
Rustの変数とメモリの関係
Rustを勉強していく上で、メモリ周りの理解が必要になります。
主に4つあります。
-
- Rustのソースコード自体を格納するメモリ領域
-
- constや文字列リテラルなどの実体を格納するStatic(静的領域)なメモリ領域
-
- 変数や配列が格納されているStack(スタック)なメモリ領域
- Stringなどの動的なデータを扱うためのHeap(ヒープ)なメモリ領域
今回はStack、Heap領域周りを勉強しました。
Rustでは、String型の変数そのものはStack領域に、実体の値はHeap領域に格納されています。
String型のv1があったとして、let v2 = v1のようにバインドするとv1の所有権がv2にmoveします。
v2にmoveされたため、その後にv1にアクセスしようとしてもエラーになります。
int型だと、copyトレイトなるものが実装しているため、
int型i1があったとしてlet i2 = i1のようにバインドしても、i1の値をi2へcopyしたことになります。
i1、i2の所有権も実データもそれぞれStack領域に格納されます。
思ったこと・・・
(Rustってこんなにスタック領域やヒープ領域、ポインタを意識せないかんのか。C++を少しやったお陰で理解しやすかった。でなきゃ、ちんぷんかんぷんやったわ?)
String型の所有権を保持したい場合
cloneを使うことで、deepcopyすることが可能になります。
let s1 = String::from("hogehoge");
let s2 = s1.clone();
これでs1にアクセスしてもエラーにはなりません。
変数はスタック領域、実体の値はヒープ領域はs1,s2とそれぞれ別で格納します。
所有権の動作確認
所有権がなくなるパターン
変数代入
関数の引数
所有権の挙動のメモリの動きについて
所有権がちゃんとmoveされて、変数のStackメモリ領域のアドレスのみが変わり、実データが格納されているHeapメモリ領域のアドレスは変わっていないのが分かる例。
あー、これが所有権の挙動なのかって、実感するかと思います。私はここで、なるほどと思いました。
所有権を取り戻すパターン
Rustの関数に戻り値をつけることで所有権を戻しているのが分かる例です。
余談
Rustの仕様で、関数のreturnの書き方にてセミコロン無しが実質returnの挙動になっています。
個人的に少しモヤモヤする。慣れもあるのが、常にセミコロンをつけていて、この場合のみセミコロンを外すという絶妙なルールがムズムズするので、現時点の私なら明示的にreturnを書くと思います。
(ワンライナーのように一行のみの関数ならありか?)
所有権周りの参照について
変数の頭に「&」をつけることで参照渡しができる。関数処理前後でもstackとheapのアドレスが変わっていません。
所有者目線では所有権を渡さずにStack領域にある変数の先頭アドレスを渡している。関数目線では所有権ではなく変数の先頭アドレスをもらい、それを使って一時的に借用してHeap領域の実データにアクセスできるようになります。
この例の55行目にて、mutである変数の参照を渡す際にわざわざ(&mut s9)とmutを書くのに一瞬モヤっとしました。
が、ミュータブルな変数であること、その変数に何かしら変更が加えられる可能性があると明示することで可読性が上がるというメリットあるなと思い、この仕様に納得しました。
一つの変数に対して複数の参照するパターン
所有権のおさらい
複数参照が成功
ただの参照で特に変更を加えることもないので、この書き方は問題ありません。
mutは複数参照できないためエラー
mutな変数を複数定義した場合、どこの変数がどのタイミングで値が書き変わるか予測できなくなるため、mut変数は1つのみしか定義できないようになっています。(この制約はおそらく”連続してmut変数定義ができない”だと思われる。)
immutとmutの混在の参照できないためエラー
75行目でimmutとして定義したので値が変更できないことが保証されているが、その後にmutを定義しようとしているため、矛盾がエラー発生。
この辺りは、よく考えていれば、納得するので、わからない場合は少しソースコードと実行結果と睨めっこする必要あります。
ちなみに、先に75行目がmutで78行目がimmutの場合でも同様にエラーが発生します。
所感
データの整合性を保つために、mutは誰かがimmutで参照した場合はmutが出来ないなど、色々制約があることがわかった。
これぞ所有権マジック!と思うようなパターン
rust所有権が本領発揮した例です。
※この制約はエラーを起こさないようにするためのものなので、ここで嫌にならず、ポジティブにこの制約を受けれよう(⌒,_ゝ⌒)ニッ
変数s12が誰にも所有者を渡していないのに、mutの変数r1が有効な時はs12にアクセスできない
変数のライフタイムの仕様によって起こる事象です。
r1がまだ生きており誰かから参照されているまでは参照元のs11はロック状態になっているかと思います。
補足
ライフタイムの仕様により、実装によっては、実データが先にdropしてしまい、変数が空のheap領域のアドレスを参照してしまうような書き方をしてしまうことがあります。このように実データは既になく、不正なアドレスを指している変数のことをダングリングポインターと言います。このような実装を起こさないようにRustはライフタイムあるので、C++とは違い無意識にダングリングポインターを作ることがないがRustのメリットの一つと言えます。
変数s12が誰にも所有者を渡しておらず、当たり前のようにs12にアクセスできる
変数のライフタイムの仕様によって起こる事象です。
mutの変数r1が85行目の処理以降参照することがないため、ロック状態のなくなり、変数s12は所有者本人であるため、普通に参照できます。
immutな変数の後でもmut変数にできる
immutの変数を複数宣言しているが、91行目でimmutの変数の役目が終わっているため、
s13に対してmutの変数を作成でき、編集することができるようになる。
所感
一見難解そうに見えるが、よく見ると納得する挙動なので、無駄に長生きな変数を作らず、
適切に変数を生成して適切に変数の役目を終わらし、次の処理に応じてさらに適切な変数を作成する行為はとても合理的で素晴らしいと思いました。
まとめ
まだRustのRもわかっていない初心者ですが、所有権の素晴らしさ、よく考えられているなと思うような動作確認でした。
所有者周りで理解に困っている人はこの記事を見て、少しでも理解の参考になれば幸いです。
Rustの学習は始まったばかりなので、また躓きそうなポイントがあれば、記事にしていけれたらと思います。
ここまで読んでいただき、ありがとうございました。m(_ _)m