Вопрос по c#, .net – Объявить IDisposable для класса или интерфейса?

36

Исходя из следующей ситуации:

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 для освобождения ресурсов. Вы можете решить это двумя способами:

1. Add the required interface to the class SampleA:

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

2. Add it to the interface ISample and force derived classes to implement it:

public interface ISample : IDisposable
{
}

Если вы помещаете его в интерфейс, вы заставляете любую реализацию реализовывать IDisposable, даже если им нечего располагать. С другой стороны, очень ясно видеть, что конкретная реализация интерфейса требует блока dispose / using, и вам не нужно приводить его как IDisposable для очистки. В обоих случаях может быть больше плюсов / минусов ... почему вы предлагаете использовать один способ предпочтительнее другого?

@Damien_The_Unbeliever: Хорошо, если предположить, что ISample получен из результата фабричного метода или из-за внедрения зависимости. Beachwalker
Это вещь - если этоis исходя из фабрики, то скорее всего ваш кодis ответственный за утилизацию, поэтому я поместил его в интерфейс. Но если он впрыскивается, то я предполагаю, что инжектор также ответственен за срок службы, поэтомуwouldn't вписывается в интерфейс - я не думаю, что есть универсальный ответ на этот вопрос. Damien_The_Unbeliever
Насколько вероятно, что код, написанный на интерфейсе (предположительно передаваемый уже созданному экземпляру), ответственен заending (полезное) время жизни этого экземпляра? Damien_The_Unbeliever

Ваш Ответ

7   ответов
8

using(){} для всех ваших интерфейсов лучше всего иметьISample вытекают изIDisposable потому что эмпирическое правило при разработке интерфейсов в пользу"ease-of-use" над"ease-of-implementation".

& quot; ... и вы только видите интерфейс ... & quot; Это ключ. Многие хорошие решения ООП будут использовать только интерфейс, поэтому я думаю, что для интерфейса имеет смысл реализовать IDisposable. Таким образом, потребительский код может обрабатывать все подклассы одинаково.
как бы вы использовали оператор использования в foreach?
4

IFoo вероятно, следует реализоватьIDisposable если есть вероятность, что, по крайней мере, некоторые реализации будут реализованыIDisposableи, по крайней мере, в некоторых случаях последняя сохранившаяся ссылка на экземпляр будет сохранена в переменной или поле типаIFoo, Почти наверняка следует реализоватьIDisposable если какие-либо реализации могут реализоватьIDisposable и экземпляры будут созданы через интерфейс фабрики (как в случае с экземплярамиIEnumerator<T>, которые во многих случаях создаются через фабричный интерфейсIEnumerable<T>).

СравнениеIEnumerable<T> а такжеIEnumerator<T> поучительно Некоторые типы, которые реализуютIEnumerable<T> также реализоватьIDisposable, но код, который создает экземпляры таких типов, будет знать, что они есть, знает, что они нуждаются в удалении, и использовать их как свои конкретные типы. Такие экземпляры могут быть переданы другим подпрограммам как типIEnumerable<T>и эти другие процедуры не имеют ни малейшего представления о том, что объекты в конечном итоге будут нуждаться в утилизации, ноthose other routines would in most cases not be the last ones to hold references to the objects, Напротив, случаиIEnumerator<T> часто создаются, используются и в конечном итоге покидаются кодом, который ничего не знает о базовых типах этих экземпляров, за исключением того факта, что они возвращаютсяIEnumerable<T>, Некоторые реализацииIEnumerable<T>.GetEnumerator() возврат реализацииIEnumerator<T> будет утечка ресурсов, если ихIDisposable.Dispose Метод не вызывается до того, как они покинуты, и большинство из них принимает параметры типаIEnumerable<T> не будет возможности узнать, могут ли такие типы быть переданы ему. Хотя это было бы возможно дляIEnumerable<T> включить недвижимостьEnumeratorTypeNeedsDisposal указать, возвращен лиIEnumerator<T> должны быть расположены, или просто требуют, чтобы процедуры, которые вызываютGetEnumerator() проверьте тип возвращаемого объекта, чтобы увидеть, реализует ли онIDisposableэто быстрее и проще безоговорочно вызватьDispose метод, который может ничего не делать, чем определить,Dispose надо и называть это только если так.

... самый простой способ для объектов выполнять свои обязательства, это вызватьDispose на счетчиков, которыми они владеют, прежде чем оставить их, независимо от того, будут ли эти счетчики заботиться.
@binki: что сделало бы вещи чище, было бы дляIEnumerator реализоватьIDisposable, Код, который получаетIEnumerable он ничего не знает о том, должен предположить, что это может быть необходимо позвонитьDispose наIEnumerator это возвращается. Если он передает возвращенный перечислитель другому методу для последующего использования, последний может вызвать вызовDispose в теме. С призывами ничего не делатьDispose на что-то, что известно для реализацииIDisposable быстрее, чем проверка того, реализует ли объектIDisposable...
ИМО,IEnumerator<T> никогда не должен был реализовыватьсяIEnumerator во-первых, потому что существующий код будет вызыватьIEnumerable.GetEnumerator() и получитьIEnumerator<T> когда написано только для обработкиIEnumerator, Я все еще думаю отделить & # x201C; inner & # x201D; API потребления от & # x201C; external & # x201D; Управление жизненным циклом предпочтительнее, но я думаю, что оно работает достаточно хорошо для синхронных сценариев и сборов в памяти. Я пытаюсь решить свои проблемы с асинхронностью, разделив время жизни и & # x201C; потребитель & # x201D; API-интерфейсы местами, но для того, чтобы сделать то же самое со множеством, мне нужно было бы изобрести совершенно новый API.
IEnumerator<T> хороший пример интерфейса, где управление жизненным циклом определенно является частью его API. Вероятно, это было сделано для упрощения работы. Тем не менее, можно предположить, что кто-то хотел бы позвонитьGetEnumerator() в одном методе и в какой-то момент передать перечислитель другому методу. Этот другой метод в идеале не имел бы доступа кDispose(), Если рамки былиinterface IDisposableEnumerator<T> : IEnumerator<T>, IDisposable {} а такжеinterface IEnumerator<T> : IEnumerator { T Current { get; } }это может сделать некоторые вещи & # x201C; более чистыми & # x201D; (но и более сложный: - /).
@binki: код C # дляforeach звонкиIDisposable если экземпляр возвращенGetEnumerator реализует это.
0

Хорошим примером двух является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();  
    }
}

Код не только ужасен, но и существенно снизит производительность, гарантируя наличие блока finally для удаления элемента для каждой итерации этого списка, даже еслиDispose() просто возвращается в реализации.

Вставил код, чтобы ответить на один из комментариев здесь, чтобы его было легче читать.

Даже если только 1% классов, которые реализуют или наследуют от определенного типа, будут делать что-либо вIDisposable.Dispose, если это когда-либо будет необходимо, чтобы позвонитьIDisposable.Dispose для переменной или поля, объявленного как этот тип (в отличие от одного из реализующего или производного типа), что является хорошим признаком того, что сам тип должен наследовать или реализовыватьIDisposable, Тестирование во время выполнения, реализует ли экземпляр объектаIDisposable и звонитIDisposable.Dispose на нем, если так (как это было необходимо с неуниверсальнымIEnumerable) является основным запахом кода.
Но что, если есть фабрики (или простое использование IoC-контейнера), которые поставляют объекты реализации с требованием их утилизации или без них? Вам придется вызывать Dispose (), вызывая и вызывая его явно. Таким образом, вашему коду нужны знания о (возможной) реализации ... какая-то связь? В противном случае вы можете просто использовать блок, который очень прост. Beachwalker
@Stegi, использующий блок, может не выглядеть уродливо, но он все равно будет иметь снижение производительности. Также я не вижу, как вы, например, используете блок использования внутри foreach. Код Microsoft содержит примеры, в которых это делается: IDisposable amIDisposable = object as IDisposable; if (amIDisposable! = null) amIDisposable.Dispose (); Поскольку as не создает исключение, если не может преобразовать его в IDisposable, снижение производительности практически отсутствует.
6

ISample«я должен быть одноразовым, я поставил бы его на интерфейс, если только некоторые из них я только поместил бы на те классы, где он должен быть».

Похоже, у вас есть последний случай.

Да, я знаю, но это будет мусором кода ... потому что КАЖДАЯ другая реализация (например, IList, I ...) может быть IDisposable. Мне кажется, IDisposable должен быть реализацией базового класса общего объекта (даже если он пустой). Beachwalker
Но как обеспечить вызов Dispose (), если пользователь вашей библиотеки не знает об этом и может потребоваться реализация с утилитой? Beachwalker
@Stegi - Легко. Просто проверитьis IDisposable и позвонитьDispose если так.
@Stegi, если вы согласны с влиянием попытки tryf на производительность
@Stegi - Тогда ты действительно ответил на свой вопрос.
19

Принцип разделения сегментов изSOLID если вы добавляете IDisposable к интерфейсу, который вы предоставляете, клиентам, которые не заинтересованы, вы должны добавить его в A.

Кроме того, интерфейс никогда не может быть одноразовым, потому что одноразовость - это то, что связано с конкретной реализацией интерфейса, а не с самим интерфейсом.

Любой интерфейс может быть потенциально реализован с или без элементов, которые должны быть расположены.

Хотя в теории ваше предложение является наиболее правильным, на практике работа с интерфейсами с потенциальными одноразовыми реализациями является обременительной. Многое зависит от того, кто управляет временем жизни объекта. Лучший ответ ИМО здесь:stackoverflow.com/a/14368765/661933
@nawfal: если теория не совпадает с практикой, это означает, что инструменты, которые мы используем, неверны, а не наоборот. Интерфейс по определению не может быть одноразовым, поскольку вы располагаете только реализациями, а не контрактами на эксплуатацию.
Также инструментам анализа кода легче предупреждать, если сами интерфейсы реализуют IDisposable.
2

и нет ничего плохого в том, что ваш интерфейс унаследован от него. Таким образом, вы будете избегать проверки типов в своем коде только за счет того, что в некоторых из ваших реализаций ISample будет реализована неоперативная реализация. Таким образом, ваш второй выбор может быть лучше с этой точки зрения.

Это просто не соответствует действительности. Если в вашем классе есть поле, которое реализует IDisposable, то этот класс также должен быть IDisposable, чтобы вызвать Dispose для поля. Если вы добавляете IDisposable, когда он не нужен, в итоге половина ваших классов становится IDisposable.
1

IDisposable на интерфейсе может вызвать некоторые проблемы. Это означает, что время жизни всех объектов, реализующих этот интерфейс, может быть безопасно завершено синхронно. Т.е. он позволяет любому писать код, подобный этому, и требуетall реализации для поддержкиIDisposable:

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();
    DoSomething(sampleC);
    sampleC.Complete();
    await sampleC.Completion;
}

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

В приведенном выше примере кода я продемонстрировал три типа сценариев управления временем жизни, добавив к двум предоставленным вами.

SampleA is IDisposable with synchronous using () {} support. SampleB uses pure garbage collection (it does not consume any resources). SampleC uses resources which prevent it from being synchronously disposed and requires an await at the end of its lifetime (so that it may notify the lifetime management code that it is done consuming resources and bubble up any asynchronously encountered exceptions).

Отделяя управление временем жизни от других интерфейсов, вы можете предотвратить ошибки разработчика (например, случайные вызовыDispose()) и более четко поддерживать будущие непредвиденные шаблоны управления временем жизни / областью действия.

Типы, которые не требуют очистки, могут быть реализованыIDisposable легко и правильно с помощьюvoid IDisposable.Dispose() {};, Если некоторым объектам, возвращенным из фабричной функции, потребуется очистка, проще и проще вернуть ей тип, который реализуетIDisposable и требуют, чтобы клиенты уравновешивали каждый вызов фабричной функции с вызовом возвращаемого объектаDispose чем требовать, чтобы клиенты определяли, какие объекты нуждаются в очистке.

Похожие вопросы