Это был вопрос о телефонном интервью, который у меня был: есть ли время, когда Dispose не будет вызван на объект, область видимости которого объявляется блоком использования?
Мой ответ не был - даже если исключение происходит во время использования блока, Dispose все равно будет вызываться.
Интервьюер не согласился и сказал, что если using
завернут в блок try
- catch
, тогда Dispose не будет вызываться к моменту ввода блока catch.
Это противоречит моему пониманию конструкции, и я не смог найти ничего, что подкрепляет точку зрения интервьюеров. Правильно ли он или я мог неправильно понять вопрос?
Ответ 1
void Main()
{
try
{
using(var d = new MyDisposable())
{
throw new Exception("Hello");
}
}
catch
{
"Exception caught.".Dump();
}
}
class MyDisposable : IDisposable
{
public void Dispose()
{
"Disposed".Dump();
}
}
Это произвело:
Disposed
Exception caught
Итак, я согласен с вами, а не с интервьюемщиком умных...
Ответ 2
Четыре вещи, которые заставят Dispose не вызываться в используемом блоке:
Ответ 3
Необычно я прочитал об одном обстоятельстве, в котором Dispose не будет вызван в блоке использования только сегодня утром. Оформить заказ blog в MSDN. Это вокруг использования Dispose с IEnumerable и ключевым словом yield, когда вы не перебираете всю коллекцию.
К сожалению, это не касается случая исключения, честно говоря, я не уверен в этом. Я бы ожидал, что это будет сделано, но, может быть, стоит проверить с небольшим количеством кода?
Ответ 4
Остальные ответы об отключении питания, Environment.FailFast()
, итераторах или обманах using
, что-то, что есть null
, являются интересными. Но мне любопытно, что никто не упоминал, что я считаю наиболее распространенной ситуацией, когда Dispose()
не будет вызываться даже при наличии using
: когда выражение внутри using
выдает исключение.
Конечно, это логично: выражение в using
выдало исключение, поэтому присваивание не состоялось, и мы ничего не могли назвать Dispose()
. Но одноразовый объект уже может существовать, хотя он может быть в половине инициализированного состояния. И даже в этом состоянии он уже может содержать некоторые неуправляемые ресурсы. Это еще одна причина, по которой важно правильно использовать одноразовый шаблон.
Пример проблемного кода:
using (var f = new Foo())
{
// something
}
…
class Foo : IDisposable
{
UnmanagedResource m_resource;
public Foo()
{
// obtain m_resource
throw new Exception();
}
public void Dispose()
{
// release m_resource
}
}
Здесь он выглядит как Foo
выпускает m_resource
правильно, и мы также правильно используем using
. Но Dispose()
on Foo
никогда не вызывается из-за исключения. Исправление в этом случае заключается в том, чтобы использовать финализатор и освобождать его там.
Ответ 5
Блок using
преобразуется компилятором в собственный блок try
/finally
в пределах существующего блока try
.
Например:
try
{
using (MemoryStream ms = new MemoryStream())
throw new Exception();
}
catch (Exception)
{
throw;
}
становится
.try
{
IL_0000: newobj instance void [mscorlib]System.IO.MemoryStream::.ctor()
IL_0005: stloc.0
.try
{
IL_0006: newobj instance void [mscorlib]System.Exception::.ctor()
IL_000b: throw
} // end .try
finally
{
IL_000c: ldloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0015: endfinally
} // end handler
} // end .try
catch [mscorlib]System.Exception
{
IL_0016: pop
IL_0017: rethrow
} // end handler
Компилятор не изменит порядок. Так происходит так:
- Исключение выбрано или распространяется на блок
using
try
part
- Элемент управления оставляет блок
using
block try
и вводит его finally
часть
- Объект размещается кодом в блоке
finally
- Элемент управления оставляет блок finally, и исключение распространяется на внешний
try
- Элемент управления оставляет внешний
try
и переходит в обработчик исключений
Точка, внутренний блок finally
всегда выполняется перед внешним catch
, потому что исключение не распространяется до завершения блока finally
.
Единственный нормальный случай, когда этого не произойдет, находится в генераторе (извините, "итератор" ).
Итератор превращается в полузасушливый конечный автомат, а блоки finally
не гарантируются, если он становится недоступным после yield return
(но до его размещения).
Ответ 6
using (var d = new SomeDisposable()) {
Environment.FailFast("no dispose");
}
Ответ 7
Да, есть случай, когда dispose не будет называться... вы уже думали об этом. Дело в том, что переменная в используемом блоке null
class foo
{
public static IDisposable factory()
{
return null;
}
}
using (var disp = foo.factory())
{
//do some stuff
}
не будет генерировать исключение, но если бы dispose вызывается в каждом случае. Конкретный случай, о котором говорил ваш интервьюер, ошибочен.
Ответ 8
Интервьюер частично прав. Dispose
может неправильно очищать базовый объект в каждом конкретном случае.
WCF, например, имеет несколько известных проблем, если исключение выбрано в блоке использования. Вероятно, ваш интервьюер думал об этом.
Вот статья из MSDN о том, как избегать проблем с блоком использования с WCF. Вот официальное решение Microsoft, хотя теперь я думаю, что сочетание этого ответа и этого элегантный подход.