Null-coalescing运算符为动态对象的属性返回null

我最近在使用Json.NET将JSON解析为动态对象时发现了null-coalescing运算符的问题。 假设这是我的动态对象:

string json = "{ \"phones\": { \"personal\": null }, \"birthday\": null }"; dynamic d = JsonConvert.DeserializeObject(json); 

如果我尝试使用?? 运算符在d的一个字段上,它返回null:

 string s = ""; s += (d.phones.personal ?? "default"); Console.WriteLine(s + " " + s.Length); //outputs 0 

但是,如果我将动态属性分配给字符串,那么它可以正常工作:

 string ss = d.phones.personal; string s = ""; s += (ss ?? "default"); Console.WriteLine(s + " " + s.Length); //outputs default 7 

最后,当我输出Console.WriteLine(d.phones.personal == null)它输出True

我已经在Pastebin上对这些问题进行了广泛的测试。

这是由于Json.NET的晦涩行为和?? 运营商。

首先,当您将JSON反序列化为dynamic对象时,实际返回的是Linq-to-JSON类型JToken的子类(例如JObjectJValue ),它具有IDynamicMetaObjectProvider的自定义实现。 即

 dynamic d1 = JsonConvert.DeserializeObject(json); var d2 = JsonConvert.DeserializeObject(json); 

实际上是回归同样的事情。 所以,对于你的JSON字符串,如果我这样做

  var s1 = JsonConvert.DeserializeObject(json)["phones"]["personal"]; var s2 = JsonConvert.DeserializeObject(json).phones.personal; 

这两个表达式都评估完全相同的返回动态对象。 但是返回了什么对象? 这让我们看到了Json.NET的第二个模糊行为:它不是用null指针表示空值, JValueJValue.Type等于JTokenType.Null的特殊JValue JTokenType.Null 。 因此,如果我这样做:

  WriteTypeAndValue(s1, "s1"); WriteTypeAndValue(s2, "s2"); 

控制台输出是:

 "s1": Newtonsoft.Json.Linq.JValue: "" "s2": Newtonsoft.Json.Linq.JValue: "" 

即这些对象不为空 ,它们被分配POCO,并且它们的ToString()返回一个空字符串。

但是,当我们将该动态类型分配给字符串时会发生什么?

  string tmp; WriteTypeAndValue(tmp = s2, "tmp = s2"); 

打印:

 "tmp = s2": System.String: null value 

为什么不同? 这是因为JValue返回的DynamicMetaObject解析了动态类型到字符串的转换,最终调用了ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type) ,它最终为JTokenType.Null值返回null ,这是相同的逻辑由显式强制转换为字符串,避免dynamic所有使用:

  WriteTypeAndValue((string)JsonConvert.DeserializeObject(json)["phones"]["personal"], "Linq-to-JSON with cast"); // Prints "Linq-to-JSON with cast": System.String: null value WriteTypeAndValue(JsonConvert.DeserializeObject(json)["phones"]["personal"], "Linq-to-JSON without cast"); // Prints "Linq-to-JSON without cast": Newtonsoft.Json.Linq.JValue: "" 

现在,到实际的问题。 正如赫斯特克注意到的那样? 当两个操作数之一是dynamic , 运算符返回dynamic ,所以d.phones.personal ?? "default" d.phones.personal ?? "default"不会尝试执行类型转换,因此返回的是JValue

  dynamic d = JsonConvert.DeserializeObject(json); WriteTypeAndValue((d.phones.personal ?? "default"), "d.phones.personal ?? \"default\""); // Prints "(d.phones.personal ?? "default")": Newtonsoft.Json.Linq.JValue: "" 

但是如果我们通过将动态返回分配给字符串来调用Json.NET的类型转换为字符串,那么转换器将在合并运算符完成其工作并返回非空JValue之后启动并返回实际的空指针:

  string tmp; WriteTypeAndValue(tmp = (d.phones.personal ?? "default"), "tmp = (d.phones.personal ?? \"default\")"); // Prints "tmp = (d.phones.personal ?? "default")": System.String: null value 

这解释了您所看到的差异。

要避免此行为,请在应用合并运算符之前强制将转换从动态转换为字符串:

 s += ((string)d.phones.personal ?? "default"); 

最后,将类型和值写入控制台的helper方法:

 public static void WriteTypeAndValue(T value, string prefix = null) { prefix = string.IsNullOrEmpty(prefix) ? null : "\""+prefix+"\": "; Type type; try { type = value.GetType(); } catch (NullReferenceException) { Console.WriteLine(string.Format("{0} {1}: null value", prefix, typeof(T).FullName)); return; } Console.WriteLine(string.Format("{0} {1}: \"{2}\"", prefix, type.FullName, value)); } 

(另外,null类型JValue的存在解释了表达式(object)(JValue)(string)null == (object)(JValue)null可能如何计算为false )。

我想我找出了这个的原因…看起来好像空合并运算符将动态属性转换为与语句的输出类型匹配的类型(在您的情况下,它对d的值执行ToString操作) .phones.personal )。 ToString操作将“null”JSON值转换为空字符串(而不是实际的空值)。 因此,null合并运算符将有问题的值视为空字符串而不是null,这会导致测试失败并且不返回“default”值。

更多信息: https //social.msdn.microsoft.com/Forums/en-US/94b3ca1c-bbfa-4308-89fa-6b455add9de6/dynamic-improvements-on-c-nullcoalescing-operator?forum=vs2010ctpvbcs

此外,当您使用调试器检查动态对象时,您可以看到它将d.phones.personal的值显示为“Empty”而不是null(请参见下图)。

在此处输入图像描述

此问题的可能解决方法是在执行空合并操作之前安全地转换对象,如下面的示例所示。 这将阻止空合并运算符执行隐式转换。

 string s = (d.phones.personal as string) ?? "default";