web-dev-qa-db-ja.com

クラスまたはインターフェイスのIDisposableを宣言しますか?

次の状況から始めます。

public interface ISample
{
}

public class SampleA : ISample
{
   // has some (unmanaged) resources that needs to be disposed
}

public class SampleB : ISample
{
   // has no resources that needs to be disposed
}

クラスSampleAは、リソースを解放するためのインターフェースIDisposableを実装する必要があります。これは次の2つの方法で解決できます。

1。クラスSampleAに必要なインターフェースを追加します:

public class SampleA : ISample, IDisposable
{
   // has some (unmanaged) resources that needs to be disposed
}

2。インターフェースISampleに追加し、派生クラスにそれを実装させる:

public interface ISample : IDisposable
{
}

それをインターフェイスに入れると、処理するものが何もない場合でも、実装にIDisposableの実装を強制します。一方、インターフェースの具体的な実装には、dispose/usingブロックが必要であり、クリーンアップのためにIDisposableとしてキャストする必要がないことは明らかです。両方の方法で長所/短所がさらにあるかもしれません...なぜあなたは他の方法よりも好ましい方法を使用することを提案するのですか?

48
Beachwalker

すべてのインターフェースにusing(){}パターンを適用する場合、ISampleIDisposableから派生させるのが最善です。インターフェースを設計する際の経験則は"使いやすさ)以上"実装の容易さ"

13
Felice Pollano

Inteface Segregation Principle of [〜#〜] solid [〜#〜] の後に、IDisposableをインターフェイスに追加した場合、興味のないクライアントにメソッドを提供します。 Aに追加する必要があります。

それとは別に、使い捨てはインターフェイスの具体的な実装に関連するものであり、インターフェイス自体に関連するものではないため、インターフェイスを使い捨てにすることはできません。

破棄する必要のある要素の有無にかかわらず、任意のインターフェースを実装できます。

21

個人的には、すべてのISampleを使い捨てにする必要がある場合は、インターフェイスに配置します。一部のみの場合は、配置する必要があるクラスにのみ配置します。

あなたは後者のケースを持っているように聞こえます。

7
Jamiec

少なくとも一部の実装がIFooを実装する可能性が高い場合、インターフェイスIDisposableはおそらくIDisposableを実装する必要があり、少なくとも場合によっては、インスタンスへの最後に残っている参照がIFoo型の変数またはフィールドに格納されます。実装がIDisposableを実装する可能性があり、インスタンスがファクトリインターフェースを介して作成される場合、ほとんどの場合IDisposableを実装する必要があります(多くの場合、ファクトリインターフェース_IEnumerator<T>_を介して作成される_IEnumerable<T>_のインスタンスの場合と同様)。

_IEnumerable<T>_と_IEnumerator<T>_の比較は有益です。 _IEnumerable<T>_を実装する一部の型はIDisposableも実装しますが、そのような型のインスタンスを作成するコードは、それらが何であるかを認識し、破棄が必要であることを認識して、それらを特定の型として使用します。そのようなインスタンスは、タイプ_IEnumerable<T>_として他のルーチンに渡される可能性があり、それらの他のルーチンは、オブジェクトが最終的に破棄する必要があるという手がかりはありませんが、 これらの他のルーチンは、ほとんどの場合、オブジェクトへの参照を保持する最後のルーチンではありません。。対照的に、_IEnumerator<T>_のインスタンスは、それらが_IEnumerable<T>_によって返されるという事実を超えて、それらのインスタンスの基になる型について何も知らないコードによって作成、使用、および最終的に破棄されることがよくあります。 IEnumerable<T>.GetEnumerator()の一部の実装では、_IEnumerator<T>_の実装が破棄される前に_IDisposable.Dispose_メソッドが呼び出されない場合、リソースがリークします。また、タイプ_IEnumerable<T>_のパラメーターを受け入れるほとんどのコードでは、そのような型が渡されるかどうかを知る方法はありません。 _IEnumerable<T>_にプロパティEnumeratorTypeNeedsDisposalを含めて、返された_IEnumerator<T>_を破棄する必要があるか、または単にGetEnumerator()を呼び出すルーチンがIDisposableが実装されているかどうかを確認するためにオブジェクトを返しました。Disposeが必要かどうかを判断して必要な場合にのみ呼び出すよりも、何もしない可能性があるDisposeメソッドを無条件で呼び出す方が早くて簡単です。

6
supercat

IDisposableをインターフェイスに配置すると、いくつかの問題が発生する可能性があると考え始めています。これは、そのインターフェイスを実装するすべてのオブジェクトの寿命を安全に同期的に終了できることを意味します。つまり、誰でもこのようなコードを記述できるようになり、IDisposableをサポートするためにall実装が必要になります。

_using (ISample myInstance = GetISampleInstance())
{
    myInstance.DoSomething();
}
_

具象型にアクセスしているコードだけが、オブジェクトの存続期間を制御する正しい方法を知ることができます。たとえば、型はそもそも破棄する必要がない場合、IDisposableをサポートする場合、または使用後にawaitingの非同期クリーンアッププロセスが必要になる場合があります(例 ここのオプション2のようなもの )。

インターフェイスの作成者は、クラスを実装するために考えられる将来のライフタイム/スコープ管理のニーズをすべて予測することはできません。インターフェイスの目的は、オブジェクトが一部のコンシューマーに役立つようにオブジェクトが一部のAPIを公開できるようにすることです。一部のインターフェイスmayはライフタイム管理(IDisposable自体など)に関連していますが、それらをライフタイム管理に関連しないインターフェイスと混在させると、インターフェイスの実装の記述が困難または不可能になる場合があります。インターフェースの実装がほとんどなく、インターフェースのコンシューマーとライフタイム/スコープマネージャーが同じメソッド内にあるようにコードを構成している場合、この違いは最初は明確ではありません。しかし、オブジェクトを渡し始めると、これはより明確になります。

_void ConsumeSample(ISample sample)
{
    // INCORRECT CODE!
    // It is a developer mistake to write “using” in consumer code.
    // “using” should only be used by the code that is managing the lifetime.
    using (sample)
    {
        sample.DoSomething();
    }

    // CORRECT CODE
    sample.DoSomething();
}

async Task ManageObjectLifetimesAsync()
{
    SampleB sampleB = new SampleB();
    using (SampleA sampleA = new SampleA())
    {
        DoSomething(sampleA);
        DoSomething(sampleB);
        DoSomething(sampleA);
    }

    DoSomething(sampleB);

    // In the future you may have an implementation of ISample
    // which requires a completely different type of lifetime
    // management than IDisposable:
    SampleC = new SampleC();
    try
    {
        DoSomething(sampleC);
    }
    finally
    {
        sampleC.Complete();
        await sampleC.Completion;
    }
}

class SampleC : ISample
{
    public void Complete();
    public Task Completion { get; }
}
_

上記のコードサンプルでは、​​提供した2つに加えて、3つのタイプのライフタイム管理シナリオを示しました。

  1. SampleAIDisposableであり、同期using () {}がサポートされています。
  2. SampleBは、純粋なガベージコレクションを使用します(リソースを消費しません)。
  3. SampleCは、リソースが同期的に破棄されないようにするリソースを使用し、存続期間の終了時にawaitを必要とします(リソースの消費が完了し、非同期で発生した例外)。

ライフタイム管理を他のインターフェースと分離しておくことで、開発者のミス(たとえば、Dispose()の偶発的な呼び出し)を防ぎ、将来の予期しないライフタイム/スコープ管理パターンをより明確にサポートできます。

3
binki

IDispoableは非常に一般的なインターフェイスであるため、インターフェイスから継承しても問題はありません。 ISample実装の一部で何もし​​ない実装を行う唯一のコストで、コードの型チェックを回避します。したがって、この観点からは、2番目の選択肢の方が良いかもしれません。

3
Seb

個人的には、2つの具体的な例を作らない限り、1を選択します。 2つの良い例はIListです。

IListは、コレクションにインデクサーを実装する必要があることを意味します。ただし、IListは実際にIEnumerableであることも意味し、クラスにはGetEnumerator()が必要です。

あなたのケースでは、ISampleを実装するクラスがIDisposableを実装する必要があることをためらっています。ただし、インターフェースを実装するすべてのクラスがIDisposableを実装する必要がない場合は、強制しないでください。

特にIDispoableに焦点を当てると、特にIDispoableは、プログラマーがクラスを使用して、かなり醜いコードを書くことを余儀なくさせます。例えば、

_foreach(item in IEnumerable<ISample> items)
{
    try
    {
        // Do stuff with item
    }
    finally
    {
        IDisposable amIDisposable = item as IDisposable;
        if(amIDisposable != null)
            amIDisposable.Dispose();  
    }
}
_

コードがひどいだけでなく、Dispose()が実装に戻ったとしても、そのリストの反復ごとにアイテムを破棄するfinallyブロックがあることを保証すると、パフォーマンスが大幅に低下します。

ここにあるコメントの1つに答えるコードを貼り付け、読みやすくしました。

1
M Afifi