Edited at

Rustだからこそできる安全な一時変更パターン


導入

オブジェクトを部分的に書き換えて何らかの処理をした後、元に戻したいことはありませんか?

これはミスが起きがちな処理ですが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); // コンパイルエラー
}

Pushedvへのミュータブルな参照を持っているために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 }
}
}
}

他にも拡張トレイトなどで使い勝手を向上できるかもしれません。