记录等于或GetHashCode引发NullReferenceException异常

6

我有很多这样的记录:

[<DataContract>]
type Rec1 = {
    [<DataMember>] mutable field1 : int;
    [<DataMember>] mutable field2 : string;
}

[<DataContact>]
type Rec2 = { 
    [<DataMember>] mutable field3 : Rec1;
    [<DataMember>] mutable field4 : int;
}

我使用 DataContactJsonSerializer 将JSON反序列化为以下结构。 这是一个有效的JSON值:
{ "field3": null, "field4": 1 }

这意味着在运行时,field3null/Unchecked.defaultOf<_>。在 Visual Studio 2010 中,这个测试正常工作:
(deserialize<Rec2> "{ field3: null, field4: 1 }") = { field3 = Unchecked.defaultOf<_>; field4 = 1 } //true

在Visual Studio 2013中,相同的代码会抛出一个“NullReferenceException”异常:
at Rec2.Equals(Rec2 obj) 

在查看ILSpy中的代码时,我发现它是自动生成的:

if(this != null)
{
    return obj != null && this.field3.Equals(obj.field3) && this.field4.Equals(obj.field4);
}
return obj == null;

因为DataContractJsonSerializer将值设置为null,所以编译器假设field3从不为null,这是问题的关键。我尝试将AllowNullLiteral属性应用于Rec1,但F#记录不允许应用该属性。我该如何告诉编译器字段可以为null或者重新构造我的类型使其可行?

3
当使用“Unchecked”时,这种情况有些可以预料。如果一个缺失的值是有效的,你应该考虑使用“option<Rec1>”代替。 - Daniel
3个回答

8

F#正在生成类型,假设它只会在F#中使用,其中null是不可能的。因为该元素上不允许使用[<AllowNullLiteral>],所以我认为没有办法控制代码生成,使其考虑到这种可能性。我能想到两种方法来解决这个问题:

  1. Rec2上实现自定义相等性,而不是默认的结构相等性。这意味着你必须自己编写相等性方法,这很不幸,但它应该允许你考虑到null的可能性。
  2. 将代码更改为生成struct实例,而不是class。这消除了任何CLR使用情况下null的可能性。这将需要你从记录定义移动到完整类型。

重写Equal和GetHashCode仍然会在field3中留下空值。每当您的代码访问field3时,都会收到“NullReferenceException”异常。 - Wallace Kelly

0

我在一个项目中遇到了这个问题。在我看来,这是由于EqualGetHashCode生成的代码中存在的一个错误。

对我来说,一个解决方法是在反序列化后立即替换空值。请参见我添加到您示例代码中的removeNulls函数:

[<DataContract>]
type Rec1 =
    {
        [<DataMember>] mutable field1 : int
        [<DataMember>] mutable field2 : string
    }
    static member empty = { field1 = 0; field2 = String.Empty }

[<DataContract>]
type Rec2 =
    { 
        [<DataMember>] mutable field3 : Rec1;
        [<DataMember>] mutable field4 : int;
    }
    static member removeNulls (rec2 : Rec2) =
        { rec2 with
            field3 = if Object.ReferenceEquals(rec2.field3, null)
                         then Rec1.empty
                         else rec2.field3
        }

      :
      :         

let rec2 = deserialize<Rec2> json
let rec2' = Rec2.removeNulls rec2

这将解决您在使用Rec2实例时的问题,而不仅仅是当框架调用EqualGetHashCode时。否则,您还需要在代码中访问它时检查field3不为空。


-1
问题似乎在于您认为nullRec1类型的有效值,但实际上它不是。该值在此处无效,因此您正在向F#代码提供垃圾数据。
如果您将null输入到任何不期望它的代码中,您将获得类似的行为。例如:
2::1::Unchecked.defaultof<_>

打印出[2],这只是无意义的废话。垃圾进,垃圾出...


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