在 F# 中测试空引用

15

给定以下内容:

[<DataContract>]
type TweetUser = {
    [<field:DataMember(Name="followers_count")>] Followers:int
    [<field:DataMember(Name="screen_name")>] Name:string
    [<field:DataMember(Name="id_str")>] Id:int
    [<field:DataMember(Name="location")>] Location:string}

[<DataContract>]
type Tweet = {
    [<field:DataMember(Name="id_str")>] Id:string
    [<field:DataMember(Name="text")>] Text:string
    [<field:DataMember(Name="retweeted")>] IsRetweeted:bool
    [<field:DataMember(Name="created_at")>] DateStr:string
    [<field:DataMember(Name="user", IsRequired=false)>] User:TweetUser
    [<field:DataMember(Name="sender", IsRequired=false)>] Sender:TweetUser
    [<field:DataMember(Name="source")>] Source:string}

使用 DataContractJsonSerializer(typeof<Tweet[]>) 进行反序列化将导致 User 或 Sender 字段为空(至少调试器是这样告诉我的)。

如果我尝试编写以下内容:

    let name = if tweet.User <> null 
                  then tweet.User.Name
                  else tweet.Sender.Name
编译器报错:"类型 'TweetUser' 没有将 'null' 作为适当值"。
在这种情况下,我该如何测试空值?

1
if tweet.User <> Unchecked.defaultof<_> 能够工作吗?如果不能,那么总有AllowNullLiteral attribute - ildjarn
Unchecked.defaultof<_> 在编译时可以通过,但在运行时无法正常工作(无法正确匹配 null)。AllowNullLiteral 对于记录字段无效。不过这是一个好的建议。 - Mike Ward
3个回答

20
为了更好地解释 @Tomas 的回答,循环扩展一下;-]
let name = if not <| obj.ReferenceEquals (tweet.User, null)
              then tweet.User.Name
              else tweet.Sender.Name

或者

let inline isNull (x:^T when ^T : not struct) = obj.ReferenceEquals (x, null)

Unchecked.defaultof<_> 正确地为您的记录类型生成 null 值;问题在于默认的相等运算符使用了通用结构比较,该比较期望您在使用 F# 类型时始终遵循 F# 的规则。无论如何,空检查只需要首先进行引用比较。


一旦你理解了这个概念,它就会有意义。谢谢! - Mike Ward
1
在这里使用Unchecked.defaultof<_>是否比仅使用null有优势?似乎后者可以避免函数调用,并允许它返回结构的正确结果。 - Dax Fohl
@DaxFohl:默认情况下,不允许将 F# 类型的实例化为 null(请参见 AllowNullLiteralAttribute)。此处使用的约束条件特别禁止值类型,因为它们在语义上没有意义。最后,Unchecked.defaultof 是一个编译器内置函数(类似于 C# 中的 default),因此在那里没有函数调用。 - ildjarn
@DaxFohl:迟来的是,我的对 Unchecked.defaultof<_> 的调用是误导性的,因为它总是转换为 Unchecked.defaultof<obj>,所以在这种情况下使用 null 是合适的。 - ildjarn

15

补充@ildjarn的评论,你收到错误消息是因为F#不允许使用null作为在F#中声明的类型的值。这样做的原因是F#试图从纯F#程序中消除null值(和NullReferenceException)。

但是,如果你使用的是在F#中未定义的类型,则仍然可以使用null(例如,在调用以System.Random为参数的函数时,可以将其设为null)。这是必要的互操作性,因为你可能需要将null传递给.NET库或将其接受为结果。

在你的示例中,TweetUser是在F#中声明的(记录)类型,所以该语言不允许将null视为TweetUser类型的值。但是,你仍然可以通过反射或来自C#代码获取null值,因此F#提供了一个“不安全”的函数,可以创建任何类型的null值-包括通常不应该具有null值的F#记录。这个函数是Unchecked.defaultOf<_>,你可以用它来实现一个辅助函数,就像这样:

let inline isNull x = x = Unchecked.defaultof<_>

或者,如果你使用AllowNullLiteral属性标记一个类型,那么你告诉F#编译器它应该允许null作为该特定类型的值,即使它是在F#声明的类型(通常不允许null)。


记录字段不允许使用AllowNullLiteral。当与Unchecked.defaultof<>进行比较时,程序故障访问tweet.User。换句话说,当tweet.User为null时仅引用它进行比较就足以导致故障。奇怪。 - Mike Ward
1
@MikeWard 这个属性需要应用于类型 - 在你的情况下是 TweetUser - 然后它指定该类型的任何出现(不仅在字段中,还包括在 if 表达式中)都可以具有 null 值。 - Tomas Petricek
1
我也尝试过了。AllowNullLiteral不能应用于记录类型。reference.equals的东西按预期工作。这只是我们必须接受的F#互操作性怪异之一。总体上真的很喜欢这种语言。 - Mike Ward
3
提议的 isNull 函数似乎无法正常工作。如果调用 Unchecked.defaultof<Foo> |> isNull,结果会出现 NullReferenceException 异常。 - Mark Seemann

3
虽然这个问题比较老,但我没有看到任何关于使用装箱解决问题的示例。在我的展示器不允许空文本的情况下,但可以从视图中设置时,我更喜欢使用装箱技术。
isNull <| box obj

或者

let isMyObjNull = isNull <| box obj

或者

match box obj with
| null -> (* obj is null *)
| _ -> (* obj is not null *)

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