ちゃなべの備忘録

ほぼ備忘録です。

Rustを触ってみた③【備忘録】

ayumu1212.hatenablog.com

こちらの続きです。

4. Understanding Ownership

Ownership(所有権)という機能についてです。メモリ管理のためのRustのユニークな機能です。

borrowing, slices, メモリ上のデータ配置について説明する章らしい。

4.1. What Is Ownership?

Rustはメモリ管理が特殊です。「GCを使って定期的に未使用のメモリをクリアする」「明示的にメモリの割り当て、解放を行う」とは違う第三の方法をとっており、それがOwnershipです。

The Stack and the Heap

スタックとヒープはどちらもメモリ構造について。

  • Stack: LIFO。データは固定サイズ。
  • Heap: 順番はない、ただの山積み。容量を用意して、空いてたらそこに入れていく。

StackはHeapよりも割り当てが高速。Stackはすでにある場所に入れていくだけなので。Heapは空き容量を探さないといけない。

StackはHeapよりもアクセスが高速。どうやらStackの方がメモリ的に近い場所にあるらしい。

...え?Heapなんでつかうん?w

コードが関数を呼び出すと、関数に渡された値とローカル関数がスタックにpushされる。実行されるとpopする。

これあれか、プログラム意味論で学んだ考え方だな。Operational Semantics。

Ownership Rules
  • 各値にはownerがいる
  • ownerは一度に一人しかいない
  • ownerがスコープ外に出ると、その値はなくなる
Variable Scope

スコープは、他のプログラミングの概念と同じで大丈夫。こんな感じ。

    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }  
The String Type

ownerという観点からStringを見てみる。stringリテラルとStringは違うらしい。

let sl = "hello"; // リテラル
let so = String::from("hello"); // String型

そして後者は可変化することができるらしい

let mut s = String::from("hello");

s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える

println!("{}", s); // これは`hello, world!`と出力する

メモリを扱う方法がどうやら違うらしい。

Memory and Allocation

文字列リテラルの場合はコンパイル時に容量がわかるので高速。実行時にしか容量がわからないものは、String型で使える。

メモリの確保と解放は1対1に対応

Variables and Data Interacting with Move

以下の2つが異なる意味になるらしい。

let x = 5;
let y = x;
let s1 = String::from("hello");
let s2 = s1;

上の場合はどちらもStackに積まれるけど、下の場合はポインタの参照が同じになるらしい。

何だけど、ポインタの参照を同じにしていると、解放する時に二重解放になっちゃうかもしれないらしくて、そのためにRustでは受け渡しが終わった段階で、元の変数は使えなくなる。これを move という。

Variables and Data Interacting with Clone

もし、以下の画像のようなことをしたかったら、cloneを使いましょう。

let s1 = String::from("hello");
let s2 = s1.clone();
Stack-Only Data: Copy

じゃあこれはmoveしないの?しないらしい。は?

let x = 5;
let y = x;

これは、「コンパイル時にわかるサイズを持つデータはコピーしても高速だから、別にmoveする必要がなくcloneのような挙動でいいよね」っていう考えらしい。

そして、moveが起こるかどうかは Copy traitというアノテーションがあるかどうかで判定される。これがあれば、代入後も古い変数が使えるよ。

integer, floating-point, boolean, characterはCopy traitがあるよ。

tupleは中身がCopy traitを含むやつだけの時、(i32, String)とかはダメ。

Ownership and Functions

関数に入れた時は、スコープを外れる時だよ。Copy traitがないと厳しい。

fn main() {
    let s = String::from("hello");  // sがスコープに入る

    takes_ownership(s);             // sの値が関数にムーブされ...
                                    // ... ここではもう有効ではない

    let x = 5;                      // xがスコープに入る

    makes_copy(x);                  // xも関数にムーブされるが、
                                    // i32はCopyなので、この後にxを使っても
                                    // 大丈夫

}
Return Values and Scope

関数に入れる時も、戻る時もmoveが起こるが、「所有権を元の変数に戻したい時」は厄介です。以下のような関数を書かないといけないですが、そんなめんどくさいことしなくても referenceという機能を使えばいけるらしい。

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

4.2. References and Borrowing

↑で話した「所有権を渡したくない時」は参照を使いましょう。

こうすれば、所有権を渡さなくていいらしい。

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    // '{}'の長さは、{}です
    println!("The length of '{}' is {}.", s1, len);
}

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

参照を用いて所有権を一時拝借することを borrowingという。借りたものを勝手に変えちゃいけない制約がある。参照は不変です。

以下はエラーが出ます。

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
Mutable References

変更可能な借用ができる。これ。

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

そして、変更可能な借用は一度に複数貸すことができない。逆に変更不可だったらいける。

    let mut s1 = String::from("hello");

    let r1 = &mut s1;
    let r2 = &mut s1; // error

    let s2 = String::from("hello");

    let r1 = &s2;
    let r2 = &s2; // ok

この制約を設けることで、データ競合が起こらない。

Dangling References

ダングリングポインタというものがあってだな、他人に渡された可能性のあるメモリを解放してしまった時発生する宙に浮いたポインタのことです。

それを発生させないようにRustでは、ポインタがスコープを抜けるまでデータがスコープを抜けることがないように確認してくれます。

これをコンパイルエラーしてくれる

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

4.3. The Slice Type

とある変数の中に入っている文字の、文字数を別の変数に保存する。

そして元となった変数を消した時、文字数を保存している変数が消えないことが嫌らしい。...そうか?もちろん同期してくれたらめっちゃ楽だけどそんなことできるんか?

できるんです、Rustなら。

まずはsliceのやり方。

let s = String::from("hello");

let slice = &s[0..2]; // "he"
let slice = &s[..2]; // "he"
let slice = &s[2..]; // "llo"
let slice = &s[..]; // "hello"

ちなみに、&strというスライスを表す型は&Stringの意味を包含します。

文字列だけじゃなくて、配列に対するスライスもあるよ。

let a = [1,2,3,4,5];

let slice = &a[1..3]; // &[i32]型

視覚的な概念図はこんな感じ。

所有権がsにずっとあるから、sが消えることに対してのアラートを出すことができるんだ。所有権と所有権を持っていない関係が、片方だけ消えるってことを抑制しているんだね。