我们有一组专门使用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#代码。它会编译。而且这是建立关系的正确方法。
但是无法正确地实例化Parent
或Child
:Parent
无法实例化,直到您有完整的子集合附加到它,但每个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.Parent
在Child.Parent
构建后不会发生变化的期望,并且比直接写入字段要快得多。
简而言之,对于我能够找到的这个问题,并不是一个简单的答案。我过去在各个方面都使用了上述各种解决方案,但他们都非常厌恶我想知道是否有人有更好的答案。
因此,总结一下:您是否有一个更好的解决方案,您已经发现Catch-22只有Child
跨类推荐?
(是的,我知道在某些方面,我尝试使用技术来解决人们的问题,但这并非如此:问题在于类readonly
模式可以起作用,而不是它是否应该存在。)
答案 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
的信息作为参数传递。
如果你仍然留在当前的方法,那么你的Parent
和Child
类具有“循环”依赖性,可以通过引入将它们两者结合起来的新类来解决。
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());
}
}