rustのBoxについて理解する

rust

Boxをひとことで

値をヒープ領域に置く

Boxの仕組み

Boxの仕組みを理解するために、以下のようなコードを書いてみます。

fn main() {
    let a = Box::new(10);
    let b = 10;
    println!("{:p}",&a);
    println!("{:p}",a);
    println!("{:p}",&b);
}

実行結果

0x16f389ed8
0x6000004f8050
0x16f389ee4

実行結果を元に値がどのように配置されているか図示してみました。

aが束縛されているアドレスは0x16f389ed8で、そこには0x6000004f8050という値が保存されています。この値はBox::newの引数として使った10が格納されているアドレスです。a,bが束縛されているアドレスとは全然値が違うことがわかります。これは、a,bが持つ値がスタックにあり、Boxが指す10がヒープにあることからくる違いだと思います。

ただ、一般的にスタックは高速で、ヒープよりも速いので、わざわざ値をヒープに置く意味はありません。ヒープに値を置く必要があるのは、実行時にサイズが決まるデータ実行時にサイズが変更されるデータです。

Vecなどはヒープにデータが置かれます。

Boxを使ってあるトレイトを実装している型を返す

Boxを使う場面としてよく見るのが、「あるトレイトを実装している型」を返す関数です。例えば、エラー処理などで、std::error::Error型を実装した型を返す時などです。以下で、簡単に「あるトレイトを実装している型」を返す関数を書いてみます。

「トレイトとは?」という人は以下をご参考ください。

Liquidトレイトを実装する2つの構造体を定義

まず、トレイトと構造体の定義を見てみます。WaterとOilという構造体があり、どちらもLiquidというトレイトを実装しています。

trait Liquid {
    fn amount(&self) -> usize;
    fn double_amount(&self) -> usize {
        self.amount() * 2
    }
}

struct Water {
    amount: usize,
}

impl Liquid for Water {
    fn amount(&self) -> usize {
        self.amount
    }
}

struct Oil {
    amount: usize,
}

impl Liquid for Oil {
    fn amount(&self) -> usize {
        self.amount
    }
}

Liquidトレイトを実装した型を戻り値とする関数

NGパターン

場合により、WaterかOilを返すような関数を書いてみると以下のようになります。

fn new_liquid(s: &str) -> dyn Liquid { // <- not allowed!
    if s == "water" {
        Water { amount: 10 }
    } else {
        Oil { amount: 10 }
    }
}
fn main() {
    let some_liquid = new_liquid("water");
}

ここでdyn Liquidは「Liquidを実装している型」を表します。

さて、上記のコードはエラーになります。これは、関数が実行されるまで、戻り値の型が決まらないからです。今は、Water型が戻り値となりますが、例えば、引数がユーザーの入力によって動的に決まる場合などは、型が決まらなくなります。

OKパターン

型を決める必要があるのは、rustがデフォルトでデータをスタックに置こうとしているからです。スタックは高速なのですが、データの大きさが決まっている必要があります。従って解決策として、データをヒープに置くようにします。ここでBoxが必要になってきます。Boxを使って、戻り値として「Liquidトレイトを実装したデータへのアドレス」を表現します(これをトレイトオブジェクトと言います)。戻り値はアドレスとなるので、データのサイズは決まります。

fn new_liquid(s: &str) -> Box<dyn Liquid> {
    if s == "water" {
        Box::new(Water { amount: 10 })
    } else {
        Box::new(Oil { amount: 10 })
    }
}
fn main() {
    let some_liquid = new_liquid("water");
    println!("{:p}", &some_liquid);
    println!("{:p}", some_liquid);
}

これで、どちらを返すか動的に切り替えることができます。

上記のコードを実行してみると、アドレスは以下のようになっています。

実行結果

0x16d96df48
0x600002cfc050

これを図示してみると、下の図のようになっています。some_liquidはスタックにあるアドレスに束縛されています。このアドレスは、Liquidトレイトを実装した何かのデータをさしています。

最後に

今回はBoxについての内容をまとめました。初めてBox<dyn ~>みたいなコードを見た時は意味不明でしたが、落ち着いて学んでみると、そんなに難しい話ではなかったです。Boxはスマートポインタの代表的な例として、TRPLで出てきます。そこでは、コンスリスト(自分自身と同じ型を要素に持った型)の記載がありますが、実際にこんな使い方はしない(少なくとも当面僕はしないと思っています)ので、トレイトオブジェクトを返すときに使う使い方が一般的かなと思います。

スマートポインタについては、以下でまとめていますので、時間があればご覧ください。

それではまた次回お会いしましょう。

コメント

タイトルとURLをコピーしました