Динамика ключевого слова 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. Здесь используется C # 3.0 в .NET MicroFramework, но я полагаю, что язык C # должен работать идентично. Причина, по которой я спрашиваю, заключается в том, что иногда, очень редко, у меня заканчиваются сокеты (что является очень ценным ресурсом для устройств .NET MF).


person AngryHacker    schedule 25.03.2010    source источник


Ответы (4)


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 не уничтожает объект и не препятствует его удалению с помощью using. Все, что вы делаете, это изменяете ссылку на переменную, а не изменяете объект, на который изначально ссылались.

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

Что касается обсуждения из комментариев о 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();
    }
}

Для справки, вот как выглядит IL через 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, поэтому мой анализ можно классифицировать как caveat emptor. Однако поведение то, что есть.

person Anthony Pegram    schedule 25.03.2010
comment
Извините, но голосование "против" неверно. Но, ладно, перепиши это. Моя точка зрения все еще в силе. Я отправлю код, соответствующий его примеру, если это поможет. - person Anthony Pegram; 25.03.2010
comment
Что ж, ваш первый пример был явно неправильным. Но так был и мой второй комментарий (который я удалил). :-) В свою защиту ссылка немного вводит в заблуждение - msdn.microsoft .com / en-us / library / yh598w02.aspx; см. часть if (var != null) - но я ее проверил, и вы правы. - person Ben M; 25.03.2010
comment
Если это поможет не путать, я удалю исходный фрагмент. Вы правы в том, что это не совсем соответствовало его программе, хотя результат тот же. - person Anthony Pegram; 25.03.2010
comment
@Ben, эта ссылка интересная. Я переписал оператор using, чтобы он соответствовал упомянутой реализации MSDN, и, конечно же, .Dispose () не был вызван (сохраняя foo, ссылаясь на экземпляр Foo, созданный ранее в коде). Я оставлю это на усмотрение языковых гуру, чтобы они, возможно, объяснили, как и почему документация MSDN не соответствует приведенному здесь примеру кода, потому что я не из тех, кто сохраняет спецификацию языка в памяти! - person Anthony Pegram; 25.03.2010
comment
Это предупреждение компилятора добавляет небольшую деталь msdn.microsoft.com/en-us/library /zhdyhfk6.aspx - person Brian Rasmussen; 25.03.2010
comment
Мой пост пересек ваше последнее обновление, но я опубликовал закомментированную версию IL для ProcessSocketMethod. - person Brian Rasmussen; 25.03.2010

Я полагаю, вы могли бы понять это, посмотрев на разборку, но намного проще просто прочитать раздел 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();
}

Это совершенно ясно, и ничего не закрывается дважды.

person Eric Lippert    schedule 25.03.2010
comment
Это намного легче читать, чем мой дамп IL. Спасибо. Почему я просто не посмотрел это в спецификации. - person Brian Rasmussen; 25.03.2010

Как указал Энтони, Dispose() будет вызываться, даже если ссылка обнуляется во время выполнения блока using. Если вы посмотрите на сгенерированный 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
}

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

person Brian Rasmussen    schedule 25.03.2010
comment
Рад не быть единственным, кто таращится на дизассемблер ИЖ! - person Anthony Pegram; 25.03.2010

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

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

person Joel    schedule 25.03.2010
comment
Вот ответ на SOF, который описывает то, что происходит: stackoverflow.com/questions/278902/ - person Joel; 25.03.2010
comment
Этот ответ вводит в заблуждение. Значение _client, установленное на момент удаления, не имеет значения; исходное значение - это то, что удаляется. - person Eric Lippert; 25.03.2010