C#でインターフェースをDisposeする

C#で引数をインターフェースで受けた時に実際の具象クラスがIDsiposabeを継承していてそれをDisposeしたいときの方法です。継承階層は以下の通りで青いIDatabseで変数を受けた時の話になります。

f:id:Takachan:20180207003342p:plain

コードにすると以下のような状態です。

// コード例
public class IDatabse
{
    void Operate(string command);
}

public class DbImple : IDatabse, IDisposable
{
    public void Operate(string command) { /* Omitted */ }
}

このような状態で以下のように処理を受けた際に、usingステートメントに放り込んで資源を解放しようとすると「エラー CS1674 'IFoo': using ステートメントで使用される型は、暗黙的に 'System.IDisposable' への変換が可能でなければなりません。」となりコンパイルエラーが発生します。

public void Main(params string[] args)
{
    using(IDatabse db = Factory.CreateInstance(args[0])) // エラーが起きる
    {
        
    }
}

通常、こういったケースが想定されるならIDatabseの宣言を

public interface IDatabase : IDisposable

のようにIDisposableを継承するようにしておく方がいいのですが、大抵リソース解放を継承クラスに強要することになりISP(インターフェイス分離の原則)に違反してしまいます。したがって、以下のようにコードを書いて、解放可能ならば解放するような処理を書きます。が、コレ、定型的な処理のため毎回書くのはちょっと面倒です。

public void Main(params string[] args)
{
    IDatabse db = Factory.CreateInstance(args[0]));
    
    try
    {
    }
    finally
    {
        if(db is IDsposable dis)
        {
            using(dis) { /* nop */ }
        }
    }
}

そこで、この問題の解決法をC#の機能を利用して2種類作成してみました。

該当箇所をUtility化する

staticクラスを作成して、該当部分をメソッド化して外に出します。

// Disposeする箇所を汎用処理にする
public static class Context
{
    public void Dispose(object source)
    {
        if(db is IDsposable dis)
        {
            using(dis) { /* nop */ }
        }
    }
}

public void Main(params string[] args)
{
    IDatabse db = Factory.CreateInstance(args[0]));
    
    try 
    {
        // 何らかの処理
    }
    finally
    {
        ResourceUtil.Dispose(db); // 上記のメソッドを呼び出す
    }
}

objectクラスへ拡張メソッドを追加する

objectクラスへ拡張メソッドとしてDisposeを追加します。

こっちの方が見かけはシンプルになります。Dispose内で安全性を確認しているので、取り合えず最後に呼び出すこともできます。

public static class ObjectExtension
{
    public static void Dispose(this object obj)
    {
        if(obj is IDisposable dis)
        {
            using (dis) { }
        }
    }
}

public void Main(params string[] args)
{
    IDatabse db = Factory.CreateInstance(args[0]));
    
    try 
    {
        // 何らかの処理
    }
    finally
    {
        db.Dispose(); // 拡張メソッドを呼び出す。
    }
}

基本クラスを拡張するのは一見いいアイデアですが、拡張しすぎて意味が分からなくなる可能性もあるので、ライフサイクルの長くなりそうなソフトではできる限りstaticクラスで処理することをおすすめします。