在F#中处理空值

42

我需要与一些C#代码进行互操作,并使用F#。空值是可能给出的值,因此我需要检查该值是否为空。文档建议使用以下模式匹配:

match value with
| null -> ...
| _ -> ...

我遇到的问题是原始代码以C#结构化:

if ( value != null ) {
    ...
}

在 F# 中要如何实现相同的功能?是否有用于模式匹配的无操作(no-op)?是否有一种方法可以使用 if 语句检查 null 值?

6个回答

63

由于某些原因(我还没有调查清楚),not (obj.ReferenceEquals(value, null)) 的性能比 value <> null 好得多。我编写了很多用于C#的F#代码,因此我保留了一个"interop"模块来简化处理null。另外,如果您希望在匹配模式时首先使用“正常”情况,可以使用活动模式:

let (|NotNull|_|) value = 
  if obj.ReferenceEquals(value, null) then None 
  else Some()

match value with
| NotNull ->
  //do something with value
| _ -> nullArg "value"

如果你想要一个简单的if语句,这个也可以工作:

let inline notNull value = not (obj.ReferenceEquals(value, null))

if notNull value then
  //do something with value

更新

以下是一些基准测试和关于性能差异的额外信息:

let inline isNull value = (value = null)
let inline isNullFast value = obj.ReferenceEquals(value, null)
let items = List.init 10000000 (fun _ -> null:obj)
let test f = items |> Seq.forall f |> printfn "%b"

#time "on"
test isNull     //Real: 00:00:01.512, CPU: 00:00:01.513, GC gen0: 0, gen1: 0, gen2: 0
test isNullFast //Real: 00:00:00.195, CPU: 00:00:00.202, GC gen0: 0, gen1: 0, gen2: 0

速度提升了775%——还不错。通过查看.NET Reflector中的代码,发现ReferenceEquals是一个本地/非托管函数。=运算符调用HashCompare.GenericEqualityIntrinsic<'T>,最终到达内部函数GenericEqualityObj。在Reflector中,这个美丽的函数反编译成 122 行 C# 代码。显然,等式是一个复杂的问题。对于null检查,简单的引用比较已经足够,因此可以避免更微妙的相等语义产生的开销。

更新2

模式匹配也避免了等号运算符的开销。以下函数的性能与ReferenceEquals类似,但仅适用于在F#之外定义或装饰了[<AllowNullLiteral>]的类型。

let inline isNullMatch value = match value with null -> true | _ -> false

test isNullMatch //Real: 00:00:00.205, CPU: 00:00:00.202, GC gen0: 0, gen1: 0, gen2: 0

更新3

正如Maslow的评论中所指出的那样,F# 4.0版本中增加了一个isNull运算符。它的定义与上面的isNullMatch相同,因此能够实现最佳性能。


4
基于F# 4中的新isNull,这个答案需要做哪些修改? Operators.isNull : value:'T -> bool when 'T : null http://blogs.msdn.com/b/dotnet/archive/2015/04/29/rounding-out-visual-f-4-0-in-vs-2015-rc.aspx - Maslow
@Maslow obj.ReferenceEquals接受“obj”参数,因此您无需首先向上转换它。 - symbiont

42

如果您不想在空情况下执行任何操作,那么可以使用单元值()

match value with
| null -> ()
| _ -> // your code here

当然,你也可以像在C#中一样执行空值检查,这在这种情况下可能更清晰:

if value <> null then
    // your code here

这只是我误解了运算符。最终找到了这个链接:http://msdn.microsoft.com/en-us/library/dd233228.aspx - Jonathan Sternberg

16

如果你有一个在C#或.NET库中声明过的类型(不是在F#中声明的),那么null就是这种类型的合适值,你可以像kvb所示那样轻松地将该值与null进行比较。例如,假设C#调用者向你提供了Random的实例:

let foo (arg:System.Random) =
  if arg <> null then 
    // do something

如果C#调用方给你一个在F#中声明的类型,事情会变得更加棘手。在F#中声明的类型没有null值,F#编译器也不允许将它们赋值为null或检查它们是否为null。问题在于C#不进行这种检查,因此C#调用方仍然可能会给你null。例如:

type MyType(n:int) =
  member x.Number = n

在这种情况下,你需要使用装箱(boxing)或者 Unchecked.defaultOf<_>

let foo (m:MyType) =
  if (box m) <> null then
    // do something

let foo (m:MyType) =
  if m <> Unchecked.defaultOf<_> then
    // do something

0

当然,在 F# 中通常不鼓励使用 null 值,但是...

不去深入举例子等,这里有一篇文章提供了一些 @ MSDN 例子,它具体揭示了如何检测 null 值 -- 然后你可以将其从输入中解析出来或者按照需要进行处理。


3
问题在于现实生活中常常会给你一些复杂的 C# 类链,因此你不得不做一些丑陋的事情,比如 "if (vc <> null && vc.NavigationController <> null && vc.NavigationController.NavigationBar <> null)"。单独一个 null 并不令人痛苦。 - James Moore

0

我最近遇到了一个类似的困境。我的问题是,我公开了一个使用F#开发的API,可以从C#代码中使用。C#在将接口或类传递给方法时不会出现问题,但如果方法及其参数的类型是F#类型,则无法正确执行空检查。

原因是F#类型最初不接受null字面量,尽管CLR中没有任何东西保护您的API免受被不正确调用的影响,特别是来自更具有空值容忍性的语言(如C#)。除非使用相当丑陋的[<AllowNullLiteral>]属性,否则您最初将无法正确地进行空值保护。

所以,我找到了相关主题中的这个解决方案,它允许我保持我的F#代码整洁和友好。通常我会创建一个函数,接受任何对象并将其转换为Option,然后我会对None进行验证,而不是null。这类似于在F#中预定义的Option.ofObj函数,但后者需要显式可空的对象(因此注释了AllowNullLiteral)。


0

我找到了一个简单的方法来做这件事

open System

Object.ReferenceEquals(value, null)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接