Feel Good.

17 июня 2010

Блок using и null object

Интересное поведение наблюдается, если в блоке using используется нулевой объект: using(null){...}. Казалось бы, очевидно, у нас должно возникнуть исключение NullReferenceException при попытке вызвать метод Dispose нулевого объекта (null object), как только мы выйдем за пределы блока using:

using ((IDisposable)null)

{

}

// Исключения NullReferenceException не будет.

// Метод Dispose вызван не будет.



Рассмотрим довольно распространенную конструкцию: Допустим, есть класс Some, реализующий интерфейс IDisposable:

class Some : IDisposable

{

    public void Dispose()

    {

        Console.WriteLine("dispose");

    }

}


и статический метод (например, это может быть какой-нибудь builder), возвращающий экземпляр Some:

static Some GetSome()

{

    // Так получилось, что вернули null

    return null;

}


Вопрос: "Возникнет ли NullReferenceException исключение, когда мы выйдем за пределы блока using?".:

using (Some some = GetSome())

{

    if (some != null)

    {

        // Действия с some...

    }

}// IDisposable.Dispose()


Ответ: "Нет, метод Dispose объекта some в данной ситуации вызван не будет.".
На самом деле, блок using будет оттранслирован в блок try...finally с проверкой на null:

Some some = GetSome();

try

{

    if (some != null)

    {

        // Действия с some...

    }

}

finally

{

    if (some != null)

        ((IDisposable)some).Dispose();

}


Или:

.method private hidebysig static void  Main(string[] args) cil managed

{

  .entrypoint

  // Code size       39 (0x27)

  .maxstack  2

  .locals init ([0] class NullUsing.Some some,

           [1] bool CS$4$0000)

  IL_0000:  nop

  IL_0001:  call       class NullUsing.Some NullUsing.Program::GetSome()

  IL_0006:  stloc.0

  .try

  {

    IL_0007:  nop

    IL_0008:  ldloc.0

    IL_0009:  ldnull

    IL_000a:  ceq

    IL_000c:  stloc.1

    IL_000d:  ldloc.1

    IL_000e:  brtrue.s   IL_0012

    IL_0010:  nop

    IL_0011:  nop

    IL_0012:  nop

    IL_0013:  leave.s    IL_0025

  }  // end .try

  finally

  {

    IL_0015:  ldloc.0

    IL_0016:  ldnull

    IL_0017:  ceq

    IL_0019:  stloc.1

    IL_001a:  ldloc.1

    IL_001b:  brtrue.s   IL_0024

    IL_001d:  ldloc.0

    IL_001e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()

    IL_0023:  nop

    IL_0024:  endfinally

  }  // end handler

  IL_0025:  nop

  IL_0026:  ret

} // end of method Program::Main


Ссылки:
  1. using Statement (C# Reference)
  2. C# using statement with a null object
  3. Dynamics of the using keyword

Progg it

9 комментариев:

  1. Немного эффективнее транслировать в такой код:

    Some some = GetSome();
    if some != null {
    try {
    // Действия с some...
    }
    finally {
    some.Dispose();
    }
    }

    Ведь изменить some вроде как нельзя.

    ОтветитьУдалить
  2. Проверки на null в блоке try нет, и в try мы словим исключение NullReferenceException, поэтому в try мы сами должны вручную проверять на null. А в finally она есть.

    ОтветитьУдалить
  3. Есть еще фишка с throw null :) вот она как раз бросает NullReferenceException

    ОтветитьУдалить
  4. Да вы правы, я не заметил сразу что проверку на null нужно делать самому. Правильно что сделали именно так. Код внутри using может быть разным и делать что то полезное если вернули null. К тому же похоже не запрещено изменять переменную внутри using блока.

    Единственно, что было бы полезно - это добавить присвоение null внутри finally после вызова Dispose.

    ОтветитьУдалить
  5. > nesteruk
    Не совсем понял фишки с throw null; Насколько я понимаю эту конструкцию, работает не throw new NullReferenceException(); а скорее NullReferenceException кидается по причине непоняток, какое исключение генерировать (null).

    ОтветитьУдалить
  6. @sunexdev

    Why does C# allow you to "throw null"?
    http://stackoverflow.com/questions/2195764/why-does-c-allow-you-to-throw-null

    ОтветитьУдалить
  7. Самое интересное, что в MSDN про эту проверку на null не сказано ни слова. Хотя это и без того очевидно.

    http://msdn.microsoft.com/en-us/library/yh598w02.aspx

    ОтветитьУдалить
  8. По-моему то что using не бросает исключение - вполне логично и даже удобно, так как в этом случае можно писать чего-нибудь наподобие:
    using (someObject as IDisposable)
    {
    }
    И не бояться исключения.

    ОтветитьУдалить
  9. @dev-pit

    Да, логично, но не очевидно, поэтому и пришлось в этом убедиться.

    ОтветитьУдалить