如何实例化不可变的相互递归对象?

我有一个不可变的递归类型:

public sealed class Foo { private readonly object something; private readonly Foo other; // might be null public Foo(object something, Foo other) { this.something = something; this.other = other; } public object Something { get { return something; } } public Foo Other { get { return other; } } } 

我需要实例化这种相互引用的两个对象,即a.Other == b && b.Other == a

我不想放弃不变性不变量,因为Foo旨在用作flyweight。 我可以(而且我认为必须)在字段上放弃readonly ,并保持“guts”可变,但公共接口必须是不可变的。

冰棒不变性是完成这项工作的唯一途径吗?

我正在尝试为一系列类型建模。 每种类型都有一个名称和几个属性。 每个属性都有一个名称和一个类型。 有一些相互递归的类型,这就是出现这个问题的地方。

还有其他方法可以完成它,但它们可能没有您想要的属性。

假设您想要表示一个不可变的值树,从树叶中构建。 这很简单。 您可能有一个节点构造函数,它接受一个值和一个子节点列表。 这使得用古树建造新树非常简单,并且它们保证是非循环的。

现在假设您想要表示值的不可变有向图。 现在你遇到了节点可以有周期的问题; 可能没有“叶子”来构建图表。 解决方案是放弃节点知道其邻居是什么的原则。 您可以通过创建一组不可变的节点和一个不可变的边缘列表来表示不可变图形。 要将节点添加到不可变图形,您需要构建一个新图形,并将该节点添加到节点集中。 同样地增加边缘; 你构造一个新的边列表。 现在,图形拓扑中存在周期的事实是无关紧要的; 没有一个对象在它引用的对象中有一个循环。

如果不了解您的实际问题空间,很难说不可变数据结构对您的应用程序有用。 你能告诉我们更多关于你想要做什么的事吗?

我正在尝试为一系列类型建模。 每种类型都有一个名称和几个属性。 每个属性都有一个名称和一个类型。 有一些相互递归的类型,这就是出现这个问题的地方。

那么geez,你应该首先这样说。 如果我知道一件事,那就是对类型的分析。 显然,编译器需要能够处理各种类型的疯狂类型,包括具有循环基类的类型,涉及内部类型的循环,类型参数,类型约束等。

在C#编译器中,我们主要通过使对象在其不变性中“暂存”来解决这些问题。 也就是说,当我们首先创建一组类型时,每个类型对象都知道它的名称及其在源代码(或元数据)中的位置。 然后名称变得不可变。 然后我们解析基类型并检查它们的周期; 然后基类型变为不可变的。 然后我们检查类型约束…然后我们解析属性……依此类推,直到分析完所有内容。

我考虑过其他方法。 例如,我们可能使用我刚才为图表建议的相同技术:创建一个名为“compilation”的不可变对象,您可以向其添加类型,从而生成新的不可变编译。 编译可以跟踪不可变哈希映射中类型及其基类型之间的“边”,然后可以检查结果图的周期。 然后,一个类型不知道它的基本类型; 你必须要求编译类型的基本类型是什么。

你可以在这里做同样的事情。 您可以拥有一个类“排版”,其中包含一组不可变类型,以及从类型到一组不可变属性的多重映射。 您可以根据需要构建一组类型和属性集; 改变的是地图,而不是类型。

不利的一面是你不再要求类型的属性; 你问排版类型的属性。 如果您需要独立于任何排版集传递类型,那么这可能无法满足您的需求。

使用一次写入不变性绝对不可能。 让我解释一下原因。 您只能在构造函数中设置字段的值。 因此,如果你想要引用b你必须将对b引用传递给a构造函数。 但是b已经被冻结了。 所以唯一的选择是在a构造函数中实例化b 。 但这是不可能的,因为你无法传递对a引用,因为this在构造函数中是无效的。

从这一点来看, 冰棒不变性是最简单,最优雅的解决方案。 另一种方法是创建静态方法Foo.CreatePair ,它将实例化两个对象,设置交叉引用并返回冻结对象。

 public sealed class Foo { private readonly object something; private Foo other; // might be null public Foo(object something, Foo other) { this.something = something; this.other = other; } public object Something { get { return something; } } public Foo Other { get { return other; } private set { other = value; } } public static CreatePair(object sa, object sb) { Foo a = new Foo(sa, null); Foo b = new Foo(sb, a); a.Other = b; return a; } } 

下面是一个Foo类的例子,它具有一次写入不变性而不是冰棒免疫性。 有点难看,但很简单。 这是一个概念validation; 你需要调整它以满足你的需求。

 public sealed class Foo { private readonly string something; private readonly Foo other; // might be null public Foo(string s) { something = s; other = null; } public Foo(string s, Foo a) { something = s; other = a; } public Foo(string s, string s2) { something = s; other = new Foo(s2, this); } } 

用法:

 static void Main(string[] args) { Foo xy = new Foo("a", "b"); //Foo other = xy.GetOther(); //Some mechanism you use to get the 2nd object } 

考虑将构造函数设为私有并创建一个工厂来生成Foo对,因为这样写起来相当丑陋,并迫使您向其他人提供公共访问权限。

您可以通过添加接受函数的构造函数重载并将this引用传递给该函数来实现。 像这样的东西:

 public sealed class Foo { private readonly object something; private readonly Foo other; public Foo(object something, Foo other) { this.something = something; this.other = other; } public Foo(object something, Func makeOther) { this.something = something; this.other = makeOther(this); } public object Something { get { return something; } } public Foo Other { get { return other; } } } static void Main(string[] args) { var foo = new Foo(1, x => new Foo(2, x)); //mutually recursive objects Console.WriteLine(foo.Something); //1 Console.WriteLine(foo.Other.Something); //2 Console.WriteLine(foo.Other.Other == foo); //True Console.ReadLine(); }