对具有循环引用设计的不可变类的批评以及更好的选择
我有一个工厂类,它创建带有循环引用的对象。我也希望它们是不可变的(在某种意义上)。因此,我使用以下技术,使用某种闭包:
[<AbstractClass>]
type Parent() =
abstract Children : seq<Child>
and Child(parent) =
member __.Parent = parent
module Factory =
let makeParent() =
let children = ResizeArray()
let parent =
{ new Parent() with
member __.Children = Seq.readonly children }
[Child(parent); Child(parent); Child(parent)] |> children.AddRange
parent
我比 internal
AddChild
方法更喜欢这种方法,因为它可以更好地保证不变性。也许这有点神经质,但我更喜欢使用闭包进行访问控制。
这个设计有什么陷阱吗?有没有更好的、也许不那么麻烦的方法来做到这一点?
I have a factory class that creates objects with circular references. I'd like them to be immutable (in some sense of the word) too. So I use the following technique, using a closure of sorts:
[<AbstractClass>]
type Parent() =
abstract Children : seq<Child>
and Child(parent) =
member __.Parent = parent
module Factory =
let makeParent() =
let children = ResizeArray()
let parent =
{ new Parent() with
member __.Children = Seq.readonly children }
[Child(parent); Child(parent); Child(parent)] |> children.AddRange
parent
I like this better than an internal
AddChild
method because there's a stronger guarantee of immutability. Perhaps it's neurotic, but I prefer closures for access control.
Are there any pitfalls to this design? Are there better, perhaps less cumbersome, ways to do this?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
即使在创建抽象类的实例时,您也可以使用 F# 对递归初始化的支持:
编译代码时,F# 使用惰性值,因此值
children
变为惰性值,并且属性Children< /code> 访问此惰性计算的值。这很好,因为它可以首先创建
Parent
实例(引用惰性值),然后实际构造序列。对记录做同样的事情不会很好地工作,因为任何计算都不会被延迟,但它在这里工作得很好,因为在创建
Parent
时实际上并没有访问序列(如果它是一条记录,这将是一个必须评估的字段)。F# 编译器(通常)无法判断这是否正确,因此它会发出警告,可以使用
#nowarn "40"
禁用该警告。总的来说,我认为使用
let rec .. and ..
来初始化递归值是一件好事 - 它有点受限(其中一个引用必须延迟),但它迫使你保持递归引用隔离,我认为这可以使您的代码更简单。编辑要添加一个可能出错的示例 - 如果
Child
的构造函数尝试访问其父级的Children
集合,那么它会强制在创建惰性值之前对其进行评估,并且您会收到运行时错误(这就是警告所说的内容)。尝试将其添加到Child
的构造函数中:You can use F#'s support for recursive initialization even when creating an instance of abstract class:
When compiling the code, F# uses lazy values, so the value
children
becomes a lazy value and the propertyChildren
accesses the value of this lazy computation. This is fine, because it can first create instance ofParent
(referencing the lazy value) and then actually construct the sequence.Doing the same thing with records wouldn't work as nicely, because none of the computations would be delayed, but it works quite nicely here, because the sequence is not actually accessed when creating the
Parent
(if it was a record, this would be a field that would have to be evaluated).The F# compiler cannot tell (in general) whether this is correct, so it emits a warning that can be disabled using
#nowarn "40"
.In general, I think that using
let rec .. and ..
to initialize recursive values is a good thing - it is a bit limited (one of the references must be delayed), but it forces you to keep the recursive references isolated and, I think, it keeps your code simpler.EDIT To add an example when this may go wrong - if the constructor of
Child
tries to access theChildren
collection of its parent, then it forces evaluation of the lazy value before it can be created and you get a runtime error (which is what the warning says). Try adding this to the constructor ofChild
:我认为托马斯的答案是正确的选择。但是,为了完整起见,我将展示如何使用递归记录来创建循环不可变对象。这可能会变得非常难看,因此我将不可变记录实现隐藏在一些更好的属性后面:
I think that Tomas's answer is the way to go. However, for completeness I'll show how you could use recursive records to create cyclic immutable objects. This can potentially get quite ugly, so I've hidden the immutable record implementation behind some nicer properties:
我想这样的事情也可以做:
I guess something like this can also be done: