我正在用F#编写一个库,其中一些接口和基类是公开可见的。通常,我避免在我的自定义类型上指定[<AllowNullLiteral>]
,因为这会使我的F#代码中的验证逻辑变得复杂(请参阅this nice post for goods and bads of null handing in F# to get a picture),而且F#最初不允许null
用于F#类型。因此,我仅对接受null
值有效的类型验证null。
但是,当我的库使用其他.NET语言(例如C#)时会出现问题。更具体地说,我担心在C#代码调用时,我应该如何实现接受F#-declared接口的方法。接口类型在C#中可以为空,我怀疑C#代码不会将null
传递给我的F#方法。
我担心调用者会因NPE而崩溃和烧毁,问题是我甚至不允许在F#代码中正确处理它 - 比如抛出ArgumentNullException
- 因为相应的接口缺少AllowNullLiteral
属性。我担心我必须使用该属性并在我的F#代码中添加相关的空检查逻辑,以最终防止这种灾难。
我的恐惧是否合理?我有点困惑,因为我最初试图坚持良好的F#做法并尽可能地避免null
。如果我的目标之一是允许C#代码继承并实现我在F#中创建的接口,这会如何改变?如果它们是公共的并且可以从任何CLR语言访问,我是否必须允许来自我的F#代码的所有非值类型的空值?是否有最好的做法或好的建议?
答案 0 :(得分:5)
您可以采取两种基本方法:
您的API设计中的文档不允许将null
传递到您的库,并且调用代码负责确保您的库永远不会收到null
。然后忽略该问题,当您的代码抛出NullReferenceException
并且用户抱怨它时,请将它们指向文档。
假设您的库从“外部”接收的输入不可信,并在库的“面向外”边缘周围放置一个验证层。该验证层将负责检查null
并抛出ArgumentNullException
。 (并指出在异常消息中说“不允许空值”的文档。)
正如你可能猜到的那样,我赞成方法#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表单中的验证问题。您的验证函数将抛出异常,否则它们将返回有效的数据结构。所以你的库的其余部分可以完全忽略空值,并用一个漂亮,简单,惯用的F#方式编写。您的验证功能可以处理域功能与不受信任的“外部世界”之间的接口,就像在HTML表单中使用用户输入一样。
更新:另请参阅https://fsharpforfunandprofit.com/posts/the-option-type/底部附近给出的建议(在“F#和null”部分中),其中Scott Wlaschin写道:“作为一般规则,空值为从未在“纯”F#中创建,但只能通过与.NET库或其他外部系统交互。[...]在这些情况下,最好立即检查空值并将它们转换为选项类型!“您希望从其他.NET库获取数据的库代码也会出现类似情况。如果要允许空值,则将它们转换为None
类型的Option
值。如果您想要禁用它们并在传递空值时抛出ArgumentNullException
,那么您也可以在库的边界处执行此操作。
答案 1 :(得分:2)
根据@ rmunn的建议,我最终创建了一个简单的null2option
函数:
let null2option arg = if obj.ReferenceEquals(arg, null) then None else Some arg
它解决了我的大部分案件。如果我希望为调用代码提供一个null参数,我只会使用这个习语:
match null2option arg with | None -> nullArg "arg" "Message" | _ -> ()