C#/IDisposableへのwrapping

(4341d) 更新


公開メモ

必ず後処理の必要なクラスが IDisposable を実装していない?!

やまねこ@楢ノ木技研 さんからのお題で、表記のような状況を解決する方法を考えてみました。

自分でコードを変更できないライブラリに、そういう不用意な設計のクラスが多用されているときの対症療法です。

IDisposable に wrap するためのライブラリを手軽に作る

こんなのが思い浮かびました。

どうでしょうね。

LANGUAGE:C#(linenumber)
// Dispose 時に Close() したいのに IDisposable を継承してないクラスがいくつかある
class ToBeClosed1
{
    public void Close()
    {
        MessageBox.Show("Closed!");
    }
}

class ToBeClosed2
{
    public void Close()
    {
        MessageBox.Show("Closed!");
    }
}

// using を使えるようにする wrapper
class DisposableWrapper: IDisposable
{
    Action action;
    protected DisposableWrapper(Action action)
    {
        this.action = action;
    }
    public void Dispose()
    {
        action.Invoke();
    }
    // ここに必要な変換を追加すれば・・・
    public static implicit operator DisposableWrapper(ToBeClosed1 tbc1)
    {
        return new DisposableWrapper(() => tbc1.Close());
    }
    public static implicit operator DisposableWrapper(ToBeClosed2 tbc2)
    {
        return new DisposableWrapper(() => tbc2.Close());
    }
}
 
// こうやって使えるようになります
private void button1_Click(object sender, EventArgs e)
{
    using ( (DisposableWrapper) new ToBeClosed1())
    {
    }

    using ( (DisposableWrapper) new ToBeClosed2())
    {
    }
}

もちろん、必要に応じて 33 行目を

LANGUAGE:C#
        return new DisposableWrapper( () => { 
            if ( ! tbc1.Closed ) tbc1.Close(); 
        });

などとすることも可能なので、この形で任意の後処理を行えます。

本来ならば

新たに ToBeClosed3 へ対応したいときに DisposableWrapper 自体に変更を加えなくて済むようにしたいところ。

書きやすさは失われますが拡張メソッドを使えば次のようにもできます。

LANGUAGE:C#(linenumber)
// Dispose 時に Close() したいのに IDisposable を継承してないクラスがいくつかある
public class ToBeClosed1
{
    public void Close()
    {
        MessageBox.Show("Closed!");
    }
}

public class ToBeClosed2
{
    public void Close()
    {
        MessageBox.Show("Closed!");
    }
}

// using を使えるようにする wrapper
public class DisposableWrapper : IDisposable
{
    Action action;
    public DisposableWrapper(Action action)
    {
        this.action = action;
    }
    public void Dispose()
    {
        action.Invoke();
    }
}

// wrapper への変換を拡張メソッドとして実装する

public static class WrapToBeClosed1
{
    public static DisposableWrapper AsDisposable(this ToBeClosed1 tbc1)
    {
        return new DisposableWrapper(() => tbc1.Close());
    }
}

public static class WrapToBeClosed2
{
    public static DisposableWrapper AsDisposable(this ToBeClosed2 tbc2)
    {
        return new DisposableWrapper(() => tbc2.Close());
    }
}

// こうやって使えるようになります
private void button1_Click(object sender, EventArgs e)
{
    using ( (new ToBeClosed1()).AsDisposable() )
    {
    }

    using ( (new ToBeClosed2()).AsDisposable() )
    {
    }
}

OOP 的にはこちらの方が正しいですが、 シンタックスシュガー的には上の方が良いかも?

変数のスコープ問題

やまねこ@楢ノ木技研 さんの TW で気づかされたことには、 上記のような方法だと実際に使うときには以下のような不格好なことになってしまいます。

LANGUAGE:C#(linenumber)
// 実際にはこうやって使うことに
private void button1_Click(object sender, EventArgs e)
{
    ToBeClosed1 tbc1;
    using ( (tbc1 = new ToBeClosed1()).AsDisposable() )
    {
        // tbc1 を使う
    }
}

これは本当に不格好です。

以下のようにすれば using 周りはきれいになりますが、 変数を使うときにしわ寄せがくることに・・・

LANGUAGE:C#(linenumber)
// using を使えるようにする wrapper
// 実際にオブジェクト自体を wrap する形にした
public class DisposableWrapper<T> : IDisposable
{
    // オブジェクト自体を保持する
    public T content { get; private set; }
    // 後処理に必要な手順
    Action action;
    // オブジェクト自身と、その後処理の組み合わせで初期化する
    public DisposableWrapper(T obj, Action action)
    {
        this.content = obj;
        this.action  = action;
    }
    // 後始末
    public void Dispose()
    {
        action.Invoke();
    }
    // 非明示的に T に変換可能
    public static implicit operator T(DisposableWrapper<T> wrapped)
    {
        return wrapped.content;
    }
}

// ここの記述がちょっと面倒になった
public static class WrapToBeClosed1
{
    public static DisposableWrapper<ToBeClosed1> WrapInDisposable(this ToBeClosed1 tbc1)
    {
        return new DisposableWrapper<ToBeClosed1>(tbc1, () => tbc1.Close());
    }
}

public static class WrapToBeClosed2
{
    public static DisposableWrapper<ToBeClosed2> WrapInDisposable(this ToBeClosed2 tbc2)
    {
        return new DisposableWrapper<ToBeClosed2>(tbc2, () => tbc2.Close());
    }
}

// こうやって使う
private void button1_Click(object sender, EventArgs e)
{
    // ラップされたものをそのまま変数に受けておいて
    using (var wrapped = (new ToBeClosed1()).WrapInDisposable())
    {   // 使うときに中身を取り出す
        wrapped.content.SomeMethod1();

        // 引数や代入で使うときは暗黙のキャストが働くのでそのまま使える
        SomeMethod2(wrapped);      // ToBeClosed1 を引数とする関数に渡す
        ToBeClosed1 obj = wrapped; // ToBeClosed1 型の変数へ代入
    }

    // あるいはすぐ取り出して、それを使ってもOK
    using (var _tbc1 = (new ToBeClosed1()).WrapInDisposable())
    using (var _tbc2 = (new ToBeClosed2()).WrapInDisposable())
    {
        var tbc1 = _tbc1.content;
        var tbc2 = _tbc2.content;
        // これ以降 ToBeClosed1 型の tbc1 と
        // ToBeClosed2 型の tbc2 を自由に使える
    }
}

使いやすいとはお世辞にも言えない?

今のところ、これ以上の案は出てません(TT

コメント





Counter: 5877 (from 2010/06/03), today: 1, yesterday: 1