Обычно вам не нужна реализация Dispose, потому что классы стандартной библиотеки сами реализуют IDisposable, и вам нужно просто применить using. Пример с файлом:
using (var reader = new StreamReader("path/to/file"))
{
// работаем с файлом...
}
Суть работы здесь простая: объект удаляется как только он не нужен больше через Dispose.
Однако если вам нужно держать IDisposable-объект среди свойств вашего класса, ваш класс должен по идее сам имплементировать IDisposable.
В случае, когда у вас просто одно или несколько IDisposable-полей, вам подходит простой вариант реализации паттерна:
// только для случая, когда у нас нет потомков
sealed class ResourceHolder : IDisposable
{
SomeResource resource;
bool isDisposed = false;
public ResourceHolder()
{
resource = AllocateResource();
}
public void Use()
{
if (isDisposed)
throw new ObjectDisposedException();
// ...
}
public void Dispose()
{
// мы уже умерли? валим отсюда
if (isDisposed) return; // идемпотентность Dispose
// освободим ресурсы
resource.Dispose();
// и запомним, что мы уже умерли
isDisposed = true;
}
}
Здесь вам вовсе не нужен финализатор, т. к. вы не пользуетесь неуправляемыми ресурсами.
Суть работы здесь немного сложнее. Если объект убивается через Dispose, то вызывается resource.Dispose, при этом ресурс освобождается (например, файл закрывается). Если объект не убивается через Dispose, то на resource в какой-то момент больше не будет корневых ссылок, и для его неуправляемых подресурсов вызовется финализатор, и освободит их (например, закроет файл). Это, как вы видите, простой случай.
Для общего случая, когда в классе есть как управляемые, так и неуправляемые ресурсы, да ещё и возможно наследование, подойдёт полная форма паттерна. Но это не рекомендованный метод, т. к. код получается избыточно сложным.
class ResourceHolder : IDisposable
{
SomeUnmanagedResource uResource;
SomeManagedResource mResource;
bool isDisposed = false;
public ResourceHolder()
{
uResource = AllocateUnmanagedResource();
mResource = AllocateManagedResource();
// если здесь происходит дополнительная инициализация, то нужно,
// чтобы финализатор не съел объект до конца конструктора.
// для этого последней строчкой прописываем
GC.KeepAlive(this);
}
void Use()
{
if (isDisposed)
throw new ObjectDisposedException();
// ...
}
public void Dispose()
{
Dispose(isDisposing: true);
// мы закрыли ресурс, финализатор не нужен
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool isDisposing)
{
// мы уже умерли? валим отсюда
if (isDisposed) return; // идемпотентность Dispose
// мы вызваны из Dispose?
if (isDisposing)
{
// освободим управляемые ресурсы
mResource.Dispose();
}
// в любом случае освободим неуправляемые ресурсы
// но нам может понадобиться проверка того, а был ли реально аллоцирован
// неуправляемый ресурс (например, его выделение могло бросить исключение)
if (uResource was indeed allocated)
FreeUnmanagedResource(uResource);
// и запомним, что мы уже умерли -- это должно быть последней строкой
isDisposed = true;
// GC.KeepAlive(this) не нужно, т. к. в предыдущей строке объект
// всё ещё достижим (isDisposed -- поле объекта)
}
~ResourceHolder()
{
// раз мы в финализаторе, то нас забыли за-Dispose-ить
// надо освободить неуправляемые ресурсы
// освобождать управляемые ресурсы здесь опасно (и вообще запрещено)
Dispose(isDisposing: false);
}
}
Видите, насколько реализация стала сложнее?
Возможно, именно поэтому гайдлайн (ссылка ниже) советует не держать в качестве полей неуправляемые ресурсы, а лишь SafeHandle на них (точнее, производный от него класс). При этом паттерн упрощается до того варианта, что приведён выше, плюс отпадает необходимость в финализаторе. И сложный случай сводится к простому.
Вот Майкрософтовский гайдлайн по этому поводу.