我最近发现null-coalescing运算符在使用Json.NET将JSON解析为动态对象时出现问题。假设这是我的动态对象:
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上对这些问题进行了广泛的测试。
答案 0 :(得分:20)
这是由于Json.NET和??
运算符的行为模糊不清。
首先,当您将JSON反序列化为dynamic
对象时,实际返回的是Linq-to-JSON类型JToken
的子类(例如JObject
或{{3} })具有JValue
的自定义实现。即。
dynamic d1 = JsonConvert.DeserializeObject(json);
var d2 = JsonConvert.DeserializeObject<JObject>(json);
实际上是回归同样的事情。所以,对于你的JSON字符串,如果我这样做
var s1 = JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"];
var s2 = JsonConvert.DeserializeObject<dynamic>(json).phones.personal;
这两个表达式都评估完全相同的返回动态对象。但是返回了什么对象?这让我们看到Json.NET的第二个模糊行为:它不是用null
指针表示空值,而是用IDynamicMetaObjectProvider
代表JValue
等于JValue.Type
的特殊JTokenType.Null
表示。因此,如果我这样做:
WriteTypeAndValue(s1, "s1");
WriteTypeAndValue(s2, "s2");
控制台输出是:
"s1": Newtonsoft.Json.Linq.JValue: ""
"s2": Newtonsoft.Json.Linq.JValue: ""
即。这些对象 not null ,它们被分配了POCO,它们的ToString()
返回一个空字符串。
但是,当我们将该动态类型分配给字符串时会发生什么?
string tmp;
WriteTypeAndValue(tmp = s2, "tmp = s2");
打印:
"tmp = s2": System.String: null value
为何与众不同?这是因为DynamicMetaObject
返回的JValue
解析动态类型到字符串的转换最终会调用ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type)
,最终会返回null
的{{1}}值,这与显式转换为字符串执行的逻辑相同,避免使用JTokenType.Null
:
dynamic
现在,到实际的问题。正如husterk注意到?? operator当两个操作数之一为 WriteTypeAndValue((string)JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"], "Linq-to-JSON with cast");
// Prints "Linq-to-JSON with cast": System.String: null value
WriteTypeAndValue(JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"], "Linq-to-JSON without cast");
// Prints "Linq-to-JSON without cast": Newtonsoft.Json.Linq.JValue: ""
时返回dynamic
,因此dynamic
不会尝试执行类型转换,因此返回是d.phones.personal ?? "default"
:
JValue
但是如果我们通过将动态返回分配给字符串来调用Json.NET的类型转换为字符串,那么转换器将在合并运算符完成其工作后启动并返回实际的空指针并返回非空 dynamic d = JsonConvert.DeserializeObject<dynamic>(json);
WriteTypeAndValue((d.phones.personal ?? "default"), "d.phones.personal ?? \"default\"");
// Prints "(d.phones.personal ?? "default")": Newtonsoft.Json.Linq.JValue: ""
:
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");
(另外,null类型public static void WriteTypeAndValue<T>(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));
}
的存在解释了表达式JValue
可能如何评估为(object)(JValue)(string)null == (object)(JValue)null
)。
答案 1 :(得分:2)
我认为我找出了原因...看起来好像空合并操作符将动态属性转换为与语句的输出类型匹配的类型(在您的情况下,它执行 < em> ToString 对 d.phones.personal 的值进行操作。 ToString 操作将“null”JSON值转换为空字符串(而不是实际的空值)。因此,null合并运算符将有问题的值视为空字符串而不是null,这会导致测试失败并且不返回“default”值。
此外,当您使用调试器检查动态对象时,您可以看到它将 d.phones.personal 的值显示为“Empty”而不是null(见下图。)
此问题的可能解决方法是在执行空合并操作之前安全地转换对象,如下面的示例所示。这将阻止空合并运算符执行隐式转换。
string s = (d.phones.personal as string) ?? "default";