互相引用的不可变对象?

今天,我正试图围绕不可变对象进行相互引用。 我得出的结论是,如果不使用懒惰的评估,你不可能做到这一点,但在我写这个(在我看来)有趣的代码的过程中。

public class A { public string Name { get; private set; } public BB { get; private set; } public A() { B = new B(this); Name = "test"; } } public class B { public AA { get; private set; } public B(A a) { //a.Name is null A = a; } } 

我觉得有趣的是,我不能想到另一种方式来观察typesA的对象在一个尚未完全构build的状态下,并且包含线程。 为什么这甚至是有效的? 有没有其他的方法来观察一个没有完全构造的物体的状态?

为什么这甚至是有效的?

你为什么期望它是无效的?

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

正确。 但编译器不负责维护该不variables。 你是 。 如果你编写的代码打破了这个不变性,那么当你这样做的时候会感到痛苦,那么就停止这样做吧

有没有其他的方法来观察一个没有完全构造的物体的状态?

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

  • 把“this”放在一个静态字段中,并从另一个线程引用它
  • 进行方法调用或构造函数调用,并将“this”作为参数
  • 做一个虚拟调用 – 特别讨厌如果虚拟方法被派生类覆盖,因为它然后运行派生类ctor体之前运行。

我说过,唯一拥有引用的用户代码是ctor,但是垃圾收集器当然也有一个引用。 因此,一个对象可以观察到处于半build立状态的另一个有趣的方式是如果对象具有析构函数,并且该构造函数抛出exception(或者像线程中止那样获得asynchronousexception;稍后将会介绍。 )在这种情况下,对象即将死亡,因此需要最终确定,但是终结器线程可以看到对象的半初始化状态。 现在我们又回到了用户代码,可以看到半结构化的对象!

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

当然,如果析构函数在上面的场景中看到半初始化的对象,然后将该对象的引用复制到一个静态字段,从而确保一半的构造对象可以被外部代码观察到,结构完整的物体从死亡中解救出来。 请不要这样做。 就像我说的,如果疼的话,不要这样做。

如果你在一个值types的构造函数中,那么事物基本上是一样的,但是这个机制有一些细微的差别。 该语言要求对一个值types的构造函数调用创build一个临时variables,只有ctor有权访问该variables,对该variables进行变异,然后将变异值的结构副本作为实际存储。 这确保如果构造函数抛出,则最终的存储不处于半变异状态。

请注意,由于结构副本不能保证是primefaces的,所以另一个线程可能以半变异的状态查看存储; 如果您处于这种情况,请正确使用锁。 另外,像线程中止这样的asynchronousexception可能会在结构副本中途抛出。 无论副本是临时还是“常规”副本,都会出现这些非primefaces性问题。 一般情况下,如果有asynchronousexception,则保持很less的不variables。

实际上,如果C#编译器能够确定这种情况没有办法产生,那么C#编译器会优化临时分配和复制。 例如,如果新值正在初始化一个未被lambdaclosures的本地,而不是迭代器块,则S s = new S(123); 只是直接改变s

有关值types构造函数如何工作的更多信息,请参阅:

揭开价值types的另一个神话

有关C#语义语义如何试图从您自己拯救您的更多信息,请参阅:

为什么初始化函数以相反的顺序作为构造函数运行? 第一部分

为什么初始化函数以相反的顺序作为构造函数运行? 第二部分

我似乎偏离了手头的话题。 在一个结构中,你当然可以用相同的方式观察一个对象的半结构 – 将半结构对象复制到一个静态字段,调用一个“this”作为参数的方法,依此类推。 (很显然,在更多的派生types上调用虚方法对于结构来说不是问题)。正如我所说的,从临时存储到最终存储的副本不是primefaces的,因此另一个线程可以观察到半复制的结构。


现在让我们来考虑一下你的问题的根本原因:你如何制作相互引用的不可变对象?

通常情况下,你发现,你不知道。 如果你有两个互相引用的不可变对象,那么它们在逻辑上构成一个有向循环图 。 你可能会考虑简单地构build一个不可改变的有向图! 这样做很容易。 一个不可变的有向图包含:

  • 一个不可变的不可变节点列表,每个节点都包含一个值。
  • 一个不可变的不可变节点对列表,每个节点对都有一个图边的起点和终点。

现在你使节点A和B彼此“引用”的方式是:

 A = new Node("A"); B = new Node("B"); G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A); 

你完成了,你已经有了一个图表,A和B“互相参照”。

问题当然是,如果没有G,你就不能从A那里得到B。 有这额外的间接水平可能是不能接受的。

是的,这是两个不可变对象互相引用的唯一方式 – 至less其中一个必须以非完全构造的方式看另一个。

让它从构造函数中逃脱通常是一个糟糕的主意,但是如果你对构造函数的作用有信心的话,这是唯一的可变select,我不认为这糟糕了。

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

这是从构造函数调用虚方法的一种变体,
一般的指导方针是: 不要这样做

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

事实上,在构造函数中泄漏this引用将允许你这样做; 如果方法被调用到不完整的对象上,可能会导致问题。 至于“观察未完全构造物体状态的其他方法”:

  • 在构造函数中调用virtual方法; 子类的构造函数将不会被调用,所以override可能会尝试访问不完整的状态(在子类中声明或初始化的字段等)
  • reflection,也许使用FormatterServices.GetUninitializedObject (它根本不调用构造函数创build一个对象)

如果你考虑初始化顺序

  • 派生静态字段
  • 派生的静态构造函数
  • 派生实例字段
  • 基本的静态字段
  • 基本的静态构造函数
  • 基础实例字段
  • 基础实例构造函数
  • 派生实例构造函数

显然,通过上传,你可以访问类,然后派生实例构造函数被调用(这是你不应该使用构造函数的虚方法的原因,他们可以很容易地访问未派生类的构造函数/构造函数初始化的派生字段不能将派生类带入“一致”状态)

你可以通过在你的构造函数中实例化B来避免这个问题:

  public A() { Name = "test"; B = new B(this); } 

如果你的build议是不可能的,那么A就不会是不变的。

编辑:固定,感谢嬉皮士。

原则是不要让你的这个对象从构造函数体中逃脱。

观察这种问题的另一种方法是通过调用构造函数中的虚拟方法。

如前所述,编译器无法知道某个对象在什么位置被构造得足够有用; 因此它假设一个从构造函数中传递过来的程序员将知道一个对象是否被构造得足以满足他的需要。

然而,我会补充说,对于那些意图是真正不可变的对象,必须避免把this传递给任何代码,在它被赋予其最终值之前,它将检查字段的状态。 这意味着this不会被传递给任意的外部代码,但并不意味着将正在构build的对象传递给另一个对象是为了存储一个实际上不会被使用的后向引用。第一个构造函数已经完成

如果有人正在devise一种语言以便于build造和使用不可变对象,那么它可能会有助于宣布这种方法仅在施工期间才能使用,只有在施工结束后才可用; 可以在施工期间声明为不可解除引用,之后是只读的。 同样可以标记参数以指示应该是不可解除引用的。 在这种制度下,编制者有可能允许build立彼此相互引用的数据结构,但在观察到之后没有任何财产可以改变。 至于这种静态检查的好处是否会大于成本,我不确定,但也许是有趣的。

顺便说一下,一个相关的function将是有用的是能够声明参数和函数返回为短暂的,可返回的,或(默认)持久。 如果一个参数或者函数的返回值被声明为临时值,那么它不能被复制到任何字段,也不能作为一个可持久parameter passing给任何方法。 另外,将一个临时或可返回值作为可返回parameter passing给方法会导致函数的返回值inheritance该值的限制(如果函数有两个可返回参数,则其返回值将inheritance其限制性较强参数)。 Java和.net的一个主要缺点是所有的对象引用都是混杂的, 一旦外面的代码得到它的手,没有任何人可能会结束它。 如果参数可以被短暂声明,那么对于只有某个事物的唯一引用的代码才可能知道它是唯一的引用,从而避免不必要的防御复制操作。 另外,如果编译器能够知道在返回之后不存在对它们的引用,那么闭包等事情就可以被回收。