導入
オブジェクトを部分的に書き換えて何らかの処理をした後、元に戻したいことはありませんか?
これはミスが起きがちな処理ですがRustの機能を使うとミスを避けられることを紹介します。
例えば一時的にVec
にある値をプッシュしたいが、プッシュ前のVec
も後で必要になるという場合を考えてみましょう。
もちろんVec
をクローンしてからプッシュしても実現できますが嫌ですよね。
クローンを回避する場合次のようなコードになると思います。
(書き換え前と後が同時に必要になる場合そうはいかないのですが永続データ構造を使うとコストのかかるクローンを避けられます。Rustならimが人気があるようです)
/// 内部での一時的な変更のためにミュータブルな参照を要求しますが、関数の実行後vは変更されていません。
fn hoge(v: &mut Vec<u32>) -> i32 {
let mut value = ... // プッシュする値を計算する
v.push(value);
// 変更後のVecをチェックした結果、計算をする必要がないならリターン
if !need_calculation(&v) {
return 0; // 変更を戻し忘れている
}
// 変更後のVecを使って計算する
let n = calculate(&v);
// 元に戻す
v.pop();
n
}
need_calculation
の結果がfalse
のとき元に戻すのを忘れています。
このように必ず最後になにかするというのはミスが起きがちです。
しかし上で述べたとおりRustに備わるRAIIと所有権を利用することでそのようなバグが起きないようにできます。
実際にやってみる
型の作成
プッシュ後のVec
への参照を持つ型を作ります。
pub struct Pushed<'a, T> {
/// 値をプッシュされたVec
inner: &'a mut Vec<T>,
}
impl<'a, T> Pushed<'a, T> {
pub fn onto(target: &'a mut Vec<T>, value: T) -> Pushed<'a, T> {
target.push(value);
Pushed { inner: target }
}
}
Drop
トレイト
Drop
を実装する型はスコープから外れると自動的にDrop::drop
が呼び出されます。(この仕組みをRAIIと呼びます)
そこで、この型に変更を元に戻す処理をDrop::drop
として実装してやれば復元漏れバグをなくすことができます。
impl<'a, T> Drop for Pushed<'a, T> {
fn drop(&mut self) {
self.inner.pop();
}
}
Deref
次に内部の変更後のVec
の参照を得るメソッドを追加します。
まずはイミュータブルな参照を得ることを考えることにします。
単にas_vec
というメソッドを追加してもいいのですが、Deref
を実装するとより使いやすくなります。
Deref
についての説明は「TRPL 1st ed, Deref
による型強制」がわかりやすいと思います。
また、イミュータブルな場合Vec
よりもスライスを返したほうがいいかもしれませんがこの記事では考えないことにします。
use std::ops::Deref;
impl<'a, T> Pushed<'a, T> {
pub fn as_vec(&self) -> &Vec<T> {
&self.inner
}
}
impl<'a, T> Deref for Pushed<'a, T> {
type Target = Vec<T>;
fn deref(&self) -> &Vec<T> {
&self.inner
}
}
これで目的の型が完成しました。
使ってみる
実際に使ってみると次のようになります。
fn main() {
let mut v = vec![0usize, 1];
println!("{:?}", &v); // [0, 1]
{
let p = Pushed::onto(&mut v, 100);
println!("{:?}", &*p); // [0, 1, 100]
}
println!("{:?}", &v); // [0, 1]
println!("{:?}", &*Pushed::onto(&mut v, 100)); // [0, 1, 100]
println!("{:?}", &v); // [0, 1]
}
きちんと変更が元に戻っていますね。
所有権
まだPushed
がスコープに存在するときに (つまりまだスライスが変更後の状態のときに)、誤って変数v
を通じて変更前のスライスにアクセスしようとするとコンパイルエラーになります。
fn main() {
let mut v = vec![0usize, 1];
let p = Pushed::onto(&mut v, 100);
println!("{:?}", &v); // コンパイルエラー
}
Pushed
がv
へのミュータブルな参照を持っているためにv
を借用できないからです。
このようにうまくRustの機能を使うとバグが起きにくい設計をすることができるのです。
使い勝手の向上 (蛇足)
revert
メソッド
明示的に変更を元に戻すためのメソッドを作っておくと、わざわざスコープを作って元に戻す必要がなくなります。
impl<'a, T> Pushed<'a, T> {
fn revert(self) {}
}
実装は空ですが、引数がself
で所有権を奪ってスコープを抜けているので自動的にDrop::drop
が呼び出されます。
これはmem::drop
のメソッド版です。
使ってみると以下のようになります。
fn main() {
let mut v = vec![0usize, 1];
println!("{:?}", &v); // [0, 1]
let p = Pushed::onto(&mut v, 100);
println!("{:?}", &*p); // [0, 1, 100]
p.revert();
println!("{:?}", &v); // [0, 1]
}
注意点として、これは直接Drop::drop
を呼び出すこととは異なります。
Drop::drop
は引数が&mut self
で所有権を奪いませんので、直接呼び出すと、スコープを抜けたときと二重にDrop::drop
を呼び出すことになり、メモリの二重解放などの恐れがあります。
実際Drop::drop
を明示的に呼び出すとコンパイルエラーになるようです。(参照: std::ops::Drop::drop)
ミュータブルな参照
複数回の変更をするためにはPushed
からさらにミュータブルな参照を得る必要があります。
安直に&mut Vec<T>
へのDerefMut
を実装すると、Vec
へのさらなるプッシュやポップがあった場合元の値と異なる値をポップしてしまいます。
そこで、複数回の変更がしたい場合はPushed
を引数に取りPushed
を返すメソッドを作成すると間違った使い方ができなくなります。
impl<'a, T> Pushed<'a, T> {
fn onto_pushed(target: &'a mut Pushed<'b, T>, value: T) -> Pushed<'a, T> {
Pushed::onto(&mut target.inner, value)
}
}
これならばさらなる変更があったとしても、それらは先にドロップされて自分がドロップされるタイミングでは元に戻っているのでうまく動作します。
あり得る変更が他にもあるのならトレイトを使って抽象化すると他の変更と組み合わせられます。
/// 一時的な借用のトレイト.
pub trait BorrowTmp<Borrowed> {
/// 一時的な書き換えを許されたミュータブルな参照を返す.
///
/// # Safety
///
/// このメソッドが返す参照の生存期間が終わるとき, 参照先は参照を得たときと同じ状態になっていなければなりません.
unsafe fn borrow_tmp(&mut self) -> &mut Borrowed;
}
impl<T> BorrowTmp<T> for T {
unsafe fn borrow_tmp(&mut self) -> &mut T {
self
}
}
impl<'a, T> BorrowTmp<Vec<T>> for Pushed<'a, T> {
unsafe fn borrow_tmp(&mut self) -> &mut Vec<T> {
&mut self.inner
}
}
impl<'a, T> Pushed<'a, T> {
pub fn onto<V>(target: &'a mut V, value: T) -> Pushed<'a, T> where V: BorrowTmp<Vec<T>> {
unsafe {
let inner = target.borrow_tmp();
inner.push(value);
Pushed { inner }
}
}
}
他にも拡張トレイトなどで使い勝手を向上できるかもしれません。