項目8:参照型とポインタ型に慣れよう
https://effective-rust.com/references.html
参照とポインタ
プログラミング全般における 参照 とは、データ構造を間接的にアクセスする手段
データ構造を所有する変数とは別に扱う
参照は ポインタ(データ構造のメモリ上のアドレスを値として持つ数値)として実装される
現代の CPU では、ポインタに対していくつか制約を課すのが一般的
メモリアドレスが有効なメモリ範囲内にあること
アラインメント されていること
e.g. 4 Byte の整数は、アドレスが 4 の倍数の場合のみアクセス可能
生ポインタ
C や Rust などの高級言語では、ポインタはメモリアドレスに存在する データ構造の型情報を持つ
これにより、そのアドレスが指すメモリとそのアドレスに続くメモリの内容を解釈することが可能になる
Rust では、このようなポインタは 生ポインタ として表現されるが、通常生ポインタを扱うことはない
代わりに、参照型 や ポインタ型 を用いる
Rust の参照(&T, Box<T>)
ある型 T に対する参照は &T と表現する
Rust における参照は内部的にはポインタだが、コンパイラは使用にいくつか制約を課す
有効で正しいアラインメントを持つ T 型のインスタンスを常に指していること
利用期間を超えて有効であること(生存期間)
借用ルール を満たすこと
warning.icon Rust の 参照は上記の制約が暗示されているため、ポインタという言葉が使われることはあまりない
上記の制約により、Rust では ダングリング(無効なポインタを参照する)を回避できる
code:rs
fn dangle() -> &'static i64 {
let x: i64 = 32; //
&x
}
参照(&T)は、対象のデータ構造への読み取り専用のアクセスを許可する
一方、書き出しが必要な場合は可変参照(&mut T)を用いる
コンパイラは、コードをサイズが(64 bit プラットフォーム では)8 Byte のポインタを使用する 機械語 に変換する
e.g. 2 つのローカル変数とそれらに対する参照
code:rs
pub struct Point {
pub x: u32,
pub y: u32,
}
let pt = Point { x: 1, y: 2 };
let x = 0u64;
let ref_x = &x;
let ref_pt = &pt;
このコード実行時の スタック の状態は以下のようになる
https://scrapbox.io/files/677757a414fb0f1c3bfc9eb5.png
Box<T> ポインタ型を用いると、強制的にデータ構造を ヒープ に置くことができる
ヒープに置くと、現在のブロックのスコープよりも長く存続することが可能 になる
内部的には、通常の参照と同様に 8 Byte のポインタである
code:rs
let box_pt = Box::new(Point { x: 10, y: 20 });
https://scrapbox.io/files/6777594e6141692f12bc9708.png
ポインタトレイト
Deref / DerefMut トレイト
これらのトレイトを実装すると、deref メソッドで Target 型への参照(DerefMut の場合は可変参照)を作成可能であることを意味する
コンパイラは デリファレンス 式(*x)を見つけると、必要に応じて Deref / DerefMut の実装を探して使用する
これにより、様々な スマートポインタ 型が通常の参照と同じように振る舞うことができる
Rust の数少ない 暗黙の型変換 の 1 つ(項目5:型変換を理解しよう)
Target はなぜジェネリクス(Deref<Target>)でないのか(関連型 なのか)?
曖昧性を回避するため
ある型 ConfusedPtr が Deref<TypeA> と Deref<TypeB> を両方実装することが可能になり、コンパイラがデリファレンス式を解釈する際にどちらを使用すべきか一意に決定できないため
AsRef / AsMut トレイト
コンパイラによる特別な挙動は無く、明示的なメソッド呼び出しによって参照(AsMut の場合は可変参照)を作成する
変換先の型はジェネリクスで指定する(AsRef<Point>)
そのため、複数の変換先をサポートできる
e.g. std::string::String
Deref を Target = str で実装しているので、式 &my_string は &str に変換される
一方、AsRef<[u8]> や AsRef<OsStr>、AsRef<Path>、AsRef<str> も実装している
Pointer トレイト
ポインタ値をフォーマット付き出力できるようにする
コンパイラはフォーマット指定子 {:p} を見つけると、自動的にこのトレイトを用いる
Borrow / BorrowMut トレイト
必須メソッドは borrow と borrow_mut のみで、それぞれ AsRef / AsMut のメソッドと同じシグネチャを持つ
code:rs
fn as_ref(&self) -> &T;
fn as_mut(&mut self) -> &mut T;
fn borrow(&self) -> &Borrowed;
fn borrow_mut(&mut self) -> &mut Borrowed;
AsRef / AsMut と異なる点は、Borrow トレイトを受け取るメソッドは、T への参照だけでなく T のインスタンスも受け取ることができる 点である
これは、標準ライブラリの ブランケット実装 からも明らか
任意の参照 &T に対して、AsRef と Borrow の実装が用意されている
同様に可変参照 &mut T に対しては、AsMut と BorrowMut の実装が用意されている
しかし、Borrow には参照型でない T へのブランケット実装も用意されている
標準ライブラリでもこの特性を活用している
e.g. HashMap::get
code:rs
pub fn get<Q>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
これにより、キーの実際の型 K に対する参照を用いても、K に 借用 できる他の型に対する参照を用いても、アイテムを取得できるようになっている
e.g. String がキーの HashMap は &String でも &str でもアクセス可能
∵ String は Borrow<str> を実装している
Borrow に関連するトレイト: ToOwned トレイト
対象の型の新たに所有権を持つデータを作る to_owned メソッドを提供する
Clone トレイトを一般化したもの
Clone は型 T の新しい値を作るのは &T だけ
ToOwned は borrow の結果の型から作ることができる
e.g. String
String は Borrow<str> を実装しているため、&String から &str へ変換できる
Clone を実装しているため、&String から String を生成できる
ToOwned も実装しているため、&str から String も生成できる
ToOwned に関連する スマートポインタ: Cow
Cow は clone-on-write(書き込み時にクローン)の略
code:rs
pub enum Cow<'a, B>
where
B: 'a + ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
所有しているデータまたは借用しているデータへの参照を保持する enum
Cow で渡されたデータは、変更が必要になるまでは 借用 されているが、変更する際には ToOwned を用いて 所有 されたコピーが作成される
ファットポインタ 型
Rust には組み込みのファットポインタが 2 つ存在する
1. スライス(&[T])
値を連続して格納した コレクション の一部を指す参照
(所有権 を持たない)単純なポインタと長さを表すフィールドで構成されており、単純なポインタの倍のサイズ(64 bit の場合は 16 Byte)を持つ
[T] は値が連続的に並んだ集合を表す 抽象的 な型であり、インスタンス化できない
しかし、この型を具体化した 2 つのコンテナ型がある
1. 配列: コンパイル時にサイズが確定する、連続した領域に確保される値のコレクション
code:rs
let array: u64; 5 = 0, 1, 2, 3, 4;
let slice = &array1..3;
上記のコードを実行したときのスタックの状態
https://scrapbox.io/files/67777966448b0ef37c906395.png
2. ベクタ(Vec<T>): 配列と同様に連続した領域に値を保持する、可変長なコレクション
保持するデータは ヒープ に格納されるが、連続して配置されるためベクタの 部分集合 を参照できる
code:rs
let mut vector = Vec::<u64>::with_capacity(8);
for i in 0..5 { vector.push(i); }
let vslice = &vector1..3;
上記のコードを実行したときのスタック / ヒープの状態
https://scrapbox.io/files/677779d57a79999826171ce6.png
&vector[1..3] という式の背後では、以下のような様々なことが起きている
Range<usize> は SliceIndex<T> トレイトを実装している
このトレイトは、任意の型 T のスライスに対するインデックス操作を実装する
コンパイラは範囲式(1..3)を最小値と最大値を保持した Range<usize> 型のインスタンスに変換する
コンパイラはインデックス式(vector[ ])を、Index トレイトの index メソッド 呼び出しに変換する
同時に、vector をデリファレンスする(*vector.index() と等価)
以上を踏まえると、vector[1..3] は Vec<T> の Index<T> 実装を呼び出す
このためには、I は SliceIndex<[u64]> のインスタンスである必要がある
type Output = <I as SliceIndex<[T]>>::Output
これが動作するのは、Range<usize> が(u64 も含む)任意の型 T に対する SliceIndex<T> を実装しているためである
& でデリファレンスを無効にしているため、最終的な式の型は &[u64] である
2. トレイトオブジェクト
特定のトレイトを実装したインスタンスへの参照
インスタンスへの単純なポインタと、その型の vtable への内部ポインタから構成されており、スライスと同様に単純なポインタの倍のサイズ(64 bit の場合は 16 Byte)を持つ
vtable には、型のサイズやアラインメントに関する情報、安全にドロップするための drop 関数へのポインタなども含まれる
トレイトオブジェクトを保持したコードは、vtable に格納されたポインタを経由して、トレイトのメソッドを呼び出すことができる
このとき、&self にそのインスタンスへのポインタを渡す
e.g.
https://scrapbox.io/files/67777f34448b0ef37c908756.png
code:rs
trait Calculate {
fn add(&self, l: u64, r: u64) -> u64;
fn mul(&self, l: u64, r: u64) -> u64;
}
struct Modulo(pub u64);
impl Calculate for Modulo {
fn add(&self, l: u64, r: u64) -> u64 {
(l + r) % self.0
}
fn mul(&self, l: u64, r: u64) -> u64 {
(l * r) % self.0
}
}
let mod3 = Modulo(3);
let tobj: &dyn Calculate = &mod3;
let result = tobj.add(2, 2);
assert_eq!(result, 1);
スマートポインタ 型
Rc<T>: 対象データの参照カウント付きの構造体
ポインタ関連のすべてのトレイトを実装しているため Box<T> のように動作する
複数の異なる経路からアクセスできるデータ構造を作成するのに有用
しかし、get_mut で変更できるのは他の経路からアクセスがない(他に同じデータに対する Rc や Weak が残っていない)場合に限る
これを実現するのは難しいので、多くの場合 RefCell と組み合わせて用いる
warning.icon
Rust の 所有権 に関するルールの 1 つ「各データの所有者は 1 人」を取り除くことになる
これにより、データリーク が発生する可能性がある
e.g. 循環参照
項目 A が B を指す Rc ポインタを持ち、項目 B が A を指す Rc ポインタを持つ場合、この 2 つはドロップされない
これは Weak<T> を用いることで緩和できる
Weak<T>: 対象となるデータに対する 所有しない参照 を保持する
これにより、強い(通常)の参照がドロップされると、弱い(weak)参照もドロップされるようになるため、上記の問題を回避できる
Weak にアクセスするには Rc<T> へのアップグレードが必要だが、この操作は失敗する可能性がある
既に基となる項目が削除されているケース
Rc は(今のところ)ヒープ上に確保された 2 つの参照カウント(強い / 弱い)と参照対象で構成される
この強い参照カウントが 0 になったときに、参照対象がドロップされる
ただし、データがドロップされた後も Weak が存在している限り、2 つの参照カウントはヒープに保持される
e.g.
code:rs
use std::rc::Rc;
let rc1: Rc<u64> = Rc::new(42);
let rc2 = rc1.clone();
let wk = Rc::downgrade(&rc1);
https://scrapbox.io/files/6777c3c4cd3b9e85b1d014a1.png
RefCell<T>
「データを変更できるのは所有者、または可変参照を保持しているコードのみ」というルールを緩和する(内部可変性)
これにより、トレイトのメソッドが &self のみを受け取る場合でも、トレイトの実装で内部状態を変更することができる
ただし、以下のようなコストがかかる
メモリが、現在の借用数を管理するために必要な isize 分だけ余分に必要になる
借用チェッカ がコンパイル時ではなく実行時に行われる
そのため、プログラマは 2 つの選択肢のうち 1 つを選ぶ必要がある
1. 借用が失敗する操作であることを受け入れ、try_borrow[_mut] を用いて Result を適切に処理する
2. コンパイル時に借用チェックしていないため、実行時に panic! が発生することを受け入れ、borrow[_mut] を用いる
いずれにせよ実行時にチェックが入るため、RefCell 自体は標準的なポインタトレイト(e.g. Deref や DerefMut)を実装していない
代わりに、アクセス操作は Ref<T> や RefMut<T> といったスマートポインタ型を返す
これらのスマートポインタ型は、標準的なポインタトレイトを実装している
もし T が Copy を実装している場合、Cell<T> を用いることで、RefCell よりも少ないオーバーヘッドで内部可変性を実現できる
メソッド
get(&self): 現在の値をコピーして返す
set(&self, val): 新しい値をコピーして格納する
Rc と RefCell の内部実装で、共有されているカウンタの値を &mut self 無しで更新するために用いられている
e.g.
code:rs
use std::cell::RefCell;
let rc: RefCell<u64> = RefCell::new(42);
let b1 = rc.borrow();
let b2 = rc.borrow();
https://scrapbox.io/files/6777c98c230f500f14681499.png
前述の 2 つのスマートポインタは スレッドセーフ ではない
そのため、並行処理 を行う場合は同期オーバーヘッドを持つスマートポインタを使う必要がある
Arc: Rc<T> のスレッドセーフ版
アトミック なカウンタを用いて参照カウンタが正確であることを保証する
対象データの可変アクセスはできない ため、変更するには後述する Mutex や RwLock と組み合わせる
Mutex: 複数 スレッド 間で安全にデータを共有しつつ、同時に 1 つのスレッドのみデータにアクセスできるようにする
RefCell 同様、Mutex 自体はポインタトレイトを実装していないが、lock メソッドが返す MutexGuard は Deref[Mut] を返す
RwLock: データに対して書き込むスレッドが無ければ、複数の並列な読み込みを許容する
書き込みより読み込みのほうが多い場合、Mutex よりも 効率的
Rust の 借用ルール と スレッドルール によって、マルチスレッドコードでは上記の同期コンテナを使用することが強制される
しかし、これは状態共有の並行処理に関する問題の一部を防ぐのみである
項目17:状態共有並列実行には気を付けよう
「コンパイラが拒否したコードと提案したコードを見る」という方法は、スマートポインタ型に対しても当てはまる
しかし、個々のスマートポインタの動作が何を意味しているのかを理解する ほうが、より効率的で ストレス も少ない
e.g. https://doc.rust-lang.org/1.15.1/book/choosing-your-guarantees.html#composition
Rc<RefCell<Vec<T>>>: 共有(Rc)されたベクタ(Vec)を一度にまとめて変更できる
Rc<Vec<RefCell<T>>>: 共有(Rc)されたベクタの各アイテムを独立して変更できる
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目