不可变对象相互引用?

时间:2011-10-05 12:49:37

标签: 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 B
{
    public A A { get; private set; }
    public B(A a)
    {
        //a.Name is null
        A = a;
    }
}

我觉得有趣的是,我想不出另一种方法来观察A类型的对象在一个尚未完全构造并包含线程的状态。为什么这甚至有效?有没有其他方法可以观察未完全构造的物体的状态?

8 个答案:

答案 0 :(得分:105)

答案 1 :(得分:47)

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

它是generally a bad idea to let this escape from your constructor但是如果你对两个构造函数的作用有信心,并且它是可变性的唯一替代方法,我认为它不会坏。

答案 2 :(得分:22)

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

这是从构造函数调用虚方法的变体,
一般准则是:不要那样做

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

答案 3 :(得分:8)

确实,在构造函数中泄露this引用将允许您执行此操作;如果在不完整的对象上调用方法,它可能会导致问题。至于“观察未完全构建的物体状态的其他方法”:

  • 在构造函数中调用virtual方法;子类构造函数尚未被调用,因此override可能会尝试访问不完整的状态(在子类中声明或初始化的字段等)
  • 反射,也许使用FormatterServices.GetUninitializedObject(创建一个对象而不调用构造函数

答案 4 :(得分:6)

如果考虑初始化顺序

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

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

答案 5 :(得分:4)

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

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

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

编辑:修复,感谢leppie。

答案 6 :(得分:3)

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

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

答案 7 :(得分:1)

如上所述,编译器无法知道对象在什么位置构建得足够有用;因此,假设从构造函数传递this的程序员将知道对象是否已经构建得足够好以满足他的需要。

但是,我要补充一点,对于那些旨在真正不可变的对象,必须避免将this传递给任何代码,这些代码将在为其分配最终值之前检查字段的状态。这意味着this不会被传递给任意外部代码,但并不意味着将正在构建的对象传递给另一个对象以存储反向引用将会出现任何问题。直到第一个构造函数完成后才实际使用

如果正在设计一种语言来促进不可变对象的构造和使用,那么将方法声明为仅在构造期间,仅在构造之后或者其中之一时可能有用;字段可以在构造期间声明为不可解除引用,之后是只读的;同样可以标记参数以指示应该是不可解除引用的。在这样的系统下,编译器可以允许构建彼此引用的数据结构,但是在观察之后没有属性可以改变。至于这种静态检查的好处是否会超过成本,我不确定,但它可能会很有趣。

顺便提一下,一个有用的相关功能是能够将参数和函数返回声明为短暂的,可返回的或(默认的)可持久的。如果参数或函数返回被声明为ephemeral,则无法将其复制到任何字段,也不能作为可持久参数传递给任何方法。此外,将一个临时或可返回值作为可返回参数传递给方法将导致函数的返回值继承该值的限制(如果函数具有两个可返回参数,则其返回值将从其继承更严格的约束参数)。 Java和.net的一个主要缺点是所有对象引用都是混杂的;一旦外部代码得到了一个,就不知道谁可能最终得到它。如果参数可以被声明为短暂的,那么对于持有唯一引用某些东西的代码来说,通常可能知道它只保留了唯一的引用,从而避免了不必要的防御性复制操作。此外,如果编译器可以知道在返回后不存在对它们的引用,那么像闭包这样的东西可以被回收。