【问题标题】:Can I safely use object initializers inside using statements?我可以在 using 语句中安全地使用对象初始化器吗?
【发布时间】:2019-12-03 20:02:19
【问题描述】:

我想知道object initializers inside using statements 的使用是否会以某种方式阻止正确处理其中声明的资源,例如

using (Disposable resource = new Disposable() { Property = property })
{
   // ...
}

我读过对象初始化器不过是同步糖,编译器会将其转换为类似于以下代码的内容:

MyClass tmp = new MyClass(); 
tmp.Property1 = 1;
tmp.Property2 = 2;
actualObjectYouWantToInitialize = tmp;

即使我可能看起来像一个困惑无知的人,我也想要求澄清一下。初始化对象(据我所知)是指向另一个对象的指针(据我所知,这也是一个指针)这一事实是否会干扰using 语句完成的资源处置?

【问题讨论】:

  • @TimSchmelter 我刚刚用以下类测试了这个:public class Test : IDisposable { public string Property { get => "Hello"; set => throw new Exception();} public void Dispose() { Console.WriteLine("Disposed"); } }。运行 using(var x = new Test() { Property = "Test"}) 时未调用 Dispose。
  • @JonathonChase 有趣,我认为这是因为编译器将using 转换为的try-finally 在调用Dispose() 之前检查资源是否为null,它是初始化失败的时候。
  • @StackLloyd 许多人都有设置resource 变量的心理模型,并在其上设置了 then 属性(这是错误的)。这种心智模式可能就是为什么有些人认为该对象将被处置的原因。

标签: c# using-statement object-initializers


【解决方案1】:

主要(唯一?)危险是如果设置Property 失败(即抛出异常),则resource won't be Disposed

using (Disposable resource = new Disposable() { Property = property })
{
   // ...
}

通常 using 块中的异常是正常的 - 因为 usingtry .. finally 的语法糖。

这里的问题是当Property = property 正在执行你有效地 还没有“内部”using 块。 这与构造函数抛出异常时发生的情况本质上没有什么不同。

finally 块将尝试 Dispose 的东西是 resource - 但从未设置 resource - resource (如您的 actualObjectYouWantToInitialize 示例所示)设置为 after the properties have all been set

https://dotnetfiddle.net/vHeu2F 展示了这在实践中是如何发生的。请注意,Dispose 记录了一次,即使有两个 using 块。

using System;

namespace Bob
{
    public class Disposable : IDisposable
    {
        private int _property;

        public int Property { get => _property; set => throw new Exception(); }

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

    public class Program
    {
        public static void Main()
        {
            Console.WriteLine("1");
            using (var resource = new Disposable())
            {
                Console.WriteLine("2");
            }
            Console.WriteLine("3");


            using (var resource = new Disposable() { Property = 1 })
            {
                Console.WriteLine("4");

            }
            Console.WriteLine("5");
        }
    }
}

CA2000 可能有助于检测此问题。

【讨论】:

  • @StackLloyd 通过使用对象初始化器语法,首先创建对象,然后设置属性。因此,实例(需要处置)创建,但没有(如您所说)分配给将在 finally 块中处置的变量。所以你有泄漏。
  • @RenéVogt 明白了。好吧,这很可耻……总的来说,这意味着 using 语句中的对象初始化器应该被视为不好的做法。
  • @StackLloyd 我不确定我会称之为 disgraceful。这是两个语法糖特征如何相交的副作用(或后果)。如果您考虑一下,这是完全有道理的(我的意思是它可能不是您想要的 - 但考虑到这两个功能如何独立工作,它们一起玩的方式是有道理的)。此外,在实践中,这很少成为问题。大多数属性设置器不会抛出异常。
  • @mjwills 虽然这完全正确,但我认为应该以某种方式警告 C# 程序员这种交互,因为它们几乎从来都不是问题,直到它们无意中变成这样。在这里发帖之前,我在官方文档上搜索了答案,但无济于事。
【解决方案2】:

@mjwills 的答案是正确的。以下是详细信息:

public void M()
{
    using (var x = new Test{Property = ""})
    {

    }
}

将生成以下 IL 代码:

.method public hidebysig 
    instance void M () cil managed 
{
    // Method begins at RVA 0x206c
    // Code size 35 (0x23)
    .maxstack 3
    .locals init (
        [0] class Test
    )

    IL_0000: nop
    IL_0001: newobj instance void Test::.ctor()
    IL_0006: dup
    IL_0007: ldstr ""
    IL_000c: callvirt instance void Test::set_Property(string)
    IL_0011: nop
    IL_0012: stloc.0
    .try
    {
        IL_0013: nop
        IL_0014: nop
        IL_0015: leave.s IL_0022
    } // end .try
    finally
    {
        // sequence point: hidden
        IL_0017: ldloc.0
        IL_0018: brfalse.s IL_0021

        IL_001a: ldloc.0
        IL_001b: callvirt instance void [mscorlib]System.IDisposable::Dispose()
        IL_0020: nop

        // sequence point: hidden
        IL_0021: endfinally
    } // end handler

    IL_0022: ret
} // end of method Test::M

可以看到在进入try之前调用了property setter,这样会导致finally在setter出现异常时不会被调用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-01
    • 1970-01-01
    • 2011-10-07
    • 2017-09-20
    • 2020-06-28
    • 2013-06-27
    相关资源
    最近更新 更多