Минимальная идентификация только для управляемых ресурсов

Существует много информации о "стандартной полной" реализации IDisposable для утилизации ресурсов, управляемых UNmanaged, но на самом деле этот случай (очень) редок (большинство ресурсов уже завернуты управляемыми классами). Этот вопрос фокусируется на некорректной реализации IDisposed для гораздо более распространенного случая с "управляемыми ресурсами".

1: Является ли реализация mimimal IDisposable в коде ниже правильной, есть ли проблемы?

2: Есть ли какая-нибудь причина для добавления полной стандартной IDisposable implimentation (Dispose(), Dispose (bool), Finalizer и т.д.) над представленной минимальной импликацией?

3: Оправдано ли это в этом минимальном случае, чтобы сделать Dispose виртуальным (поскольку не предоставляли Dispose (bool))?

4: Если эта минимальная реализация заменяется полной стандартной реализацией, которая включает (бесполезный, в данном случае) финализатор - это изменяет, как GC обрабатывает объект? Есть ли недостаток?

5: пример включает в себя таймеры и обработчики событий, поскольку эти случаи особенно важны, чтобы не пропустить, поскольку отказ от их использования будет сохранять объекты в живых и ногами ('this' в случае Timer, eventSource в случае обработчика событий) до тех пор, пока GC обходит их в свое время. Есть ли другие примеры, подобные этим?

class A : IDisposable {
    private Timer timer;
    pubic A(MyEventSource eventSource) {
        eventSource += Handler
    }

    private void Handler(object source, EventArgs args) { ... }

    public virtual void Dispose() {
        timer.Dispose();
        if (eventSource != null)
           eventSource -= Handler;
    }
}

class B : A, IDisposable {
    private TcpClient tpcClient;

    public override void Dispose() {
        (tcpClient as IDispose).Dispose();
        base.Dispose();
    }   
}

рефов:
MSDN
SO: Когда мне нужно управлять управляемыми ресурсами
SO: Как удалить управляемый ресурс в методе Dispose() на С#
SO: Dispose() для очистки управляемых ресурсов

Ответ 1

  • Реализация верна, нет проблем, если ни один производный класс не владеет собственным неуправляемым ресурсом.

  • Одной из хороших причин для реализации полной картины является "принцип наименьшего удивления". Поскольку в MSDN нет авторитетного документа, описывающего этот более простой шаблон, разработчики обслуживания в строю могут иметь свои сомнения - даже если вы почувствовали необходимость спросить StackOverflow:)

  • Да, это нормально, если Dispose будет виртуальным в этом случае.

  • Накладные расходы ненужного финализатора незначительны, если Dispose был вызван и правильно реализован (т.е. вызывает GC.SuppressFinalize)

Подавляющее большинство классов IDisposable вне самой платформы .NET Framework IDisposable, потому что они владеют управляемыми ресурсами IDisposable. Редко для них непосредственно держать неуправляемый ресурс - это происходит только при использовании P/Invoke для доступа к неуправляемым ресурсам, которые не отображаются .NET Framework.

Поэтому, вероятно, есть хороший аргумент для продвижения этого более простого шаблона:

  • В редких случаях, когда используются неуправляемые ресурсы, они должны быть завернуты в герметичный класс оболочки IDisposable, который реализует финализатор (например, SafeHandle). Поскольку он запечатан, этот класс не нуждается в полном шаблоне IDisposable.

  • Во всех остальных случаях подавляющее большинство может использоваться ваш более простой шаблон.

Но если до тех пор, пока Microsoft или какой-либо другой авторитетный источник не будет активно продвигать его, я продолжу использовать полный шаблон IDisposable.

Ответ 2

Другой вариант - реорганизовать ваш код, чтобы избежать наследования и сделать ваши классы IDisposable запечатаны. Тогда более простой шаблон легко оправдать, так как неудобные вращения для поддержки возможного наследования больше не нужны. Лично я принимаю этот подход большую часть времени; в редком случае, когда я хочу сделать незапечатанный класс одноразовым, я просто следую стандарту. Одна из хороших вещей в культивировании этого подхода заключается в том, что он стремится подтолкнуть вас к составу, а не к наследованию, что обычно упрощает процесс проверки и тестирования кода.

Ответ 3

Мой рекомендуемый шаблон Dispose предназначен для не виртуальной реализации Dispose для привязки к виртуальному void Dispose(bool), предпочтительно после чего-то вроде:

int _disposed;
public bool Disposed { return _disposed != 0; }
void Dispose()
{
  if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0)
    Dispose(true);
  GC.SuppressFinalize(this); // In case our object holds references to *managed* resources
}

Использование этого подхода гарантирует, что Dispose(bool) получает только один раз, даже если несколько потоков пытаются вызвать его одновременно. Хотя такие одновременные попытки удаления редки (*), дешево охранять их; если базовый класс не делает что-то вроде выше, каждый производный класс должен иметь свою собственную избыточную двоякую защитную логику и, вероятно, также избыточный флаг.

(*) Некоторые классы связи, которые в основном однопоточные и используют блокирующий ввод-вывод, позволяют Dispose вызываться из любого контекста нитей для отмены операции ввода-вывода, которая блокирует свой собственный поток [очевидно, Dispose не может быть вызван в этот поток, поскольку этот поток не может ничего сделать, пока он заблокирован]. Для таких объектов или объектов, которые их инкапсулируют, вполне возможно - и не необоснованно - иметь внешний поток, чтобы попытаться Dispose их как средство прервать свою текущую операцию в тот момент, когда они будут удалены их основной нити. Одновременные вызовы Dispose могут быть редкими, но их возможность не указывает на какую-либо "проблему дизайна" при условии, что код Dispose может действовать на один вызов и игнорировать другой.