📌

【所有権と移動】プログラミングRust 第4章

2024/03/20に公開1

はじめに

本記事はプログラミングRust 第2版を読んで要点をもとに、情報を補完し、まとめた記事です。この書籍はとてもタメになりますがなかなかに難しいので、本記事と合わせて読むと少しは分かりやすいかな...?と思って書いています。諦める人が1人でも減りますように。

出典は本記事の最後に記載しています。
詳しく知りたい方は、書籍を購入して読むことをおすすめします。

1. 所有権

Rustでは全ての値はそのライフタイムを決定する「1つの」所有者を持ちます。
所有者が解放された時、所有されていた値も開放されます。※Rustではドロップという

変数は値を所有しますが、構造体はそのフィールド、タプル・配列・ベクタはその要素を所有します。
Box<T>の場合、BoxがドロップされるとそのT型の値もドロップされます。

所有者と所有される値はツリー構造をなす

所有者を親とし、所有される値を子とするツリー構造となっており、所有者がドロップされるとツリー全体が削除されます。

2. 移動

Rustでは、変数を別の変数へ格納(Rustでは束縛という)した場合や、関数への引数の受け渡しをした場合にコピーされずに「移動」します。

以下の例では、let child1 = parent;の時点で所有権がparentchild1に移動しています。その後parentchild2に束縛しようとしますが、所有権がparentに無いためエラーが発生します。

fn main() {
    let parent = String::from("parent");
    let child1 = parent;
    let child2 = parent; // ERROR: Stringの所有権がparentからchild1に移動したため、parentは使用できない
}

これを実現するにはclone()を使用します。
clone()はディープコピーを作成するため、所有権が移動しません。

fn main() {
    let parent = String::from("parent");
    let child1 = parent.clone();
    let child2 = parent.clone();
    println!("parent: {}, child1: {}, child2: {}", parent, child1, child2);
}

移動を伴う他の操作

所有権が移動するケースをいくつか紹介します。

  • 新しい値の作成(letキーワードなどで新しく値を作成した場合、その変数が所有権を持ちます)
  • 変数への束縛(先述の例)
  • 関数からの値の返却(関数の戻り値が返却される際、所有権も移動します)
  • 関数への値渡し(戻り値同様、引数に値を渡した場合所有権も移動します)

3. コピー型: 移動の例外

ほとんどの型で所有権の移動が発生しますが、発生しない型もあります。
このような型を「Copy型」と呼び、所有権が移動せず元の値も初期化した状態で使用できます。

これらの型は、Copyトレイトを実装しています。以下は、所有権が移動しない主な型のリストです。

  1. スカラー型
    • 整数型(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128
    • 浮動小数点型(f32, f64
    • 文字型(char
    • ブール型(bool
  2. 参照型
    • 共有参照(&T
    • スライス(&[T], &mut [T], &str
  3. タプル
    • 要素がすべてCopyトレイトを実装している場合、タプル全体がCopyトレイトを実装します。
      例:(i32, f64, bool)
  4. 配列
    • 要素がすべてCopyトレイトを実装している場合、配列全体がCopyトレイトを実装します。
      例:[i32; 4], [String; 0](空の配列)
  5. ユーザー定義型
    • ユーザーがCopyトレイトを手動で実装した型。ただし、型のすべてのフィールドがCopyトレイトを実装している必要があります。
#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1; // 所有権は移動しない
    println!("p1: ({}, {})", p1.x, p1.y); // p1: (1, 2)
    println!("p2: ({}, {})", p2.x, p2.y); // p2: (1, 2)
}

なぜCloneを実装しているの?

Cloneを実装しないとコンパイルエラーが発生します。

   Compiling hello v0.1.0 (/Users/totsuka/tmp/hello)
error[E0277]: the trait bound `Point: Clone` is not satisfied
   --> src/main.rs:1:10
    |
1   | #[derive(Copy)]
    |          ^^^^ the trait `Clone` is not implemented for `Point`
    |
note: required by a bound in `Copy`
   --> /Users/totsuka/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/marker.rs:472:17
    |
472 | pub trait Copy: Clone {
    |                 ^^^^^ required by this bound in `Copy`
    = note: this error originates in the derive macro `Copy` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Point` with `#[derive(Clone)]`
    |
2   + #[derive(Clone)]
3   | struct Point {
    |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `hello` (bin "hello") due to 1 previous error

Rustでは、Copyトレイトを実装するには、Cloneトレイトも一緒に実装する必要があります。

  • Cloneトレイトは、値をコピーするためのトレイトです。
  • Copyトレイトは、値を自動的にコピーするためのトレイトです。

Copyトレイトを実装するには、以下の2つの条件を満たす必要があります:

  1. 型のすべてのフィールドがCopyトレイトを実装していること。
  2. Cloneトレイトも実装されていること。

Copyトレイトを実装すると、値を代入するだけで自動的にコピーされます。Cloneトレイトを実装すると、cloneメソッドを呼び出して値を明示的にコピーできます。

つまり、Copyトレイトを実装するには、Cloneトレイトも一緒に実装する必要があるということです。

4. RcとArc: 所有権の共有

RcとArcとは

RcとArcは、Rustにおける参照カウントを使用したスマートポインタです。これらは、複数の所有権を持つデータを安全に共有するために使用されます。

  • Rc(Reference Counting):
    • Rcは、単一スレッド内で複数の所有権を持つデータを共有するために使用されます。
    • 参照カウントを使用して、データの所有者の数を追跡します。
    • 最後の所有者がいなくなると、データは自動的に解放されます。
  • Arc(Atomic Reference Counting)
    • Arcは、マルチスレッド環境で複数の所有権を持つデータを共有するために使用されます。
    • マルチスレッドを使用しない場合はRcを使用します。
use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    let data_clone = data.clone();

    println!("data: {:?}", data); // data: [1, 2, 3]
    println!("data_clone: {:?}", data_clone); // data_clone: [1, 2, 3]
}

cloneではダメなのか?

cloneでも同様の挙動をするものを作成することができますが、cloneはディープコピーであるため、データが大きい場合にメモリ使用量が増大する可能性があります。

対してRcでは複数の所有者がヒープ上の同じデータを共有できます。つまり、複数の変数がデータへの参照を持つことができます。

番外編: 参照カウントとは?

参照カウントは、プログラミングにおけるメモリ管理手法の一つです。オブジェクトやデータへの参照の数を数えることで、そのオブジェクトやデータがいつ解放できるかを決定します。Rust以外の言語でも広く使用されています。

参照カウントの基本的な仕組みは以下の通りです:

  1. オブジェクトやデータを作成すると、初期の参照カウントは1になります。
  2. 他の変数やオブジェクトがそのオブジェクトやデータを参照するたびに、参照カウントが1ずつ増加します。
  3. 参照が不要になり、変数やオブジェクトが参照を解放すると、参照カウントが1ずつ減少します。
  4. 参照カウントが0になった時点で、そのオブジェクトやデータは自動的に解放されます。

参照カウントを使用することで、プログラマはメモリの割り当てと解放を明示的に管理する必要がなくなります。システムが自動的にメモリを解放してくれるため、メモリリークを防ぐことができます。

Rust以外の言語でも、参照カウントはよく使用されます。例えば:

  • Python:Pythonのガベージコレクションは参照カウントを使用しています。
  • Objective-C:Objective-Cのメモリ管理はAutomatic Reference Counting(ARC)と呼ばれる参照カウント方式を使用しています。
  • Swift:SwiftもARCを使用してメモリ管理を行っています。

ただし、参照カウントにはいくつかの欠点もあります:

  1. 参照カウントの更新にはオーバーヘッドがあります。
  2. 循環参照が発生すると、メモリリークが発生する可能性があります。

Rustでは、RcArcを使用して参照カウントを実装しています。これにより、所有権システムの制約を緩和し、複数の所有権を持つデータを安全に共有できます。

出典

プログラミングRust 第2版
Jim Blandy、Jason Orendorff、Leonora F. S. Tindall 著、中田 秀基 訳
原書: Programming Rust, 2nd Edition
O'Reilly
https://www.oreilly.co.jp/books/9784873119786/

Discussion