C# 相互引用的不可变对象?

C# 相互引用的不可变对象?,c#,immutability,C#,Immutability,今天我试着把我的头绕在相互参照的不可变对象上。我得出的结论是,如果不使用惰性求值,就不可能做到这一点,但在这个过程中,我编写了这段(我认为)有趣的代码 public class A { public string Name { get; private set; } public B B { get; private set; } public A() { B = new B(this); Name = "test"; }

今天我试着把我的头绕在相互参照的不可变对象上。我得出的结论是,如果不使用惰性求值,就不可能做到这一点,但在这个过程中,我编写了这段(我认为)有趣的代码

public class A
{
    public string Name { get; private set; }
    public B B { get; private set; }
    public A()
    {
        B = new B(this);
        Name = "test";
    }
}

public class B
{
    public A A { get; private set; }
    public B(A a)
    {
        //a.Name is null
        A = a;
    }
}

我发现有趣的是,我想不出另一种方法来观察A型对象处于一种尚未完全构造且包含线程的状态。为什么这是正确的?是否有其他方法来观察一个未完全构造的对象的状态?

如果您考虑初始化顺序

  • 导出静态场
  • 派生静态构造函数
  • 派生实例字段
  • 基本静态场
  • 基本静态构造函数
  • 基本实例字段
  • 基本实例构造函数
  • 派生实例构造函数

显然,通过向上转换,您可以在调用派生实例构造函数之前访问该类(这就是您不应该使用构造函数中的虚拟方法的原因。它们可以轻松访问未由构造函数初始化的派生字段/派生类中的构造函数不能使派生类处于“一致”状态state)

原则是不要让您的这个对象从构造函数主体中逃逸


观察此类问题的另一种方法是在构造函数中调用虚方法。

可以通过在构造函数中最后实例化B来避免问题:

 public A() 
    { 
        Name = "test"; 
        B = new B(this); 
    } 
如果你的建议不可能,那么A也不是一成不变的

编辑:由于leppie,修复了此问题。

完全构造”是由您的代码定义的,而不是由语言定义的

这是从构造函数调用虚拟方法的变体,
总的方针是:不要那样做


要正确实现“完全构造”的概念,请不要将
从构造函数中传递出去

实际上,在构造函数期间泄漏
引用将允许您这样做;显然,如果在不完整的对象上调用方法,可能会导致问题。至于“观察未完全构造对象状态的其他方法”:

  • 在构造函数中调用
    virtual
    方法;子类构造函数还没有被调用,因此
    重写
    可能会尝试访问未完成状态(在子类中声明或初始化的字段等)
  • 反射,可能使用
    FormatterServices.GetUninitializedObject
    (它创建对象时根本不调用构造函数)

是的,这是两个不可变对象相互引用的唯一方法-至少其中一个对象必须以不完全构造的方式看到另一个对象

但如果你对两个构造函数都有信心,并且这是可变性的唯一替代方案,我认为这并不太糟糕

为什么这是正确的


为什么你认为它是无效的

因为构造函数应该保证它包含的代码在外部代码能够观察对象的状态之前被执行

对。但编译器不负责维护该不变量。你是。如果你写的代码破坏了这个不变量,当你这么做的时候,它会伤害你,那么停止这样做

有没有其他方法可以观察未完全构造的对象的状态

当然。对于引用类型,所有这些类型都涉及以某种方式从构造函数中传递“this”,显然,因为保存对存储的引用的唯一用户代码是构造函数。构造函数泄漏“this”的一些方法是:

  • 将“this”放在一个静态字段中,并从另一个线程引用它
  • 进行方法调用或构造函数调用,并将“this”作为参数传递
  • 进行虚拟调用——如果虚拟方法被派生类重写,则尤其令人讨厌,因为它会在派生类主体运行之前运行
我说过唯一保存引用的用户代码是ctor,当然垃圾收集器也保存引用。因此,观察对象处于半构造状态的另一种有趣方式是,如果对象具有析构函数,且构造函数抛出异常(或获取异步异常,如线程中止;稍后将详细介绍)。在这种情况下,对象即将死亡,因此需要最终确定,但是终结器线程可以看到对象的半初始化状态。现在我们回到了用户代码中,可以看到半构造的对象

在这种情况下,析构函数必须是健壮的。析构函数不能依赖于由被维护的构造函数设置的对象的任何不变量,因为被销毁的对象可能永远不会被完全构造

外部代码可以观察到半构造对象的另一种疯狂方式当然是,如果析构函数在上述场景中看到半初始化的对象,然后将对该对象的引用复制到静态字段,从而确保半构造、半最终的对象从死亡中拯救出来请不要那样做。就像我说的,如果疼,就不要这样做

如果你在一个值类型的构造函数中,那么事情基本上是一样的,但是在机制上有一些小的不同。该语言要求对值类型的构造函数调用创建一个临时变量,只有ctor有权访问该变量,对该变量进行变异,然后将变异后的值进行结构复制到实际存储中。这确保了如果构造函数抛出,那么最终存储不会处于半变异状态

请注意,由于结构副本不能保证是原子的,因此另一个线程可能会看到存储处于半变异状态;如果需要,请正确使用锁
A = new Node("A");
B = new Node("B");
G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);