2
1

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勉強】スマートポインタをざっくり

Last updated at Posted at 2022-04-28

はじめに

この記事は公式ドキュメントを読んでRustを勉強している時のメモを軽く清書したものです。

簡単に理解できたところはあまり書いてない傾向にあるので、全ては書いてありません。

みんな公式ドキュメント読もうね。あとタコピーの原罪も読もうね。

スマートポインタ

  • ポインタのように振る舞うかつ追加のメタデータと能力があるデータ構造。

  • 通常構造体を利用して定義されていて、普通の構造体との違いはDerefDropトレイトを実装していること。

  • ここで紹介するBoxRcRefcellは代表的なだけで他にもスマートポインタはある。

Box

  • Box<T>はスタックに格納されるデータをヒープに格納することができる。その時スタックに残るのはヒープへのポインタ。

Boxでヒープにデータを格納する

例えばこのプログラムはi32の値がヒープに保存される。

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);  // b = 5
}

通常の値と同じように、スコープを抜けた時「ヒープに格納されている値」と「スタックに格納されているポインタ」が開放される。

単独の値をヒープにおいてもあまり嬉しくないからこういう使われ方はあまりしない。

Boxで再帰的な型を定義する

再帰的な型は理論上無限に続く可能性があるので、コンパイル時にどれくらいのメモリ領域が必要なのか計算できない。そこで定義にBoxを挟むことで、Boxのさいずはわかっているので解決できる。

コンスリストをBoxで定義する

これは再帰的なリスト。なぜなら自身の要素のConsは引数に自分自身を含むから。

enum List {
    Cons(i32, List),
    Nil,
}

しかし、このままコンパイルすると無限のサイズが必要だと怒られる。

まず、コンパイラはこのenumを見た時に、Consの中身がどれくらいに領域が必要なのかを確認しようとする。Cons の中にはListが含まれているのでListの中をもう一度確認しに行く。この中にはConsが入っているので…と無限に続く。
そのため定義ができないのだ。

そこでConsに渡すListBoxでヒープに格納し、そのポインタを渡す。こうすることでコンパイラはポインタの領域だけを確保すれば良いのでコンパイルが通る。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

BoxがスマートポインタなのはDerefトレイトとDropトレイトがあるからだ。これらのトレイトのおかげで

  • Box<T>を参照のように扱うことができる

  • Box<T>値がスコープを抜けると、ボックスが参照しているヒープデータも片付けられる

の機能が実装されている。

Derefトレイト

Boxはヒープに保存されることを無視すると、1変数のタプルのような形をしている。

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

しかしこれだけでは、MyBoxは本家Boxのように*で参照外しはできない。それを解決するのがDerefトレイトである。

Derefトレイトは、標準ライブラリで提供されていてderefメソッドの実装を要求する。

derefメソッドは、selfを借用して内部のデータへの参照を返すメソッド。

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

MyBoxDerefトレイトを実装することで、*演算子が機能する。*xは実はRustは実際には*(x.deref())というコードを実行している。

参照外し型矯正

参照外し型矯正とは、Derefを実装している型への参照をDerefが元の型を変換できる型への参照に変換する機能。

??????????

具体的に見ましょう。こんなhello関数を考える。

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

helloは文字列スライスを引数として呼び出す。例えばhello("Takopi")みたいに。

helloMyBox<String>値とともに呼び出すには通常こうする必要がある。

fn main() {
    let m = MyBox::new(String::from("Takopi"));
    hello(&(*m)[..]);
}

helloの引数でmの型の推移はこう。

引数
m MyBox<String>
*m String
&(*m)[..] &str

最初mMyBox<String>で覆われているので*で参照外しできる。

*mStringなので&[..]で文字列全体のスライスをとって代入している。

うーんこれは面倒。しかし、参照外し型矯正のおかげでこう書ける。

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

コンパイラはDerefトレイトが実装されている型を自動的に参照外しする。

流れはこんな感じ。

引数 説明
m MyBox<String>
&m &MyBox<String>
&m &String MyBox<T>Derefを持ち、参照外し型矯正される
&m &str StringDerefを持ち、参照外し型矯正される

 &m&MyBox<T>を表しているが、MyBox<T>Derefがあるので、&MyBox<T>&Stringになる。またStringは標準でDerefが実装されているので、&String&strに変換される。

Dropトレイト

Dropは値がスコープを抜けそうになった時に起こることをカスタマイズできる。Dropはどんな型にも実装することができる。

Dropトレイト使う例を見よう。

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        // CustomSmartPointerをデータ`{}`とともにドロップするよ
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };      // 俺のもの
    let d = CustomSmartPointer { data: String::from("other stuff") };   // 別のもの
    println!("CustomSmartPointers created.");                           // CustomSmartPointerが生成された
}

CustomSmartPointerDropトレイトを持っていて、Dropトレイとはdropメソッドを持たなくてはならない。dropメソッドではドロップしたことを標準出力するようにしてある。

CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

このように出力される。dcより先にドロップしているのは変数は生成されたのと逆の順序でドロップされるからである。

この例では標準出力でドロップされたことを明示するだけだったが、通常は自分の型が使用した領域をクリーンアップするために使われる。

もしも、変数がドロップするよりも早く手動でドロップしたい場合にdropメソッドを直接呼び出すことはできない。std::mem::drop関数を利用する。

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    // CustomSmartPointerはmainが終わる前にドロップされた
    println!("CustomSmartPointer dropped before the end of main.");
}
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

mainが終了するよりも早くcがドロップしていることがわかる。

Rc<T>

単独の値が複数の所有者を持つ場合がある。複数の所有権を可能にするために使われるのがRc<T>という型。

ヒープにプログラムの複数箇所で読む何らかのデータを確保したいけど、コンパイル時にどの部分が最後にデータを使用し終わるかを決定できない時にRc<T>を使う。

Rc<T>はシングルスレッドでしか使用できないことに注意。

次の例は、Rc<T>を使わずにaの所有権を共有しようとするBox<T>の2つのリストを作るコード。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

aの所有権はbに移動しているから、cを作る時にaを使用しようとするとエラる。

Consの定義を借用するように変更することもできるが、それだとライフタイム引数を指定しなければならなくなり、リストの各要素が最低でもリスト全体と同じだけ生きることを宣言する必要が出てくる。

代わりにBox<T>Rc<T>に変更する。そしてbcを作る際はRc::cloneaRc<List>をクローンする。このRc::clonea.clone()とは違い、参照カウントに1を足すだけなので時間はかからない。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Rc<T>の参照カウントを得るためにはRc::strong_countを呼び出す。

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Rc<T>は単独の値に複数の所有者をもたせることができ、かつ所有者が存在している限り、値が有効であり続けることをカウントは保証してくれる。

RefCell<T>

  • コードの特性はコードを解析するだけではわからない時がある。例えば、プログラムの停止性問題など。
  • Rustコンパイラ不可能な分析にぶち当たった時は、正しいプログラムでも拒否する場合がある(保守的)。
  • コードが借用規則に従っているとプログラマは確証を得ている場合はRefCell<T>が有効。 

Rc<T>とRefCell<T>で可変データに複数の所持者をもたせる

Rc<T>を使ったコンスリストの例をもう一度思い出そう。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Rc<T>は不変値しか持たないので、一旦生成したリストの値はどれも変更できないが、
RefCell<T>と組み合わせること可変値をもたせることができる。

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

まずvalueRc<RefCell<i32>>のインスタンス値を生成して、valueという名前の変数に格納している。そしてコンスリストavalueを使って生成している。

bcの生成の時は、aの所有者を複数にするためにRc::cloneをしている。

abcを作った後、borrow_mutメソッドで中の値を変更する。この時自動参照外し機能が使われている。borrow_mutRefMut<T>を返し、それに対して参照外し演算子*を使用して中の値を変更している。

a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))

Box・Rc・RefCell使い分け

Box Rc RefCell
所有者 単独 複数 単独
不変借用
(コンパイル時の精査)
o o x
可変借用
(コンパイル時の精査)
o x x
  • RefCellは不変でもRefCell内の値を可変化できる。代わりに実行時に精査するだけなのでもし侵害した場合はpanic!を起こす。
2
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?