やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Rustの所有権(ムーブ)

 あるヒープ領域を参照できるポインタは必ずひとつだけ。ヒープ変数を代入すればその所有権はコピー先へ移る。

成果物

はじめに

 私なりの解釈なので間違ってるかも? 内容に責任は持てないので正しく知りたいならドキュメント参照。

所有権とは

 所有権とは、ある特定のヒープメモリを参照するポインタがただひとつである仕組み。これにより二重解放や解放後の読書が生じない。メモリを安全に使用できる。

ムーブとは

 ムーブとは、ヒープ領域を参照するポインタ変数を代入したときに所有権を代入先へ移すこと。代入後、元のポインタ変数はヒープ領域を参照できず、無効化される。これにより、ある特定のヒープ領域を参照するポインタは常にひとつとなる。

概要

所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。

 Rustは所有権のおかげでメモリ操作の安全性が保たれる。なので理解できないとRust書けないよ。

この章では、所有権以外にも、関連する機能を いくつか話していきます: 借用、スライス、そして、コンパイラがデータをメモリにどう配置するかです。

 読み進めてみると「参照」も関連機能だと思うのだが。

所有権とは?

Rustの中心的な機能は、所有権です。

 断言。

機能は説明するのに単純なのですが、言語の残りの機能全てにかかるほど 深い裏の意味を含んでいるのです。

 鬼門になりそう。C言語でいうポインタ。Rustでは所有権が鬼門か。

メモリ管理

全てのプログラムは、実行中にコンピュータのメモリの使用方法を管理する必要があります。プログラムが動作するにつれて、 定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、 プログラマが明示的にメモリを確保したり、解放したりしなければなりません。

 たしかC言語ならmalloc/free関数でメモリの確保/解放するはず。

#include <stdio.h>
int main() {
    char* str = (char*)malloc(sizeof(char) * 16); // メモリ確保
    free(str); // メモリ解放
}

 Java, C#などの言語ではガベージコレクションにて自動解放してくれる。参照しなくなった変数を自動で。(ただしイベント実装時に参照が消えず増加し続けるコードを書いてメモリリークするという定番の罠などがあるが)

 明示的に解放できないのが不便に感じるときもある。C#ではusing () {}構文などを使うこともできる。そのブロックが終了したタイミングで指定インスタンスを消せる。ファイル操作のときなどによく使う。

Rustでは第3の選択肢を取っています: メモリは、コンパイラコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。

 ほかの言語とはちがうRust独自機能。

学ぶ意義

所有権は多くのプログラマにとって新しい概念なので、慣れるまでに時間がかかります。 Rustと所有権システムの規則と経験を積むにつれて、自然に安全かつ効率的なコードを構築できるようになることは、 素晴らしいお知らせです。その調子でいきましょう!

 学習コストは高いが、安全かつ高級言語の恩恵にあずかれるコードが書けるようになる。

所有権を理解した時、Rustを際立たせる機能の理解に対する強固な礎を得ることになるでしょう。この章では、 非常に一般的なデータ構造に着目した例を取り扱うことで所有権を学んでいきます: 文字列です。

スタックとヒープ

 急に超長文の説明w C言語を勉強したならわかるはずだが……。

どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。

 所有権はヒープ領域のメモリを管理する機能である。

メモリ領域

 プログラミングにおいて、メモリ領域は以下2種類ある。

メモリ領域 容量 速度 サイズ 用途
スタック 速(アクセス位置が常に最上のため) 固定 関数の引数、ブロック内(関数など)のプリミティブ変数。
ヒープ 遅(アクセス位置はポインタで指定する必要があるため) 可変 クラス(インスタンス

 C言語ではintなどプリミティブ変数を宣言したときはスタック領域。宣言したブロックが終了すると自動的に解放される。再帰によって繰り返しスタック領域を確保するとスタック・オーバーフローする場合がある。

 C言語ではmalloc/freeで確保/解放するのはヒープ領域。スタックと違い、任意のサイズを確保できる。ポインタでメモリのアドレスを指定することにより参照する。しばしば構造体で任意の型をつくる。これをヒープで確保し、関数の引数として渡す。ヒープ領域はスタックよりも多いので、スタックオーバーフローさせずに操作できる。

 また、ヒープはライフサイクルも制御できる。スタックと違い、関数が終了しても解放されない。freeするまで保持される。ひとつの関数で書ききれないなどコードを細分化するときに必然的に使わざるを得ない。だが、しばしばメモリリークやメモリ破壊の原因になる。freeし忘れてメモリリークする。やがてメモリを食いつぶして動作不能やシステムのフリーズなどを招く。また、複数回freeしたり、すでにfreeして別の変数の一部として使われているのに書込んでしまうことも起こりうる。それはシステムが破壊されコンピュータが異常な挙動になるなど致命的な問題を生じかねない。C/C++ではよくこのバグを作り込んでしまう。特に大規模になり複雑化すると確率が高まる。

 Rustの所有権は、ヒープ領域の危険なメモリ操作を抑制するしくみ。コーディングやコンパイルの時点で防げるので、上記のような問題が生じない。

データ構造

スタック(FILO)

 箱があるとする。出入口は上部1箇所のみ。

| |
| |
+-+
 0

 箱にA, Bを順に入れる。

 A         B
▼        ▼
| |  | |  | |  |B|
| |  |A|  |A|  |A|
+-+  +-+  +-+  +-+
 1    2    3    4

 次に1個ずつ取り出す。

      B    A
      ▲   ▲
|B|  | |  | |  
|A|  |A|  | |  
+-+  +-+  +-+
 5    6    7

 B, Aの順に取り出される。先に入れたAは後にとりだされる。

 つまり先入れ後出し。first in last out。FILO。スタックという名のデータ構造

データへのアクセス方法のおかげで、スタックは高速です: 新データを置いたり、 データを取得する場所を探す必要が絶対にないわけです。というのも、その場所は常に一番上だからですね。 スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定サイズにならなければならないということです。

キュー(FIFO)

 キューは今回の話と関係ないのだが、スタックのデータ構造と対比するために出す。

 筒があるとする。上が入口、下が出口。

入口

| |
| |

出口

 0

 筒にA, Bを順に入れる。

 A         B
▼        ▼
| |  | |  | |  |B|
| |  |A|  |A|  |A|

 1    2    3    4

 入れた順番どおりに取り出される。

|B|  | |  | |  
|A|  |B|  | |
   ▼   ▼
      A    B

 5    6    7

 つまり先入れ先出し。first in first out。FIFOキューという名のデータ構造

 しばしば待ち行列にたとえられる。ジェットコースターの順番待ちや、人気ランチ店の順番待ちが、まさにキューである。

所有権規則

まず、所有権のルールについて見ていきましょう。 この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください:

  • Rustの各値は、所有者と呼ばれる変数と対応している
  • いかなる時も所有者は一つである
  • 所有者がスコープから外れたら、値は破棄される

プリミティブ型変数(スタック領域に確保される型)

{
    let x = 0; // x のメモリ領域が確保される
}               // x のメモリ領域が解放される

 説明では確保・解放でなく「有効」「無効」とある。もしかすると上記は正確ではないかもしれない。

String型変数(ヒープ領域に確保される型)

文字列型(&str, String)

文字列の型 メモリ領域 サイズ コード例
&str スタック 固定 let s = "hello";
String ヒープ 可変 let mut s = String::from("hello");

 コンパイル時にサイズが確定するなら、バイナリコードに直接ハードコードできて高速に実行できる。だが、実行時にサイズが確定するなら、予めバイナリコード化できず低速になる。たとえばユーザ入力受付など。これらを使い分けるために、文字列の型は2種類ある。

ムーブ

  • Rustではヒープ変数のコピーはされずムーブされる
    • 浅いコピー、深いコピーはされない
      • 浅いコピー: ポインタ変数をコピーする
      • 深いコピー: 参照先のヒープ領域をコピーする

 コピーされると複数回、解放処理がされうる。べつの変数ではその領域を使っているのに、べつの変数では解放が指示される。このようなことが起こりうる。するとメモリ安全性が脅かされる。まだ領域を使用中の変数がひとつでもあった場合、そのコードは解放された後の領域を参照することになってしまう。

 そこで、Rustはヒープ領域のコピーをやめて、ムーブすることにした。ムーブとは、参照できるポインタをひとつだけにすること。コピー元はもはや無効な変数とし、参照できないようにする。そしてコピー先の変数のみ、ヒープ領域にアクセスできるようにする。メモリ解放はコピー先の変数がスコープから外れたとき自動で行われる。ムーブしたおかげで、ほかにその領域を使える変数がないため、解放後にアクセスされる心配がない。

fn main() {
    let a = String::from("AAA"); // ヒープ領域のメモリ確保
    let b = a; // ムーブ(ヒープ領域の所有権がポインタ変数`a`から`b`へ移った。ヒープ領域の所有は必ず1つのポインタ変数のみ)
    println!("b = {}", b); // OK
    println!("a = {}", a); // エラー
}
error[E0382]: use of moved value: `a`
 --> main.rs:9:24
  |
7 |     let b = a; // ムーブ(ポインタ変数aが示すヒープ領域の所有権をポインタ変数aからbへ移す)
  |         - value moved here
8 |     println!("b = {}", b); // AAA
9 |     println!("a = {}", 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

For more information about this error, try `rustc --explain E0382`.

 これはC/C++など既存のプログラミング言語と大きく違う。

言語 ヒープ スタック
C/C++ (深い 浅い)コピー|深いコピー
Rust ムーブ 深いコピー
これまでの言語
  • 代入
    • スタック変数
      • 深いコピー(新たなメモリを確保する(おなじサイズのスタック要素として))
    • ヒープ変数
      • 浅いコピー(ヒープ領域を指すポインタ変数のメモリを確保する)
      • 深いコピー(新たなメモリを確保する(ポインタ変数が指すヒープ領域とおなじサイズ))
Rust言語
  • 代入
    • スタック変数
      • 深いコピー(新たなメモリを確保する(おなじサイズのスタック要素として))
    • ヒープ変数
      • ムーブ(代入元は無効になり、代入先のみ有効になる)

 もし深いコピーをしたければ、以下コードでできる。

  • let s = String::from("").clone();

 深いコピー(clone())は別のヒープメモリ領域が確保されるので、それを参照できるポインタ変数はsのみ。やはり所有権をもつ変数はひとつだけである。

疑問
  • マルチスレッドのときはどうするの?
    • バックグラウンド処理
    • GUIによるユーザ入力
  • 単一責任(疎結合)にできるの?
    • 別々の関数がおなじ変数を使いまわすことになるのでは?
      • 関数の引数と戻り値でも所有権がらみの機能があるらしい。詳しくは次回以降

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux

前回まで