mimikakimemo

自分用メモ。

Rust に付け焼き刃で入門する 3

Rust に付け焼き刃で入門する 2 の続き。

4. 所有権を理解する

Rust 独特の機能。「Rust を手っ取り早く解説」系の記事では、あまりよく挙動がわからなかったので、しっかり理解したい。

4.1. 所有権とは

所有権とは? - The Rust Programming Language 日本語版

プログラムが動作するにつれて、 定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、 プログラマが明示的にメモリを確保したり、解放したりしなければなりません。Rustでは第3の選択肢を取っています: メモリは、コンパイラコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。

GC でも malloc/free でもない第3の方法が所有権 ownership システム。

この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。

  • スタック:高速。LIFO。既知で固定サイズのデータに限る
  • ヒープ:低速。使いたいときは領域を確保 allocate して、そのポインタを返す

いきなり結構低レベルな話になってきた。このあたりの概念は Rust に特有ということでもない。

コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。

ここも一般的なコールスタックの話。

一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由

なるほど。

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

このルールは明快なので、「手っ取り早く解説」系の記事でも読んで覚えている。

変数は、宣言された地点から、現在のスコープの終わりまで有効になります。

よくある感じ。スコープが何を指すかの説明がないが、とりあえずは { ... } を思い描いておけばいいんだろうか。

Rustには、 2種類目の文字列型、String型があります。この型はヒープにメモリを確保するので、 コンパイル時にはサイズが不明なテキストも保持することができるのです。

ちなみに、もう一方の型(文字列リテラルの型)は &str 型のようだ。両者の名前が似ていて最初は???という感じだったが、よくある区別。

変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。[…] Rustは、閉じ波括弧で自動的にdrop関数を呼び出します。

デストラクタ的な。

他の言語を触っている間に"shallow copy"と"deep copy"という用語を耳にしたことがあるなら、 データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。 ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、 ムーブとして知られているわけです。

shallow copy だけだと二重解放の問題が起きるので、shallow copy っぽいことをしつつ、古い変数の方は無効にする。これがムーブ。なるほど、そういうことか。

ムーブによって「いかなる時も所有者は一つである」というルールが満たされる。すると、後は「所有者がスコープから外れたら、値は破棄される」というルールだけ用意しておけば、二重解放や、解放し忘れに悩まされることがなくなる。

Rustでは、 自動的にデータの"deep copy"が行われることは絶対にないわけです。それ故に、あらゆる自動コピーは、実行時性能の観点で言うと、 悪くないと考えてよいことになります。

ふむ。自動コピー automatic copy ってのは後で出てくるのかな。

型がCopyトレイトに適合していれば、代入後も古い変数が使用可能になります。

ルールはそんなに単純じゃなかった。他の言語も deep copyshallow copy が混ざったりするので、それと同じだといえば同じだが。プリミティブだけでなく、例えば (i32, i32)Copy らしい。

関数に変数を渡すと、 代入のようにムーブやコピーされます。

関数に変数を渡すと関数にムーブされる、というのはなんか独特な感じ。関数にムーブされた変数は、その関数を抜けるときに drop される。変数が Copy だったら、関数に渡すとムーブではなくコピーされる。参照渡しと値渡しのようなものではあるが…。

関数から値を返した場合は、drop されずに呼び出し元(の新たな変数)にムーブされる。

4.2. 参照と借用

参照と借用 - The Rust Programming Language 日本語版

fn main() {
    let s1 = String::from("hello");
    // s1 を渡すと calculate_length にムーブしてしまうが、次の println! でもう一度使いたいので、関数から返してもらう必要がある
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

受け取った変数をいちいち返すのは煩雑なので、参照 reference という仕組みがある。

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

変数名や型注釈に & をつける。C言語っぽい記法(忘れていたが、C の & はアドレス演算子という名前らしい)。

この&s1という記法により、s1の値を参照する参照を生成することができますが、これを所有することはありません。所有してないということは、指している値は、参照がスコープを抜けてもドロップされないということです。

意味の取りづらい日本語だが、原文を見てみるに、「&s1 って書くと s1 の参照を作ることができるけど、参照 &s1 は変数 s1 を所有するわけじゃないよ。参照 &s1 があるスコープを抜けたとしても、&s1 が参照している変数 s1 はドロップされないよ。&s1s1 を所有していないからね。」ということらしい。

関数の引数に参照を取ることを借用と呼びます。

borrowing。

変数が標準で不変なのと全く同様に、参照も不変なのです。

&foo は不変。&mut foo で渡せば、可変になる。

ところが、可変な参照には大きな制約が一つあります: 特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てないことです。

さらに不変な参照をしている間は、可変な参照をすることはできません。

競合を防ぐため。納得できる。一方で、不変な参照だけであれば、いくつも同時に作ることができる。

対照的にRustでは、コンパイラが、 参照がダングリング参照に絶対ならないよう保証してくれます

浮いた参照にならないように、コンパイルエラーにしてくれる。

4.3. スライス型

スライス型 - The Rust Programming Language 日本語版

所有権のない別のデータ型は、スライスです。

なるほど? 「別の」ということは、参照とも違う扱いなんだろうか。Python のスライスみたいなものかな?

for (i, &item) in bytes.iter().enumerate() {

このへんも Python っぽい。

let s = String::from("hello");
let slice = &s[3..]; // "lo"

sString 型のとき、文字列スライス &s[3..]&str 型。スライスとして表現することによって、元の変数に対する変更などをコンパイラでチェックすることができる。

let s = "Hello, world!";

ここでのsの型は、&strです: バイナリのその特定の位置を指すスライスです。 これは、文字列が不変である理由にもなっています。要するに、&strは不変な参照なのです。

な、なるほど…。そういう扱いなのか。


所有権まわりのメリットはだいぶ理解できた。