使用循环引用设计的不可变类的批判,以及更好的选项

Dan*_*iel 6 f# closures class-design circular-dependency immutability

我有一个工厂类,用于创建带有循环引用的对象.我希望它们也是不变的(在某种意义上说).所以我使用以下技术,使用一种类型的闭包:

[<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
Run Code Online (Sandbox Code Playgroud)

我比一种internal AddChild方法更喜欢这个,因为它有更强的不变性保证.也许它是神经质的,但我更喜欢关闭访问控制.

这个设计有什么陷阱吗?有没有更好的,也许不那么繁琐的方法来做到这一点?

Tom*_*cek 9

即使在创建抽象类的实例时,您也可以使用F#的支持进行递归初始化:

let makeParent() =
  let rec children = seq [ Child(parent); Child(parent); Child(parent) ]
  and parent = 
    { new Parent() with
      member __.Children = children }
  parent
Run Code Online (Sandbox Code Playgroud)

编译代码时,F#使用延迟值,因此该值children变为惰性值,属性Children访问此延迟计算的值.这很好,因为它可以首先创建Parent(引用惰性值)的实例,然后实际构造序列.

使用记录执行相同的操作不会很好,因为没有计算会被延迟,但它在这里工作得很好,因为在创建时实际上没有访问序列Parent(如果它是记录,这将是一个必须评估的字段).

F#编译器无法判断(通常)这是否正确,因此它会发出可以使用禁用的警告#nowarn "40".

一般来说,我认为使用let rec .. and ..初始化递归值是一件好事 - 它有点受限(其中一个引用必须被延迟),但它会强制你保持递归引用被隔离,我认为它保留了你的代码简单.

编辑要添加一个示例,当这可能出错 - 如果构造函数Child尝试访问Children其父集合,那么它会强制评估延迟值,然后才能创建它并且您得到运行时错误(这是警告所说的).尝试将此添加到以下构造函数Child:

do printfn "%d" (Seq.length parent.Children)
Run Code Online (Sandbox Code Playgroud)