我应该 null 保护我的 F# 代码免受 C# 调用吗

Should I null-protect my F# code from C# calls

我正在用 F# 编写一个库,其中一些接口和基础 类 public 仅可见。通常,我避免在我的自定义类型上指定 [<AllowNullLiteral>],因为这会使我的 F# 代码中的验证逻辑复杂化(请参阅 this nice post for goods and bads of null handing in F# to get a picture ),而且 F# 最初不允许 F# 类型使用 null。因此,我仅对接受 null 值作为有效值的类型验证空值。

但是,当从其他 .NET 语言(例如 C#)使用我的库时,会出现问题。更具体地说,我担心在 C# 代码调用时我应该如何实现接受 F# 声明的接口的方法。接口类型在 C# 中可以为空,我怀疑 C# 代码将 null 传递给我的 F# 方法不会有问题。

我担心调用者会因 NPE 而崩溃和燃烧,问题是我什至不允许在 F# 代码中正确处理它——比如抛出一个 ArgumentNullException——因为各自的接口缺少 AllowNullLiteral 属性。我担心我必须使用该属性并在我的 F# 代码中添加相关的空值检查逻辑才能最终防止此类灾难。

我的担心合理吗?我有点困惑,因为我最初尝试坚持良好的 F# 实践并尽可能避免 null。如果我的目标之一是允许 C# 代码子类化并实现我在 F# 中创建的接口,这会发生什么变化?如果来自我的 F# 代码的所有非值类型是 public 并且可以从任何 CLR 语言访问,我是否必须允许它们为空值?是否有最佳实践或好的建议可以遵循?

您可以采用两种基本方法:

  1. 您的 API 设计中的文档不允许将 null 传递给您的库,并且调用代码负责确保您的库永远不会收到 null。然后忽略这个问题,当你的代码抛出 NullReferenceExceptions 并且用户抱怨它时,将他们指向文档。

  2. 假设您的图书馆从 "outside" 收到的输入不可信,并在图书馆的 "outside-facing" 边缘放置一个验证层。该验证层将负责检查 null 并抛出 ArgumentNullException。 (并指向异常消息中显示 "No nulls allowed" 的文档)。

正如您可能猜到的那样,我赞成方法 #2,尽管它需要更多时间。但是您通常可以创建一个随处使用的函数来为您完成此操作:

let nullArg name message =
    raise new System.ArgumentNullException(name, message)

let guardAgainstNull value name =
    if isNull value then nullArg name "Nulls not allowed in Foo library functions"

let libraryFunc a b c =
    guardAgainstNull a nameof(a)
    guardAgainstNull b nameof(b)
    guardAgainstNull c nameof(c)
    // Do your function's work here

或者,如果您有一个更复杂的数据结构,您必须检查内部空值,然后将其视为 HTML 表单中的验证问题。您的验证函数将抛出异常,否则它们将 return 有效的数据结构。所以你的库的其余部分可以完全忽略空值,并以一种漂亮、简单、惯用的 F# 方式编写。并且您的验证函数可以处理您的域函数和不受信任的 "outside world" 之间的接口,就像您处理 HTML 表单中的用户输入一样。

更新: 另见 https://fsharpforfunandprofit.com/posts/the-option-type/ 底部附近给出的建议(在 "F# and null" 部分),Scott Wlaschin 写道,"As a general rule, nulls are never created in "纯“F#,但仅通过与 .NET 库或其他外部系统交互。[...] 在这些情况下,立即检查空值并将其转换为选项类型是一种很好的做法!”您的库代码希望从其他 .NET 库获取数据,情况类似。如果您想允许空值,您可以将它们转换为 Option 类型的 None 值。如果你想禁止它们并在传递 null 时抛出 ArgumentNullExceptions,你也可以在你的库的边界处这样做。

根据@rmunn 的建议,我最终创建了一个简单的 null2option 函数:

let null2option arg = if obj.ReferenceEquals(arg, null) then None else Some arg

它独自解决了我的大部分案件。如果我希望调用代码出现空参数,我会简单地使用这个习惯用法:

match null2option arg with | None -> nullArg "arg" "Message" | _ -> ()