.NET:确定对象是部分构造还是完全构造

时间:2018-02-16 13:30:08

标签: c# .net constructor

有没有办法查询.NET运行时以确定对象是否已完成构造,或者构造是否仍在进行和/或是否因异常而中止?

基本上,相当于:

class Foo {
    public string ConstructionState { get; private set; }

    public Foo() {
        try {
            ConstructionState = "ongoing";

            // ... do actual constructor stuff here ...

            ConstructionState = "completed";
        }
        catch (Exception) {
            ConstructionState = "aborted";
            throw;
        }    
    }
}

...除了考虑字段初始值设定项,基类构造函数等外,无需修改构造函数。

1 个答案:

答案 0 :(得分:2)

一个表现良好的对象在完全构造之前不应该暴露自己。如果部分构造的对象泄露,那么您已经违反了该合同。

当然,运行时并不关心。就运行时而言,部分构造的对象没有什么特别之处 - 它仍然受到相同的内存约束,最终化和垃圾收集作为完全构造的对象。

如果您拥有该对象,解决方案很简单 - 在施工期间不要泄漏对象。在对象初始化期间进行一些全局更改的常用方法是使用静态方法(或工厂)而不是构造函数。如果你不拥有这个物品,那你就差点不幸了。

运行时规范没有明确说明没有办法检查一个对象是否是部分构造的,但它没有说(或者我可以告诉) - 所以即使你找到某种方法,它也是如此依靠它是不安全的。手动检查显示.NET对象头没有这样的信息,并且构造函数的反汇编表明在构造函数完成后可能没有非用户代码可以更新这样的状态。

运行时确实在“怪异”的地方存储了一些标志。标记&在桌面MS.NET中扫描垃圾收集器将其标记存储在例如指向虚方法表的指针的“未使用”位中。但就运行时而言,该对象甚至在其任何构造函数运行之前就已“完成” - 在构造函数(特殊实例方法)运行之前,所有这些都在newobj中的分配期间处理。对象头(也包含对象大小)和虚方法表(因此在构造函数运行之前对象是派生类型最多的)已经在此处设置,并且该实例直接使用的所有内存都已分配(和预先归零 - 所以你没有得到指向随机内存位的指针。这意味着就运行时而言,内存安全性不受部分构造的对象的影响

构造函数和另一个实例方法之间的主要区别在于构造函数必须只能在任何实例上调用一次。在CIL级别上,这只是由于您无法直接调用构造函数而强制执行 - 您只使用newobj,它将构造的对象推送到堆栈上。就像其他实例方法一样,它不会跟踪某个特定方法是否完成 - 毕竟,拥有一个永远不会完成的方法是完全合法的,并且你实际上可以做同样的事情一个(非静态)构造函数。

如果你想要运行时不关心的证明,我会向你展示......在构造函数完成之前,GC 可以收集对象:

class Test
{
    public static WeakReference<Test> someInstance;

    public static void AliveTest()
    {
        Test t;
        if (someInstance == null) Console.WriteLine("Null");
        else Console.WriteLine(someInstance.TryGetTarget(out t));
    }

    public Test()
    {
        someInstance = new WeakReference<Test>(this);

        AliveTest();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        AliveTest();
    }
}

class Program
{
    static void Main(string[] args)
    {
        Test t = new Test();
        Test.AliveTest();
        Console.ReadLine();
    }
}

这个测试程序写出True,False,False(确保在Release模式下运行它,没有调试器 - .NET可以阻止很多这样的事情来简化调试)。在构造函数完成之前已经收集了对象,这意味着在这方面没有对构造函数进行特殊处理。另一个原因是不使用“构造函数更新某些静态”模式,尤其不是“终结器更新它”。如果为此示例代码添加终结器,它将在构造函数完成之前运行。哎哟。

在一般情况下,即使您的解决方案也不够。引用CLI规范:

  

明确要求CLI的一致性实现保证在构造函数完成之前在构造函数中执行的所有状态更新都是统一可见的。

无法保证其他主题有关于构造状态的正确信息。

对于奖励积分,如果物品未密封也无济于事。派生类构造函数将在基类构造函数之后运行,而在C#中,无法重写此函数以包含所有通常在序列中运行的构造函数。你能做的最好的事情是为每个构造函数维护一个单独的“构造状态”,这最多会让人感到困惑(并打破一些OOP原则 - 它会要求对象的所有使用者知道对象可能拥有的所有可能的类型)。