1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustの所有権を理解する

Posted at

前回の記事
https://qiita.com/revioness/items/6534b03432db5b9347a8

4. 所有権を理解する

所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。この章では、所有権以外にも、関連する機能を いくつか話していきます: 借用、スライス、そして、コンパイラがデータをメモリにどう配置するかです。

Rustのコアの部分なのでかなり細かくやっていくつもりです。

4.1 所有権とは

  • 他の言語には2種類のメモリの使用方法を管理する方法がある
    ガベージコレクション:定期的に使用していないメモリを検索してメモリを開放する
    明示的にメモリの確保や解放:C言語などで使われている手法。慣れていないとメモリの解放忘れなどで頻繁にメモリリークが起きる
  • Rustでは第三の方法として所有権システムというものがある

所有権規則

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

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

    fn main() {
        {                      // sは、ここでは有効ではない。まだ宣言されていない
            let s = "hello";   // sは、ここから有効になる
    
            // sで作業をする
        }                      // このスコープは終わり。もうsは有効ではない
    }
    

    sがスコープに入ると有効になる
    スコープを抜けるまで有効なまま
    この辺はほかの言語と同じ
    例えば

    function foo(){
        var s = 5;
        for(var i=0; i<3; i++){
            var m = 3;
        }
    }
    

    jsで書いているがこの場合sはfoo関数内で有効であり、mはfor文の中でのみ有効になっている
    この認識を元に次へ進む

スタックとヒープ

  • 公式ドキュメントは難しいので簡単な概念を説明する

スタックとヒープ

  • どちらも実行時に使用できるメモリ領域のこと
  • スタック
    • ラストインファーストアウトの意味
    • 簡単にいうとメモリに変数などを保存する際にメモリ領域が決まっており、追加される際に後ろにどんどん入れていく
    • 取り出す際には古いものから順に取り出していく
    • 新しいデータを置いたり取り出したりする時に場所を探す必要がないので非常に高速
    • しかし、可変の配列などを入れることができない(データを入れた瞬間にケツのメモリが決まっているのでメモリが後ろのデータとかぶってしまうため)
  • ヒープ
    • ヒープ領域にデータを格納する場合はある程度の空きを持ったメモリ領域を確保し、メモリのポインタ(場所)を返す
    • ヒープ領域の場合はメモリのアドレスの場所を探さなければいけないのでスタックより低速
    • もしそのある程度の空きを持ったメモリ領域のメモリがいっぱいになってしまったら再度割当て直すようになっている

ここではこの程度の認識でよい

String型

  • 例としてString型の所有権にまつわる部分を扱う
let s = String::from("hello");

文字列リテラル(固定長)からString型(可変長)をこのように生成できる

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

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

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

メモリの確保

  • 文字列リテラルの場合は固定長のため、コンパイル時にバイナリファイルに変換され"直接"ハードコーディングされる
    • このため文字列リテラルは高速
  • String型は可変な文字列を実装するためにコンパイル時には不明な量のメモリをヒープ領域に確保するようになっている
    • この時二つの手順が発生する

      メモリは、実行時にOSに要求される。(ヒープ領域への割当て)
      String型を使用し終わったら、OSにこのメモリを返還する方法が必要である。(メモリの解放)
      
    • メモリは、実行時にOSに要求される。(ヒープ領域への割当て)

      • これは宣言時に自動で行われている
        let s = String::from("hello");
        
    • String型を使用し終わったら、OSにこのメモリを返還する方法が必要である。(メモリの解放)

      • これが所有権にまつわる話になっている
        {
            let mut s = String::from("hello");
        
            s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える
        
            println!("{}", s); // これは`hello, world!`と出力する
        }
        
        こんなコードがあった時に、どこでメモリを解放するのが自然だろうか
        もちろんsがスコープを抜ける瞬間(以降使用されなくなる瞬間)
        println!("{}", s); // これは`hello, world!`と出力する
        ここの後だろう
        Rustはこのタイミングでsのdrop関数というものを呼び出す
        これによってsに割り当てられていたメモリは自動的に開放される
        仮にjsやpythonなどの言語をやっていた場合はGCが動くまでメモリは確保されたままである

変数とデータの相互作用法: ムーブ

  • Rustは複数の変数が同じデータに対して異なる手段で相互作用することができる

    let x = 5;
    let y = x;
    
    println!("x:{}", x);
    println!("y:{}", y);
    
    x:5
    y:5
    

    この場合は当然このように表示される
    では以下の場合はどうなるだろうか

    let s1 = String::from("hello");
    let s2 = s1;
    
    println!("s1:{}", s1);
    println!("s2:{}", s2);
    
    error[E0382]: borrow of moved value: `s1`
    --> src\main.rs:5:23
    |
    2 |     let s1 = String::from("hello");
    |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
    3 |     let s2 = s1;
    |              -- value moved here
    4 |
    5 |     println!("s1:{}", s1);
    |                       ^^ value borrowed here after move
    |
    = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
    
    For more information about this error, try `rustc --explain E0382`.
    error: could not compile `own` due to previous error
    

    なぜこうなってしまうのか
    String型は以下のようにメモリ上に保存されている

    • ポインタ(ヒープ領域のポインタ)
    • 長さ
    • 容量
      つまりここには実際に入っている値は保存されていない(文字列リテラルの場合は先ほど言ったように、固定長なので文字列がそのままハードコーディングされる)
      この状態でs1s2を代入すると

    s1

    • ポインタ
    • 長さ
    • 容量

    s2

    • ポインタ
    • 長さ
    • 容量

    当然このように同じものが二つできる
    つまり、s1s2は同じヒープポインタを指すものが入る(値はコピーされていないので一つしかない)
    もし仮に以下の状態で先ほど説明したdrop関数が呼び出されると

    let s1 = String::from("hello");
    let s2 = s1;
    //ここでs1 drop
    
    println!("s2:{}", s2);
    //ここでs2 drop
    

    これは2重解放エラーとなる(そもそもs1をprintする時点でエラーになるが)
    これを避けるためにRustはs1s2に代入された時点で有効でないと判断し何もしなくなる
    そのためdropもしないし参照もできなくなる
    これが所有権の考え方
    この場合はs1に入っていたString型のhelloの持ち主はs2になる(これをムーブと呼ぶ)

    • もしどちらの変数も使いたい場合は?
      • その場合はよくあるメソッドとしてcloneメソッドがある。例えば以下のようなコードはコンパイルが通る
        let s1 = String::from("hello");
        let s2 = s1.clone();
        
        println!("s1 = {}, s2 = {}", s1, s2);
        
        cloneメソッドが実行された時にs1helloが新たにヒープメモリに追加され、そこへのポインタがs2に入るようになっている
        これをdeep copyと呼ぶ

スタックのみのデータ: コピー

  • では以下のコードはどうだろうか
    let x = 5;
    let y = x;
    
    println!("x = {}, y = {}", x, y);
    
    これはコンパイルが通るようになっている
    なぜかというと、これは5はイミュータブルであり、固定長であるため、スタック領域を使用しており、値のコピーも簡単だからである(わざわざxを無効化する理由がない)

所有権と関数

  • ここまでムーブとコピーを学んだが、関数に値を渡すことは、ムーブとコピーに似ている。以下のコードがあったとする
fn main() {
    let s = String::from("hello");  // sがスコープに入る

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

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

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

} // ここでxがスコープを抜け、sもスコープを抜ける。ただし、sの値はムーブされているので、何も特別なことは起こらない。
  //

fn takes_ownership(some_string: String) { // some_stringがスコープに入る。
    println!("{}", some_string);
} // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。
  // 

fn makes_copy(some_integer: i32) { // some_integerがスコープに入る
    println!("{}", some_integer);
} // ここでsome_integerがスコープを抜ける。何も特別なことはない。

基本的にはコメントにある通り
仮に

takes_ownership(s);
println!({}, s);

このようにするとコンパイルエラーになる
関数に値を渡すと、その値の所有権は関数に奪われ、sは無効化されてしまう
ではこの場合はどうだろう

makes_copy(x);
println!({}, x);

この場合はコンパイルが通る
xはi32型になっているので、コピーされるため

Copyされる型は以下(一部)

  • あらゆる整数型。u32など。
  • 論理値型であるbool。trueとfalseという値がある。
  • あらゆる浮動小数点型、f64など。
  • 文字型であるchar。
  • タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。

戻り値とスコープ

fn main() {
    let s1 = gives_ownership();         // gives_ownershipは、戻り値をs1に
                                        // ムーブする

    let s2 = String::from("hello");     // s2がスコープに入る

    let s3 = takes_and_gives_back(s2);  // s2はtakes_and_gives_backにムーブされ
                                        // 戻り値もs3にムーブされる
} // ここで、s3はスコープを抜け、ドロップされる。s2もスコープを抜けるが、ムーブされているので、
  // 何も起きない。s1もスコープを抜け、ドロップされる。

fn gives_ownership() -> String {             // gives_ownershipは、戻り値を
                                             // 呼び出した関数にムーブする

    let some_string = String::from("hello"); // some_stringがスコープに入る

    some_string                              // some_stringが返され、呼び出し元関数に
                                             // ムーブされる
}

// takes_and_gives_backは、Stringを一つ受け取り、返す。
fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。

    a_string  // a_stringが返され、呼び出し元関数にムーブされる
}

これもコメントの通り
戻り値を受け取ることで、関数内の変数にあった所有権を、関数外の値にムーブすることができる
実際これだとかなりめんどうくさい

  • 変数を定義
  • その変数を関数に渡す(この時点で所有権はなくなる)
  • 戻り値を受け取る用の変数を用意してその変数に関数の戻り値を入れる
    それを解消するのが次の章

4.2 参照と借用

  • 関数に参照を渡すことで値は変更できないが、値を見ることができるようになる
    //これが上でやったもの
    let s = String::from("hello");
    takes_and_gives_back(s);
    fn takes_and_gives_back(a_string: String) -> String {
        a_string;
    }
    
    //参照を渡すパターン
    let s = String::from("hello");
    takes_and_gives_back(&s); //前に&をつけることで参照を生成することができる
    fn takes_and_gives_back(a_string: &String) -> String { //受け取る側も参照を受け取っていると明記する必要がある
        a_string;
    }
    
    これは参照を渡しているだけなので、参照先の値は変更することができない
    所有権は渡されていないのでa_stringのスコープを抜けても、参照が指しているものはdropされることはない
    このように参照を渡すことを借用という

参照を変更したい場合は?

  • 参照を渡すことで所有権を失わずに関数へ値を渡すことができるということがわかったが、仮にその参照先の値を変更したい場合はどうすればいいか
    • 簡単にいうと、参照をミュータブルにしてしまえばいい

      fn main() {
          let mut s = String::from("hello");
      
          change(&mut s);
          println!("{}", s);
      }
      
      fn change(some_string: &mut String) {
          some_string.push_str(", world");
      }
      

      この場合の出力は以下

      hello, world
      

      可変参照には制約があり、同時に一つしか可変な参照は取れない
      以下の場合はエラーになる

      let mut s = String::from("hello");
      
      let r1 = &mut s;
      let r2 = &mut s;
      
      println!("{}, {}", r1, r2);
      

      以下のパターンもエラーになる

      let mut s = String::from("hello");
      
      let r1 = &s; // 問題なし
      let r2 = &s; // 問題なし
      let r3 = &mut s; // 大問題!
      

      r1r2はあくまで参照を渡しているだけなのでいくつ渡しても値を変更されることがないので問題ないが、参照を渡している部分が一つでもある場合は可変参照は渡すことはできない

4.3 スライス型

  • 勉強次第追記します
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?