Динамика ключевого слова using

Рассмотрим следующий код:

// module level declaration
Socket _client;

void ProcessSocket() {
    _client = GetSocketFromSomewhere();
    using (_client) {
        DoStuff();  // receive and send data

        Close();
    }
}

void Close() {
    _client.Close();
    _client = null;
}

Учитывая, что код вызывает метод Close(), который закрывает сокет _client и устанавливает его на null, но все же внутри блока `using ', что именно происходит за кулисами? Действительно ли розетка закрыта? Есть ли побочные эффекты?

P.S. Это использует С# 3.0 в .NET MicroFramework, но я полагаю, что С#, язык, должен функционировать одинаково. Причина, по которой я спрашиваю, заключается в том, что иногда, очень редко, у меня заканчиваются сокеты (что очень ценный ресурс на устройствах .NET MF).

Ответ 1

Dispose все равно будет вызываться. Все, что вы делаете, указывает на переменную _client на что-то другое в памяти (в данном случае: null). Объект, который ссылается на _client, все еще будет удален в конце инструкции using.

Запустите этот пример.

class Program
{
    static Foo foo = null;

    static void Main(string[] args)
    {
        foo = new Foo();

        using (foo)
        {
            SomeAction();
        }

        Console.Read();
    }

    static void SomeAction()
    {
        foo = null;
    }
}

class Foo : IDisposable
{
    #region IDisposable Members

    public void Dispose()
    {
        Console.WriteLine("disposing...");
    }

    #endregion
}

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

Позднее редактирование:

Относительно обсуждения комментариев MSDN с использованием ссылки http://msdn.microsoft.com/en-us/library/yh598w02.aspx и кода в OP и в моем примере я создал более простая версия этого кода.

Foo foo = new Foo();
using (foo)
{
    foo = null;
}

(И, да, объект все равно удаляется.)

Вы можете сделать вывод из ссылки выше, что код переписывается следующим образом:

Foo foo = new Foo();
{
    try
    {
        foo = null;
    }
    finally
    {
        if (foo != null)
            ((IDisposable)foo).Dispose();
    }
}

Что бы не уничтожить объект, и это не соответствует поведению фрагмента кода. Поэтому я просмотрел его через ildasm, и самое лучшее, что я могу собрать, это то, что исходная ссылка копируется в новый адрес в памяти. Оператор foo = null; применяется к исходной переменной, но вызов на .Dispose() происходит на скопированном адресе. Итак, вот посмотрите, как я считаю, что код действительно переписывается.

Foo foo = new Foo();
{
    Foo copyOfFoo = foo;
    try
    {
        foo = null;
    }
    finally
    {
        if (copyOfFoo != null)
            ((IDisposable)copyOfFoo).Dispose();
    }
}

Для справки, это то, что ИЛ выглядит через ildasm.

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       29 (0x1d)
  .maxstack  1
  .locals init ([0] class Foo foo,
           [1] class Foo CS$3$0000)
  IL_0000:  newobj     instance void Foo::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  stloc.1
  .try
  {
    IL_0008:  ldnull
    IL_0009:  stloc.0
    IL_000a:  leave.s    IL_0016
  }  // end .try
  finally
  {
    IL_000c:  ldloc.1
    IL_000d:  brfalse.s  IL_0015
    IL_000f:  ldloc.1
    IL_0010:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0015:  endfinally
  }  // end handler
  IL_0016:  call       int32 [mscorlib]System.Console::Read()
  IL_001b:  pop
  IL_001c:  ret
} // end of method Program::Main

Я не зарабатываю на жизнь, глядя на ildasm, поэтому мой анализ можно классифицировать как предостерегающий emptor. Однако поведение это то, чем оно является.

Ответ 2

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

Чтение этих правил дает понять, что код

_client = GetSocketFromSomewhere(); 
using (_client) 
{ 
    DoStuff();
    Close(); 
} 

преобразуется компилятором в

_client = GetSocketFromSomewhere();
{
    Socket temp = _client;
    try 
    { 
        DoStuff();
        Close(); 
    }
    finally
    {
        if (temp != null) ((IDispose)temp).Dispose();
    }
}

Итак, что происходит. Сокет дважды удаляется по пути неисключительного кода. Это поражает меня, вероятно, не смертельно, но определенно плохой запах. Я бы написал это как:

_client = GetSocketFromSomewhere();
try 
{ 
    DoStuff();
}
finally
{
    Close();
}

Совершенно ясно, что путь и ничто не закрывается двойным образом.

Ответ 3

Как заметил Энтони, Dispose() будет вызываться, даже если ссылка будет отменена во время выполнения блока использования. Если вы посмотрите на сгенерированный IL, вы увидите, что даже жесткий ProcessSocket() использует элемент экземпляра для хранения поля, локальная ссылка все еще создается в стеке. Именно через эту локальную ссылку вызывается Dispose().

IL для ProcessSocket() выглядит следующим образом

.method public hidebysig instance void ProcessSocket() cil managed
{
   .maxstack 2
   .locals init (
      [0] class TestBench.Socket CS$3$0000)
   L_0000: ldarg.0 
   L_0001: ldarg.0 
   L_0002: call instance class TestBench.Socket     TestBench.SocketThingy::GetSocketFromSomewhere()
   L_0007: stfld class TestBench.Socket TestBench.SocketThingy::_client
   L_000c: ldarg.0 
   L_000d: ldfld class TestBench.Socket TestBench.SocketThingy::_client
   L_0012: stloc.0 
   L_0013: ldarg.0 
   L_0014: call instance void TestBench.SocketThingy::DoStuff()
   L_0019: ldarg.0 
   L_001a: call instance void TestBench.SocketThingy::Close()
   L_001f: leave.s L_002b
   L_0021: ldloc.0 
   L_0022: brfalse.s L_002a
   L_0024: ldloc.0 
   L_0025: callvirt instance void [mscorlib]System.IDisposable::Dispose()
   L_002a: endfinally 
   L_002b: ret 
   .try L_0013 to L_0021 finally handler L_0021 to L_002b
}

Обратите внимание на локальное и обратите внимание, как это установлено, чтобы указать член на строки L_000d - L_0012. Локальный загружается снова в L_0024 и используется для вызова Dispose() в L_0025.

Ответ 4

используя только что переведенный в простой try/finally, где в блоке finally _client.Dispose() вызывается, если _client не является нулевым.

так как вы закрываете _client и устанавливаете его в null, использование ничего не делает, когда оно закрывается.