LoginSignup
0
1

Rust the book 要約 4章:所有権を理解する

Last updated at Posted at 2023-09-05

目次

4.0 概要

  • 所有権とは
  • 参照
  • 借用
  • スライス
  • コンパイラはデータをメモリにどう配置するか

4.1 所有権とは

所有権は「ヒープ領域にメモリ領域を確保したり、それを解放する機能を言語側で管理するための概念」

所有権により以下が可能になる

  • どの部分のコードがどのヒープ上のデータを使用するかの把握
  • ヒープ上のデータの重複の最小化
  • メモリ不足の防止

コラム: 「スタックとヒープ」

スタック

  • コンパイル時に必要メモリサイズが分かっているデータを保持
  • データは固定長
  • 得た順にデータを並べ、逆順で取り除く

ヒープ

  • コンパイル時には必要メモリサイズが不明なデータを保持
  • OS が管理
  • OS に、ある大きさのメモリ領域をヒープ領域に確保するよう依頼すると、OS はメモリ領域を確保したのちに、そのメモリ領域のベースアドレスを返す
  • そのポインタを指定してオブジェクトの破棄をOSに依頼すると、メモリ領域が解放され、貸出可能状態に戻る

所有権の規則

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

例1: str 型

{
  // *1
  let s = "hello"; // *2
  // *3
} // *4
  • 上記のコード内で変数s にはプログラムにハードコードされた"hello"という文字列への参照が束縛される
  • この文字列への参照は、s が宣言された行 (*2 の時点) から s がスコープから抜けるまで (*3 まで) はメモリ上に存在するが、それ以外の領域では存在しない

例2:ヒープに値を保持する場合(String型)

{
  // *1
  let s = String::from("hello"); // *2
  // *3
} // *4

上記のコードでは

  • 変数 s の宣言時に OS に(ヒープ領域内での)メモリ領域の確保が依頼され
  • 確保された領域のベースアドレスなどの情報( pointer, size, capacity )が s に束縛される
  • s に束縛された値は *2 ~ *3 の範囲ではメモリ上に存在するが、それ以外の領域では存在しない
  • また、ヒープ領域に確保されたメモリ領域は s がスコープを抜けると同時に解放される
    • より厳密には、以下のような仕組みになっている:
      • Rust では、閉じ括弧 } が自動的に(該当スコープで有効なすべての(Dropトレイトが有効な)変数の) drop 関数を呼び出す
      • drop はその変数と結びついたヒープ領域内のメモリ領域を解放するよう定義されている

二重解放エラーを防ぐ仕組み(ムーブセマンティクス)

  • Rust では2つ以上の異なる変数に、同じヒープ領域へのポインタが束縛されることはない
    • ヒープ領域と結びついたある変数 a をほかの変数 b に代入 let b = a すると、a はそれ以降無効化される

例:String型の変数のムーブの挙動

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

によって

  • ヒープ領域内に b'h', b'e', b'l', b'l', b'o' の一連のデータが保持され
  • このメモリ領域へのポインタがスタック領域に保持されたとする
  • この時、このポインタは変数s1に束縛される

ここでさらに

let s2 = s1;

とすると

  • 変数 s2 にヒープ領域へのポインタが束縛され
  • 変数 s1 は無効化される
  • 無効化された変数を利用しようとするとコンパイルエラーが発生する
  • たとえば以下のコードコンパイルエラーを起こす
    let s1 = String::from("hello");
    let s2 = s1; // この時点で s1 は無効化される
    println!("s1: {}, s2: {}", s1, s2);
    

ヒープ領域内のデータを複製する方法(クローン)

  • スタック上のデータだけではなくヒープ領域内のデータの deep copy が必要なら clone メソッドを用いる
  • たとえば以下のコードは問題なく動く
    let s1 = String::from("hello");
    let s2 = s1.clone(); // clone メソッドを使うことでヒープ領域上のデータごと複製されるため、s1 は破棄されない
    println!("s1: {}, s2: {}", s1, s2);
    

スタックのみのデータの複製(コピー)

  • スタック上で完結するデータ型には Copy トレイトが実装されていることが期待される
  • Copy トレイトが実装された型の値はムーブされずにコピーされる
  • Drop トレイトを実装した型、もしくは Drop トレイトを実装した型を一部に含む型には Copy トレイトの実装ができない(コンパイルエラーを起こす)
  • 以下は自動的に Copy
    • スカラー型
    • Copy の型だけを含むタプル

所有権と関数

  • 関数に変数を渡すと、その変数のもつ所有権が関数内に移動する
    • 変数を別の変数に代入した場合と同様に、あるヒープ領域と結びついた変数 a を関数 f に渡す( つまりf(a)のようにする) と、a はそれ以降無効化される

例:所有権と関数

fn main() {
  let s = String::from("hello");          // sがスコープに入る
  takes_ownership(s);                     // 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がスコープを抜ける。何も特別なことはない。

戻り値とスコープ

  • 関数が値を返すことでも、所有権は移動する

例:戻り値とスコープ

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 参照と借用

参照

  • 変数 hoge の参照は &hoge で取得できる

  • Hoge の変数の参照の型は &Hoge となる

  • &hoge には hoge へのポインタ(=「hoge に束縛された(ヒープデータへの)ポインタ」へのポインタ)が束縛される

  • &hoge がスコープを抜けても hoge に結びついたヒープデータはドロップされない

  • &hoge がスコープを抜けても hoge に束縛された(ヒープデータへの)ポインタはドロップされない

  • &hoge がスコープを抜けると &hoge 束縛された hoge へのポインタが消去される

  • 参照外し演算子 * で参照を外せる

  • 例:以下のコードに登場する s とその参照 s = &s1 の関係は下図の通り;

    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()
    }
    

参照と関数

  • 関数の引数に参照を取ることを借用と呼ぶ

可変参照

  • 参照はデフォルトではimmutable(不変)
  • 可変変数 hoge の可変参照は &mut hoge で取得できる
  • 例:
    fn main() {
      let mut s = String::from("hello");
    
      change(&mut s);
    }
    
    fn change(some_string: &mut String) {
      some_string.push_str(", world");
    }
    

参照についてのルール

「参照は、一つの可変参照か複数の不変参照」

「ダングリング参照は許されない」

1. 一つのスコープ内では、一つのデータに対して、一つしか可変な参照を作れない

  • このルールにより、複数のポインタが同じデータに同時にアクセスすることで発生する問題などを防げる
  • 例1:以下のコードはコンパイルエラーを起こす
    let mut s = String::from("hello");
    
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
    
  • 例2:以下のコードは問題なく動く
    let mut s = String::from("hello");
    {
      let r1 = &mut s;
    } // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる
    
    let r2 = &mut s;
    

2. 不変参照と可変参照は同一スコープ内で共存できない

3. 不変参照は複数共存可能

  • 以下のコードはコンパイルエラーを起こす
    let mut s = String::from("hello");
    
    let r1 = &s; // 問題なし
    let r2 = &s; // 問題なし
    let r3 = &mut s; // 問題あり!
    

4. ダングリング参照はコンパイラエラーで阻止される

  • 以下のコードはコンパイルエラーを起こす
    fn main() {
      let reference_to_nothing = dangle();
    }
    
    fn dangle() -> &String {
      let s = String::from("hello");
    
      &s
      // ここで s はドロップされる
      // --> ドロップ済みの値への参照をこの関数の外側にムーブしようとしていることになる
      // --> 無効な参照を作る不法な操作なのでコンパイラに怒られる
    }
    

4.3 スライス型

  • スライスにより、コレクション全体ではなく、その内の一連の要素を参照することができる

  • スライスは、&hoge[1..3] のような形で取得する

  • スライスは、元のコレクションオブジェクトの不変参照である

  • スライスは、通常の参照と異なり、スタック上のポインタへのポインタではなく、ヒープデータへのポインタを保持する

    • 内部的には先頭へのポインタと長さを保持する(下図は、s = String::from("hello world"), world = &s[6..] を示す)
  • このような成り立ちのために、スライスのスライスはスライスである(型は変わらない)

    • ただし、元のコレクションオブジェクトとは型が異なる
      • たとえば、String 型に対するスライスは &str, &str のスライスは &str
      • たとえば、[i32; 10] 型に対するスライスは &[i32], &[i32] のスライスは &[i32]
  • 一方、Hoge 型の変数への参照は &Hoge であり、その参照への参照は &&Hoge であるので注意

    // `String` のスライスは `&str`
    // `&str` のスライスは `&str`
    fn main() {
        let s: String = String::from("hello");
        let slice: &str = &s[1..4];     // ell
        let slice: &str = &slice[1..];  // ll
        let slice: &str = &slice[..1];  // l
    
        println!("slice: {}", slice);
    }
    
    // `[i32; 10]` のスライスは `&[i32]`
    // `&[i32]` のスライスは `&[i32]`
    fn main() {
        let a:[i32; 10] = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5];
        let slice: &[i32] = &a[1..9];     // [2, 3, 4, 5, 1, 2, 3, 4]
        let slice: &[i32] = &slice[..7];  // [2, 3, 4, 5, 1, 2, 3]
        let slice: &[i32] = &slice[3..];  // [5, 1, 2, 3]
        println!("slice: {:?}", slice);
    }
    
    // `String` の参照は `&String`,
    // `&String` の参照は `&&String`
    fn main() {
        let s: String = String::from("hello");
        let s: &String = &s;
        let s: &&String = &s;
    }
    
  • また、&&..&Hoge のスライスを取るのと、Hoge のスライスを取るのは同等らしい(Rust 側で参照外しをやってくれるっぽい?)

    fn main() {
        let s: String = String::from("hello");
        let s: &String = &s;
        let s: &&String = &s;
        let s: &&&String = &s;
        let slice: &str = &s[1..4];     // ell
    
        println!("slice: {}", slice);
    }
    

参考文献

0
1
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
0
1