可区分联合的结构属性

Eri*_*icP 7 f# struct discriminated-union

我刚刚意识到 F# 记录是引用类型以及我进行了多少装箱和拆箱。我有很多这样的小记录:

type InputParam =
    | RegionString of string
    | RegionFloat of float32
Run Code Online (Sandbox Code Playgroud)

但是如果我尝试用“Struct”属性来标记它,我会收到一个编译器错误,指出“FS3204 如果联合类型有多个案例并且是一个结构,那么联合类型中的所有字段都必须被赋予唯一的名称。” 该语言参考显示了创建结构歧视这样的工会:

[<Struct>]
type InputParamStruct =
    | RegionString of RegionString: string
    | RegionFloat of RegionFloat: float32
Run Code Online (Sandbox Code Playgroud)

x of string 和 x of x: string 有什么区别?这些字段一开始就不是唯一的?为什么 F# 不默认为记录结构?

Phi*_*ter 11

首先,这些不是记录——它们是受歧视的工会。Record 是具有生成的相等性/散列的命名数据的简单聚合,也可以将其设为结构体,但没有附加要求。

对 struct Discriminated Unions 的更严格要求是:

  • 没有可调用的默认构造函数
  • 没有循环引用/没有递归定义
  • 多大小写必须有唯一的名称

前两点是值类型所固有的。值和引用类型只是不同。

最后一点很有趣。考虑以下:

type DU1 =
    | Case1 of string
    | Case2 of float

[<Struct>]
type DU2 =
    | Case1 of sval: string
    | Case2 of fval: float
Run Code Online (Sandbox Code Playgroud)

在 的情况下DU1,每种情况都有一个内部类,这些内部类包含用于访问基础数据的属性。这些属性被命名为Item1、 等Item2,并且由于它们封装在内部类中,因此在访问时是唯一的。

在 的情况下DU2svalfval值被平放;没有包含它们的内部类。这是因为目标是结构的性能/大小。联合大小写 ( Item1/ Item2/etc. ) 中的数据命名策略不适用,因为所有数据都是平面布局的。所以设计决定是要求唯一命名的案例,而不是应用一些技巧来将案例本身的名称和Item1/ Item2/ 等的一些变体混在一起。唯一性问题是编译器中联合本身的设计所固有的,而不仅仅是代码生成设计选择。

最后,这个问题还有一个有趣的答案:

为什么 F# 不默认为记录结构?

F# 中的元组、记录和 DU 都可以标记为,[<Struct>]但默认情况下不是结构。这是因为结构不仅仅是您可以按下的“使其更高效”的按钮。很多时候,由于结构太大,过度复制会导致 CPU 性能变差。在 F# 中,有大元组和非常非常大的记录和有区别的联合是很正常的。默认情况下制作这些结构不是一个好的选择。引用类型非常强大,设计用于在 .NET 上运行良好,默认情况下不应避免,因为在某些情况下结构可能会导致性能稍快。

每当您关心性能时,永远不要仅仅根据假设或直觉来改变事情:使用性能分析工具,如 PerfView、dotTrace 或 dotMemory;并使用 BenchmarkDotNet 等统计工具对微小变化进行基准测试。性能是一个极其复杂的空间,一旦你解决了明显糟糕的严重问题(比如大型数据集上的 O(n^2) 算法或其他东西),就很少是简单的事情了。


Eri*_*icP 3

毫无疑问,这应该是一个结构体。它是不可变的并且 16 字节。看看反汇编,这个引用类型:

type InputParam =
    | RegionString of string
    | RegionFloat of float32
Run Code Online (Sandbox Code Playgroud)

这个参考类型:

type InputParam =
    | RegionString of RegionString: string
    | RegionFloat of RegionFloat: float32
Run Code Online (Sandbox Code Playgroud)

功能相同。唯一的区别在于编译器如何命名事物。它们都创建了一个名为“RegionString”的子类,但具有不同的属性名称——“RegionString.item”与“RegionString.RegionString”。

当您将第一个示例转换为结构时,它会删除子类并尝试在记录上粘贴 2 个“项目”属性,这会导致 FS3204唯一名称错误。

就性能而言,您应该在编写时在每个像这样的小型类型上使用结构。考虑这个示例脚本:

type Name = Name of string
let ReverseName (Name s) =
    s.ToCharArray() |> Array.rev |> System.String |> Name

[<Struct>]
type StrName = StrName of string
let StrReverseName (StrName s) =
    s.ToCharArray() |> Array.rev |> System.String |> StrName

#time
Array.init 10000000 (fun x -> Name (x.ToString()))
|> Array.map ReverseName
|> ignore
#time

#time
Array.init 10000000 (fun x -> StrName (x.ToString()))
|> Array.map StrReverseName
|> ignore
#time

sizeof<Name>
sizeof<StrName>
Run Code Online (Sandbox Code Playgroud)

第一个将 ref 类型包装在 ref 类型中,这使性能提高了一倍:

Real: 00:00:04.637, CPU: 00:00:04.703, GC gen0: 340, gen1: 104, gen2: 7
...
Real: 00:00:02.620, CPU: 00:00:02.625, GC gen0: 257, gen1: 73, gen2: 1
...
val it : int = 8
val it : int = 8
Run Code Online (Sandbox Code Playgroud)

功能域建模很棒,但您必须记住它们具有相同的性能开销:

let c = CustomerID 5
let i = 5 :> obj
Run Code Online (Sandbox Code Playgroud)

建议任何小于 16 字节的不可变内容都应该是 struct。如果超过 16 字节,则必须查看其行为。如果它被多次传递,您最好传递 64 位引用指针并承受引用开销。但对于组合类型或函数内的内部数据,请坚持使用结构。

  • 我认为这是一个很好的实践,我认为可以记录在此处的 F# 编码约定中:https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions (2认同)