对具有循环引用设计的不可变类的批评以及更好的选择

发布于 2024-11-29 12:58:21 字数 623 浏览 2 评论 0原文

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

[<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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

九命猫 2024-12-06 12:58:21

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

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

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

对记录做同样的事情不会很好地工作,因为任何计算都不会被延迟,但它在这里工作得很好,因为在创建 Parent 时实际上并没有访问序列(如果它是一条记录,这将是一个必须评估的字段)。

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

总的来说,我认为使用 let rec .. and .. 来初始化递归值是一件好事 - 它有点受限(其中一个引用必须延迟),但它迫使你保持递归引用隔离,我认为这可以使您的代码更简单。

编辑要添加一个可能出错的示例 - 如果Child的构造函数尝试访问其父级的Children集合,那么它会强制在创建惰性值之前对其进行评估,并且您会收到运行时错误(这就是警告所说的内容)。尝试将其添加到 Child 的构造函数中:

do printfn "%d" (Seq.length parent.Children)

You can use F#'s support for recursive initialization even when creating an instance of abstract class:

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

When compiling the code, F# uses lazy values, so the value children becomes a lazy value and the property Children accesses the value of this lazy computation. This is fine, because it can first create instance of Parent (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 the Children 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 of Child:

do printfn "%d" (Seq.length parent.Children)
ま昔日黯然 2024-12-06 12:58:21

我认为托马斯的答案是正确的选择。但是,为了完整起见,我将展示如何使用递归记录来创建循环不可变对象。这可能会变得非常难看,因此我将不可变记录实现隐藏在一些更好的属性后面:

type Parent = internal { children : Children option }
and internal Children = { first : Child; rest : Children option }
and Child = internal { parent : Parent }

let rec internal listToChildren = function
| [] -> None
| c::cs -> Some { first = c; rest = listToChildren cs }

let rec internal childrenToList = function
| None -> []
| Some { first = c; rest = cs } -> c::(childrenToList cs)

module Factory =
    let makeParent() = 
        let rec parent = { children = children }
        and child1 = { parent = parent }
        and child2 = { parent = parent }
        and child3 = { parent = parent }
        and children = [child1; child2; child3] |> listToChildren
        parent

type Parent with
    member p.Children = childrenToList p.children
type Child with
    member c.Parent = c.parent

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:

type Parent = internal { children : Children option }
and internal Children = { first : Child; rest : Children option }
and Child = internal { parent : Parent }

let rec internal listToChildren = function
| [] -> None
| c::cs -> Some { first = c; rest = listToChildren cs }

let rec internal childrenToList = function
| None -> []
| Some { first = c; rest = cs } -> c::(childrenToList cs)

module Factory =
    let makeParent() = 
        let rec parent = { children = children }
        and child1 = { parent = parent }
        and child2 = { parent = parent }
        and child3 = { parent = parent }
        and children = [child1; child2; child3] |> listToChildren
        parent

type Parent with
    member p.Children = childrenToList p.children
type Child with
    member c.Parent = c.parent
倦话 2024-12-06 12:58:21

我想这样的事情也可以做:

type ParentFactory private (n) as X = 
    inherit Parent()
    let childs = [for i=1 to n do yield Child(X :> Parent)]
    override X.Children = childs |> List.toSeq;               
    static member Create n = (new ParentFactory(n)) :> Parent

I guess something like this can also be done:

type ParentFactory private (n) as X = 
    inherit Parent()
    let childs = [for i=1 to n do yield Child(X :> Parent)]
    override X.Children = childs |> List.toSeq;               
    static member Create n = (new ParentFactory(n)) :> Parent
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文