Название шаблона
Инициализация при получении ресурса (Resource Acquisition Is Initialization или RAII).
Тип
Порождающий шаблон проектирования (Creational).
Описание
Шаблон "Инициализация при получении ресурса" используется для управления доступом к некоему ресурсу. При этом инициализация объекта совмещается с его получением, а уничтожение – с обязательным освобождением.
Реализация шаблона должна гарантировать, что при уничтожении объекта ресурс будет освобожден. Причем не зависимо от того, как закончился блок, в котором он используется: нормально, с ошибкой или по исключению.
Данный подход часто используется при получении любых ресурсов системы: доступе к файлам, памяти, аппаратному обеспечению, создание мьютексов, критических секций и т.д.
Реализация шаблона в общем виде
- в конструкторе объекта получаем доступ к необходимому ресурсу;
- в деструкторе вызываются функции, освобождающие ресурс;
- класс может предусматривать метод, который позволяет освободить ресурс самостоятельно. Однако, деструктор все равно должен проверить состояние ресурса и при необходимости освободить его.
Реализация и ее особенности в C#
Как известно, в .NET уничтожение переменной не гарантирует уничтожение самого объекта. Он будет удален из памяти при очередном проходе сборщика мусора. При этом не должно быть других переменных, ссылающихся на него. Предугадать когда это произойдет практически невозможно.
Как следствие, в C# нет деструкторов, как они понимаются, например, в С++ и других языках. Вместо них есть финализаторы (finalizer). Но с ними связан ряд особенностей:
- они вызываются именно перед удалением объекта сборщиком мусора, т.е. так же невозможно определить этот момент времени;
- при отсутствии ссылок на объект с финализатором, он, вместо уничтожения, переходит в следующее поколение и существует там до следующей сборки мусора.
В качестве решения можно предложить обернуть часть кода, где используется некий ресурс, в конструкцию try-finally. Тогда в блоке finally можно освободить занятый ресурс. Такой подход гарантирует что это произойдет при любом варианте завершения контролируемого блока. На практике, понимание такого кода несколько усложняется, т.к. сразу не ясна цель использования конструкции try-finally. Да и выглядит, на мой взгляд, такой к��д не очень опрятно.
Поэтому в C# введен оператор using, который помогает избавиться от указанных выше проблем. Его использование выглядит так:
using (var obj = new SomeObject()) {
// Do something with 'obj'
}
Здесь есть одно условие – класс SomeObject должен реализовывать интерфейс IDisposable:
public interface IDisposable
{
void Dispose();
}
Метод Dispose() отвечает за освобождение используемых ресурсов. Он будет вызываться при выходе из контролируемого блока.
Поскольку using это особенность C#, то при компиляции он будет развернут в try-finally:
SomeObject obj = null;
try {
obj = new SomeObject();
// Do something with 'obj'
}
finally {
if (obj != null) {
obj.Dispose();
}
}
Таком образом, реализация шаблона "Инициализация при получении ресурса" на C# состоит из:
- реализации IDisposable и освобождении ресурсов в методе Dispose();
- использовании конструкции using, которая гарантирует освобождение ресурса при выходе из контролируемого блока.
Но есть еще одна проблема: программист, который будет работать с этим классом, может не использовать using или не вызвать Dispose(). Можно ли учесть эту ситуацию?
Решение заключается в подстраховке с использованием финализатора. В нем мы можем или освободить ресурс или выкинуть исключение. Последний вариант не укажет на место ошибки, но заставит программиста пересмотреть код (и, наконец, изучить документацию на используемый класс). Чтобы не увеличивать срок жизни объекта, в случае его правильного использования, финализатор блокируется вызовом метода GC.SuppressFinalize(). В этом случае, неиспользуемый объект будет уничтожен при ближайшей сборке мусора.
С учетом всех указанных выше моментов, класс будет выглядеть следующим образом:
public class RAIIObject : IDisposable
{
public RAIIObject()
{
// lock resources
}
~RAIIObject()
{
throw new InvalidOperationException("you should use 'using' keyword!")
// or
// this.ReleaseResources();
}
void IDisposable.Dispose()
{
this.ReleaseResources();
GC.SuppressFinalize(this);
}
private void ReleaseResources()
{
// release resources
}
}