只读的跨类引用:A Catch-22

时间:2016-12-01 17:59:41

标签: c# hierarchical-data readonly

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

特别是,这些类使用readonly来确保在代码库中工作的其他开发人员不会改变它们 - 即使是偶然的 - 包括在同一类中的方法中:它是一个安全检查以保持编码器技能较低或代码库知识较少的问题,这就是为什么这些类更喜欢使用readonly而不仅仅是拥有private set方法的属性。没有比编译器本身告诉你的更好的安全检查" "。

但是当所有readonly时,会出现一个有趣的Catch-22,如果引用应该指向那么父/子关系就变得不可能了em>两种方式。请考虑以下代码:

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将{1}}挂起,则无法对其进行实例化。

(在我们的示例中,数据形成树,但此问题出现在许多其他方案中:同一问题的变体显示在readonly双向链接列表中,readonly DAG,{{ 1}}图表,等等。)

通常情况下,我使用某种黑客攻击过这个Catch-22。我过去使用的选项包括:

  • readonly移除readonly,并使用评论告诉人们不要写入该字段。这有效,但它依赖于人类的信任,而不是编辑器的锤子告诉人们不要做他们不应该做的事情。

  • Child.Parent转换为带有Child.Parent的属性。这样可行,但允许同一程序集中的其他方法可能会将internal set更改为好吧,这违反了将所有内容都设为Child.Parent的设计目标。

  • readonly放入包含Child.Parent的媒体资源中,然后添加private set方法。这有效,但它允许internal Child.SetParent()上的方法1}}可能直接更改Child,并且仍然为同一程序集中其他位置的代码提供了一个漏洞来改变Parent。虽然Child.Parent构造函数仍然是O(n),但它的常数时间表现要差得多。

  • 使用Parent()方法使Parent()构造函数克隆每个Child实例。这有效,但它违反了对您的实例的期望传入是Child.Clone()将存储的实例。如果克隆是深度克隆,它也可能使Parent构造函数的运行速度远远低于预期的O(n)时间,这违反了对Parent()的构造性能的预期。

  • Parent构造函数采用实际Parent(),而不是使用IList并创建IEnumerable副本。这但是它相信调用者在构造readonly之后不会使用列表。它还会更改Parent构造函数的预期行为,您可以在任何类型的集合中传递它,它只会工作。&#34;

  • Parent()构造函数使用反射来更新Parent()这样可行,但它是偷偷摸摸的,并且是神奇的&#34; ,它违反了Child.ParentChild.Parent构建后不会发生变化的期望,并且比直接写入字段要快得多。

简而言之,对于我能够找到的这个问题,并不是一个简单的答案。我过去在各个方面都使用了上述各种解决方案,但他们都非常厌恶我想知道是否有人有更好的答案。

因此,总结一下:您是否有一个更好的解决方案,您已经发现Catch-22只有Child跨类推荐?

(是的,我知道在某些方面,我尝试使用技术来解决人们的问题,但这并非如此:问题在于类readonly模式可以起作用,而不是它是否应该存在。)

3 个答案:

答案 0 :(得分:1)

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

他们所做的是他们有“builder classes”,这是唯一允许违反不变性规则的东西,允许这样做,因为它所做的更改在类之外是不可见的,直到你调用ToImmutable()就可以了。

您可以使用Parent.Builder执行类似操作,public void AddChild(Foo foo, Bar bar, Baz baz)可以将public Parent ToImmutalbe()添加到父级的内部状态。

添加完所有内容后,您可以在构建器上调用Parent,它会返回一个{{1}}对象,其中包含所有不可变的子项。

答案 1 :(得分:1)

正如我的评论中提到的,另一种不需要破坏父/子引用的只读的可能性就是通过传递给“Parent”对象的工厂方法来构造你的“Child”对象:

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构造函数的工厂,例如要创建一个包含5个孩子的父母,您可以使用以下内容:

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

...

var parent = new Parent(CreateChildren);

...

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

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

答案 2 :(得分:0)

我认为这比语言更具设计性问题。

Child班真的需要了解Parent吗? 如果是这样,那么它需要什么样的信息或功能。是否可以移至Parent
如果您遵循“不要问”规则/原则,那么在Parent课程中,您可以调用Child方法并将Parent的信息作为参数传递。

如果你仍然留在当前的方法,那么你的ParentChild类具有“循环”依赖性,可以通过引入将它们两者结合起来的新类来解决。

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());
    }
}