Работа с выделенными ресурсами и исключениями

Если вы обладаете большим опытом работы в С++, то одна вещь в мире С# покажется наиболее захватывающей — отсутствие детерминированной деструкции. Разработчики С++ привыкли использовать конструкторы и деструкторы объектов, располагаемых в стеке, для управления ценными ресурсами.

Эта идиома даже имеет собственное название — RAII (Resource Acquisition Is Initialization — захват ресурсов является инициализацией). Это означает возможность создания объектов в стеке С++, в которых некоторый ценный ресурс выделяется в конструкторе, и если он освобождается в деструкторе, то можно положиться на его автоматический вызов для очистки в нужное время.

Например, независимо от того, каким образом объекты, расположенные в стеке, покидают область видимости — через нормальное выполнение при достижении конца контекста либо по исключению — деструктор будет гарантированно выполнен, освободив при этом ценный ресурс.

Когда С# и CLR впервые были представлены разработчикам в виде бета-версии, многие немедленно подняли шум об этом недостатке исполняющей системы. Считаете вы это недостатком или нет, но оно точно не касается наиболее полного расширения, которое появилось после замечаний от сообщества разработчиков, использующих бета-версию.

Проблема происходила отчасти из природы объектов CLR, которыми заведует сборщик мусора, наряду с тем фактом, что дружественный деструктор в синтаксисе С# был повторно использован для реализации финализаторов объекта. Важно также помнить, что финализаторы существенно отличаются от деструкторов. Применение синтаксиса деструктора для финализаторов только добавило путаницы.

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

После некоторых размышлений было предложено решение в виде шаблона Disposable, предусматривающего реализацию интерфейса IDisposable. По сути, если объект нуждается в детерминированной деструкции, он получает ее за счет реализации интерфейса IDisposable.

Однако метод Dispose должен быть вызван явно, чтобы выполнить “уборку” за одноразовым объектом. Если вы забудете это сделать, а объект закодирован правильно, то ресурс не будет утерян — он будет просто очищен тогда, когда сборщик мусора, наконец, вызовет финализатор.

Внутри С++ нужно только не забыть поместить код очистки в деструктор, после чего никогда не придется заботиться об очистке локальных объектов, поскольку такая очистка происходит автоматически при выходе из области определения.

Рассмотрим следующий надуманный пример, иллюстрирующий опасность, с которой можно столкнуться:

using System;
using System.10;
using System.Text;
public class EntryPoint {
public static void DoSomeStuff ()  {
// Открыть файл.
FileStream fs = File.Open ( "log.txt",
FileMode.Append,
FileAccess.Write,
FileShare.None );
Byte[] msg = new UTF8Encoding(true) .GetBytes("Doing Some"+
" Stuff");
fs.Write( msg, 0, msg.Length );
}
public static void DoSomeMoreStuff()  {
// Открыть файл.
FileStream fs = File.Open ( "log.txt",
FileMode.Append,
FileAccess.Write,
FileShare.None );
Byte[] msg = new UTF8Encoding(true).GetBytes("Doing Some"+
" More Stuff");
fs.Write( msg, 0, msg.Length );
}
static void Main()  {
DoSomeStuff();
DoSomeMoreStuff();
}

Этот код выглядит вполне невинно. Однако в результате его запуска почти наверняка возникнет исключение IOException. Код в DoSomeStuff создает объект FileStream с монопольной блокировкой файла. Как только объект FileStream выходит из контекста в конце функции, он помечается для уборки, но когда именно она произойдет, приходится полагаться на сборщик мусора (GC).

Таким образом, во время следующей попытки открытия файла в DoSomeMoreStuff генерируется исключение, поскольку ценный ресурс все еще заблокирован недоступным объектом FileStream. Ясно, что это весьма неприятная ситуация. Даже не думайте о явном вызове GC. Collect в Main перед вызовом DoSomeMoreStuff.

Попытка управлять алгоритмом работы CG, чтобы заставить его подбирать объекты в определенное время — путь к снижению производительности. Вы не сможете помочь GC лучше выполнять его работу, поскольку не имеете представления о том, как он реализован.

Как же поступить? Так или иначе, необходимо гарантировать закрытие файла. Однако здесь есть шероховатость: независимо от того, как это делается, нужно помнить о том, что это должно быть сделано. В этом состоит отличие от языка С++, где можно поместить код очистки в деструктор и потом просто быть уверенным, что ресурс будет очищен в надлежащий момент. Одним из вариантов может быть вызов метода Close объекта FileStream в каждом методе, использующем его.

Это работает хорошо, но степень автоматизации ниже, и всегда необходимо помнить, что это нужно сделать. Однако даже если не забыть это сделать, то что произойдет, если до вызова метода Close сгенерируется исключение? Вы окажетесь в той же ситуации, что и раньше, с утерянным ресурсом, к которому невозможно обратиться даже для его освобождения.

Те, кто знаком с обработкой исключений, отметят, что описанную выше проблему можно решить с использованием блоков try/finally, как показано в следующем примере:

using System;
using System.10;
using System.Text;
public class EntryPoint {
public static void DoSomeStuff ()  {
// Открыть файл.
FileStream fs = null;
try {
fs = File.Open ( "log.txt",
FileMode.Append, FileAccess.Write, FileShare.None );
Byte[] msg =
new UTF8Encoding(true).GetBytes("Doing Some"+
" Stuff\n");
fs.Write( msg, 0, msg.Length );
}
finally {
if ( fs != null )  {
fs .Close () ;
}
public static void DoSomeMoreStuff()  {
// Открыть файл.
FileStream fs = null;
try {
fs = File.Open ( "log.txt",
FileMode.Append, FileAccess.Write, FileShare.None );
Byte[] msg =
new UTF8Encoding(true).GetBytes("Doing Some"+
" More Stuff\n");
fs.Write( msg, 0, msg.Length );
}
finally {
if ( fs != null )  {
fs .Close () ;
}
}
}
static void Main()  {
DoSomeStuff() ;
DoSomeMoreStuff() ;
}
}

Блоки try/finally решают проблему. Но насколько некрасивым стал код! Вдобавок давайте признаем, то многие из нас довольно ленивы, а здесь — огромный объем дополнительного клавиатурного набора. Более того, больше текста означает больше возможностей внести ошибку. И, наконец, это затрудняет чтение кода.

Как и можно было ожидать, существует путь получше. Многие объекты, такие как FileStream, имеющие метод Close, также реализуют шаблон Disposable.

Обычно вызов Dispose на этих объектах — это то же самое, что вызов Close. Вызов Close через Dispose или наоборот — это спор о разных вкусах, если все равно нужно явно вызывать тот или другой метод. К счастью, есть хорошая причина того, почему большинство классов, имеющих метод Close, реализуют Dispose — появляется возможность использовать их в операторе using, который обычно применяется как часть шаблона Disposable в С#.

Поэтому следует изменить код, как показано ниже:

using System;
using System.10;
using System.Text;
public class EntryPoint {
public static void DoSomeStuff ()  {
// Открыть файл.
using ( FileStream fs = File.Open ( "log.txt",
FileMode.Append, FileAccess.Write, FileShare.None )  )  {
Byte[] msg =
new UTF8Encoding(true).GetBytes("Doing Some" +
" Stuff\n");
fs.Write( msg, 0, msg.Length );
}
public static void DoSomeMoreStuff()  {
// Открыть файл.
using ( FileStream fs = File.Open ( "log.txt",
FileMode.Append, FileAccess.Write, FileShare.None )  )  {
Byte[] msg =
new UTF8Encoding(true) .GetBytes("Doing Some" +
" More Stuff\n");
fs.Write( msg, 0, msg.Length );
}
static void Main()  {
DoSomeStuff();
DoSomeMoreStuff() ;
}

Как видите, этот код гораздо легче понять, и оператор using позаботится обо всем, что связано с блоками try/finally. Возможно, вы не удивитесь, просмотрев сгенерированный код в ILDASM и увидев там, что компилятор генерирует блоки try/finally вместо оператора using. Можно даже вкладывать конструкции using одну внутрь другой — точно так же, как это можно делать с блоками try/finally.

Но даже несмотря на то, что оператор using устраняет симптом некрасиво выглядящего кода и сокращает шансы появления дополнительных ошибок, он все-таки требует помнить о необходимости его применения. Это не так удобно, как детерминированная деструкция локальных объектов в С++, но это лучше, чем оснащение кода блоками try/ finally, и определенно это лучше, чем ничего.

В результате мы получаем в С# некоторую форму детерминированной деструкции через оператор using, но она остается детерминированной, только если не забыть ее сделать таковой.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100