Есть ли ситуация, при которой Dispose не будет вызываться для "использования" блока?

Это был вопрос о телефонном интервью, который у меня был: есть ли время, когда 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, хотя теперь я думаю, что сочетание этого ответа и этого элегантный подход.