【问题标题】:Dynamics of the using keywordusing关键字的动态
【发布时间】:2025-12-29 20:15:17
【问题描述】:

考虑以下代码:

// 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,同时仍在“使用”块内,幕后究竟发生了什么?套接字真的关闭了吗?有副作用吗?

附:这是在 .NET MicroFramework 上使用 C# 3.0,但我认为 c# 语言应该具有相同的功能。我问的原因是,我偶尔会用完套接字(这是 .NET MF 设备上非常宝贵的资源)。

【问题讨论】:

    标签: c# programming-languages using .net-micro-framework


    【解决方案1】:

    using just 转换为简单的 try/finally,如果 _client 不为 null,则在 finally 块中调用 _client.Dispose()

    因此,由于您关闭了 _client 并将其设置为 null,因此 using 在关闭时实际上并没有做任何事情。

    【讨论】:

    • 这是关于 SOF 的一个类似的答案,描述了正在发生的事情:*.com/questions/278902/…
    • 这个答案具有误导性。 _client 设置为与处置发生时无关;原始值是被处置的。
    【解决方案2】:

    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 处置。您所做的只是更改变量的引用,而不是更改最初引用的对象。

    后期编辑:

    关于 cmets 关于 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();
        }
    }
    

    不会释放对象,并且与代码 sn-p 的行为不匹配。所以我通过 ildasm 看了一下,我能收集到的最好的结果是原始引用正在被复制到内存中的新地址中。 foo = null; 语句适用于原始变量,但对 .Dispose() 的调用发生在复制的地址上。所以这里看看我是如何相信代码实际上正在被重写的。

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

    作为参考,这是通过 ildasm 看到的 IL。

    .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 谋生的,所以我的分析可以归类为买者自负。然而,行为就是这样。

    【讨论】:

    • 很抱歉,否决票不正确。但是,好吧,重写它。我的观点仍然成立。如果有帮助,我将发布与他的示例匹配的代码。
    • 好吧,你的第一个例子显然是错误的。但我的第二条评论(我删除了)也是如此。 :-) 在我的辩护中,这个参考有点误导——msdn.microsoft.com/en-us/library/yh598w02.aspx;请参阅if (var != null) 部分——但我对其进行了测试,你是对的。
    • 如果有助于不混淆事物,我将删除原来的 sn-p。你是对的,它与他的程序不完全匹配,尽管结果是一样的。
    • @Ben,那个链接很有趣。我重写了 using 语句以匹配 MSDN 引用的实现,果然 .Dispose() 没有被调用(保持 foo 引用代码中前面创建的 Foo 实例)。我将留给语言专家来解释 MSDN 文档如何以及为什么与此处的示例代码不匹配,因为我不是将语言规范提交给内存的人!
    • 这个编译器警告增加了一点细节msdn.microsoft.com/en-us/library/zhdyhfk6.aspx
    【解决方案3】:

    正如 Anthony 指出的那样,即使在执行 using 块期间引用为空,也会调用 Dispose()。如果你看一下生成的 IL,你会发现即使 ProcessSocket() 使用实例成员来存储字段,仍然会在堆栈上创建本地引用。正是通过这个本地引用调用了Dispose()

    ProcessSocket() 的 IL 如下所示

    .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中再次加载,用于在L_0025中调用Dispose()

    【讨论】:

    • 很高兴不是唯一一个盯着 IL 反汇编程序的人!
    【解决方案4】:

    我想你可以通过查看反汇编来弄清楚这一点,但是阅读规范的第 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();
    }
    

    这样一来就很清楚了,没有任何东西会被双重关闭。

    【讨论】:

    • 这比我的 IL 转储更容易阅读。谢谢。为什么我不直接在规范中查找。