只读交叉 class 引用:第 22 条军规

Readonly cross-class references: A Catch-22

我们有一组 classes 专门使用 readonly 字段来帮助确保 classes 在整个软件范围内安全稳定,尤其是跨线程.众所周知,只读数据是一件好事™。

特别是,这些 classes 使用 readonly 来确保在代码库中工作的其他开发人员不能更改它们 - 即使是意外 - 包括在相同 class 的方法内:这是一项安全检查,可以让技能较低或代码库知识较少的编码人员远离麻烦,这就是为什么这些 class 更喜欢使用 readonly 而不是只使用 属性 和private set 方法。没有比编译器本身告诉您“NO”更好的安全检查了。


但是当 一切 都是 readonly 时,就会出现一个有趣的第二十二条军规,也就是说,如果假定引用,parent/child 关系就变得不可能了指向两种方式。考虑以下代码:

public class Parent
{
    public readonly IList<Child> Children;

    public Parent(IEnumerable<Children> children)
    {
        Children = Array.AsReadOnly(children.ToArray());
    }
}

public class Child
{
    public readonly Parent Parent;

    public Child(Parent parent)
    {
        Parent = parent;
    }
}

这是合法的 C# 代码。它会编译。这是建立关系模型的正确方式。

但是不可能正确地实例化 ParentChildParent 无法实例化,直到你有一个完整的子集合可以附加到它,但是每个 Child 不能在没有有效的 Parent 的情况下实例化以将其挂起。

(在我们的例子中,数据形成了树,但这个问题出现在许多其他场景中:同一问题的变体出现在 readonly 双链表、readonly DAG 中, readonly 图表等。)


通常,我使用某种技巧解决了这个 Catch-22。我过去使用的选项包括:

简而言之,对于这个问题,我找不到一个简单的答案。我过去曾在不同的时间点使用过上述每种解决方案,但它们都非常令人反感,我想知道是否有人有更好的答案。


所以,总而言之:对于 Catch-22 是否有更好的解决方案,即完全 readonly 交叉 class 引用?


(是的,我知道在某些方面,我正在尝试使用技术来解决人的问题,但这不是重点:问题是交叉 class readonly 模式可以实现功能化,而不是它是否应该首先存在。)

我认为这更多是设计问题而不是语言问题。

Child class 真的需要了解 Parent 吗?
如果是这样,那么它需要什么样的信息或功能。可以移到Parent吗?
如果按照"Tell not ask"rule/principle,那么在Parentclass中就可以调用Child方法,将Parent的信息作为参数传递。

如果您仍然采用当前方法,那么您的 ParentChild class 具有 "circular" 依赖性,可以通过引入新的 class 将它们结合起来。

public class Family
{
    private readonly Parent _parent;
    private readonly ReadOnlyCollection<Child> _childs;

    public Family(Parent parent, IEnumerable<Child> childs)
    {
        _parent = parent;
        _childs = new ReadOnlyCollection<Child>(childs.ToList());
    }
}

您真正谈论的是不可变的 类,而不仅仅是只读字段。因此,要解决您的问题,请查看 Microsoft's immutable collections 做什么。

他们所做的是他们有“builder classes”,这是唯一允许打破不变性规则的东西,允许这样做是因为它所做的更改在 [= 外部不可见26=] 直到你在上面调用 ToImmutable()

您可以对 Parent.Builder 做类似的事情,它可以有一个 public void AddChild(Foo foo, Bar bar, Baz baz) 将子级添加到父级的内部状态。

添加完所有内容后,您在构建器上调用 public Parent ToImmutalbe(),它 returns 一个 Parent 对象,其中包含所有不可变的子对象。

正如我在评论中提到的,另一种不需要损害 parent/children 引用的 readonly-ness 的可能性是通过以下方式构建 "Child" objects传递给 "Parent" object:

的工厂方法
public class Parent
{
    private readonly Child[] _children;

    public Parent(Func<Parent, IEnumerable<Child>> childFactory)
    {
        _children = childFactory(this).ToArray();
    }
}

public class Child
{
    private readonly Parent _parent;

    public Child(Parent parent)
    {
        _parent = parent;
    }
}

有很多方法可以生成传递给 Parent 构造函数的工厂,例如只需创建一个 parent 和 5 children 你可以使用类似的东西:

private IEnumerable<Child> CreateChildren(Parent parent)
{
    for (var i = 0; i < 5; i++)
    {
        yield return new Child(parent);
    }
}

...

var parent = new Parent(CreateChildren);

...

正如评论中也提到的那样,这种机制并非没有潜在的缺点 - 你必须确保你的 parent object 在调用 child 工厂之前完全初始化构造函数,因为它可以在 parent object 上执行操作(调用方法、访问属性等),因此 parent object 必须处于以下状态这不会导致意外行为。如果有人从你的 parent object 派生,事情会变得更加复杂,因为派生的 class 永远不会在调用 child 工厂之前完全初始化(因为基础 class 在派生的 class).

中的构造函数之前被调用

因此,您的里程可能会有所不同,由您来确定这种方法的好处是否超过成本。