はじめに
この記事は公式ドキュメントを読んでRustを勉強している時のメモを軽く清書したものです。
簡単に理解できたところはあまり書いてない傾向にあるので、全ては書いてありません。
みんな公式ドキュメント読もうね。あとタコピーの原罪も読もうね。
スマートポインタ
-
ポインタのように振る舞うかつ追加のメタデータと能力があるデータ構造。
-
通常構造体を利用して定義されていて、普通の構造体との違いは
Deref
とDrop
トレイトを実装していること。 -
ここで紹介する
Box
、Rc
、Refcell
は代表的なだけで他にもスマートポインタはある。
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
に渡すList
をBox
でヒープに格納し、そのポインタを渡す。こうすることでコンパイラはポインタの領域だけを確保すれば良いのでコンパイルが通る。
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
}
}
MyBox
にDeref
トレイトを実装することで、*
演算子が機能する。*x
は実はRustは実際には*(x.deref())
というコードを実行している。
参照外し型矯正
参照外し型矯正とは、Deref
を実装している型への参照をDeref
が元の型を変換できる型への参照に変換する機能。
??????????
具体的に見ましょう。こんなhello
関数を考える。
fn hello(name: &str) {
println!("Hello, {}!", name);
}
hello
は文字列スライスを引数として呼び出す。例えばhello("Takopi")
みたいに。
hello
にMyBox<String>
値とともに呼び出すには通常こうする必要がある。
fn main() {
let m = MyBox::new(String::from("Takopi"));
hello(&(*m)[..]);
}
hello
の引数でm
の型の推移はこう。
引数 | 型 |
---|---|
m |
MyBox<String> |
*m |
String |
&(*m)[..] |
&str |
最初m
はMyBox<String>
で覆われているので*
で参照外しできる。
*m
はString
なので&
と[..]
で文字列全体のスライスをとって代入している。
うーんこれは面倒。しかし、参照外し型矯正のおかげでこう書ける。
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 |
String はDeref を持ち、参照外し型矯正される |
&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が生成された
}
CustomSmartPointer
はDrop
トレイトを持っていて、Drop
トレイとはdrop
メソッドを持たなくてはならない。drop
メソッドではドロップしたことを標準出力するようにしてある。
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
このように出力される。d
がc
より先にドロップしているのは変数は生成されたのと逆の順序でドロップされるからである。
この例では標準出力でドロップされたことを明示するだけだったが、通常は自分の型が使用した領域をクリーンアップするために使われる。
もしも、変数がドロップするよりも早く手動でドロップしたい場合に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>
に変更する。そしてb
、c
を作る際はRc::clone
でa
のRc<List>
をクローンする。このRc::clone
はa.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);
}
まずvalue
にRc<RefCell<i32>>
のインスタンス値を生成して、value
という名前の変数に格納している。そしてコンスリストa
をvalue
を使って生成している。
b
、c
の生成の時は、a
の所有者を複数にするためにRc::clone
をしている。
a
、b
、c
を作った後、borrow_mut
メソッドで中の値を変更する。この時自動参照外し機能が使われている。borrow_mut
はRefMut<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!
を起こす。