Null 合并运算符为动态对象的属性返回 null
Null-coalescing operator returning null for properties of dynamic objects
我最近在使用 Json.NET 将 JSON 解析为动态对象时发现空合并运算符存在问题。假设这是我的动态对象:
string json = "{ \"phones\": { \"personal\": null }, \"birthday\": null }";
dynamic d = JsonConvert.DeserializeObject(json);
如果我尝试使用 ?? d 字段之一的运算符,它 returns 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.
上对这些问题进行了广泛的测试
我想我找到了原因...看起来好像 null 合并运算符将动态 属性 转换为与语句的输出类型匹配的类型(在您的情况下它对 d.phones.personal[= 的值执行 ToString 操作33=]). ToString 操作将 "null" JSON 值转换为空字符串(而不是实际的空值)。因此,null 合并运算符将所讨论的值视为空字符串而不是 null,这会导致测试失败并且不会返回 "default" 值。
此外,当您使用调试器检查动态对象时,您可以看到它显示了 d.phones.personal 的值作为 "Empty" 而不是 null(见下图)。
此问题的可能解决方法是在执行空合并操作之前安全地转换对象,如下例所示。这将阻止 null 合并运算符执行隐式转换。
string s = (d.phones.personal as string) ?? "default";
这是由于 Json.NET 和 ??
运算符的模糊行为造成的。
首先,当您将 JSON 反序列化为 dynamic
对象时,实际上 returned 是 Linq-to-JSON 类型 JToken
(例如 JObject
or JValue
) which has a custom implementation of IDynamicMetaObjectProvider
。即
dynamic d1 = JsonConvert.DeserializeObject(json);
var d2 = JsonConvert.DeserializeObject<JObject>(json);
实际上 return 正在做同样的事情。所以,对于你的 JSON 字符串,如果我这样做
var s1 = JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"];
var s2 = JsonConvert.DeserializeObject<dynamic>(json).phones.personal;
这两个表达式的计算结果完全相同 returned 动态对象。但是 returned 是什么对象?这让我们看到了 Json.NET 的第二个模糊行为:它不是用 null
指针表示空值,而是用一个特殊的 JValue
with JValue.Type
equal to JTokenType.Null
表示 then。因此,如果我这样做:
WriteTypeAndValue(s1, "s1");
WriteTypeAndValue(s2, "s2");
控制台输出为:
"s1": Newtonsoft.Json.Linq.JValue: ""
"s2": Newtonsoft.Json.Linq.JValue: ""
即这些对象 not null,它们被分配了 POCO,它们的 ToString()
return 是一个空字符串。
但是,当我们将该动态类型分配给字符串时会发生什么?
string tmp;
WriteTypeAndValue(tmp = s2, "tmp = s2");
打印:
"tmp = s2": System.String: null value
为什么不同?这是因为 DynamicMetaObject
returned by JValue
to resolve the conversion of the dynamic type to string eventually calls ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type)
最终 returns null
为 JTokenType.Null
值,这与显式转换为字符串所执行的逻辑相同,避免了对 [=22 的所有使用=]:
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: ""
现在,进入实际问题。由于 noted the ?? operator returns dynamic
当两个操作数之一是 dynamic
时,所以 d.phones.personal ?? "default"
不会尝试执行类型转换,因此 return 是一个 JValue
:
dynamic d = JsonConvert.DeserializeObject<dynamic>(json);
WriteTypeAndValue((d.phones.personal ?? "default"), "d.phones.personal ?? \"default\"");
// Prints "(d.phones.personal ?? "default")": Newtonsoft.Json.Linq.JValue: ""
但是如果我们通过将动态 return 分配给字符串来调用 Json.NET 到字符串的类型转换,那么转换器将启动并且 return 一个实际的空指针 合并运算符完成其工作并return编辑了一个非空 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");
最后,将类型和值写入控制台的辅助方法:
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
可能如何计算为 false
)。
虽然上述所有答案都是正确的,但如果您有包含许多成员的大型 JSON 结构,解决方法可能会很麻烦。这是由于 JSON.NET 的设计方式所致。
我发现的一个解决方案是使用 JSON.NET 的替代方案,它本身支持空合并运算符,并且可以通过最少的代码更改轻松替换库,如下所述:
我最近在使用 Json.NET 将 JSON 解析为动态对象时发现空合并运算符存在问题。假设这是我的动态对象:
string json = "{ \"phones\": { \"personal\": null }, \"birthday\": null }";
dynamic d = JsonConvert.DeserializeObject(json);
如果我尝试使用 ?? d 字段之一的运算符,它 returns 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.
上对这些问题进行了广泛的测试我想我找到了原因...看起来好像 null 合并运算符将动态 属性 转换为与语句的输出类型匹配的类型(在您的情况下它对 d.phones.personal[= 的值执行 ToString 操作33=]). ToString 操作将 "null" JSON 值转换为空字符串(而不是实际的空值)。因此,null 合并运算符将所讨论的值视为空字符串而不是 null,这会导致测试失败并且不会返回 "default" 值。
此外,当您使用调试器检查动态对象时,您可以看到它显示了 d.phones.personal 的值作为 "Empty" 而不是 null(见下图)。
此问题的可能解决方法是在执行空合并操作之前安全地转换对象,如下例所示。这将阻止 null 合并运算符执行隐式转换。
string s = (d.phones.personal as string) ?? "default";
这是由于 Json.NET 和 ??
运算符的模糊行为造成的。
首先,当您将 JSON 反序列化为 dynamic
对象时,实际上 returned 是 Linq-to-JSON 类型 JToken
(例如 JObject
or JValue
) which has a custom implementation of IDynamicMetaObjectProvider
。即
dynamic d1 = JsonConvert.DeserializeObject(json);
var d2 = JsonConvert.DeserializeObject<JObject>(json);
实际上 return 正在做同样的事情。所以,对于你的 JSON 字符串,如果我这样做
var s1 = JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"];
var s2 = JsonConvert.DeserializeObject<dynamic>(json).phones.personal;
这两个表达式的计算结果完全相同 returned 动态对象。但是 returned 是什么对象?这让我们看到了 Json.NET 的第二个模糊行为:它不是用 null
指针表示空值,而是用一个特殊的 JValue
with JValue.Type
equal to JTokenType.Null
表示 then。因此,如果我这样做:
WriteTypeAndValue(s1, "s1");
WriteTypeAndValue(s2, "s2");
控制台输出为:
"s1": Newtonsoft.Json.Linq.JValue: ""
"s2": Newtonsoft.Json.Linq.JValue: ""
即这些对象 not null,它们被分配了 POCO,它们的 ToString()
return 是一个空字符串。
但是,当我们将该动态类型分配给字符串时会发生什么?
string tmp;
WriteTypeAndValue(tmp = s2, "tmp = s2");
打印:
"tmp = s2": System.String: null value
为什么不同?这是因为 DynamicMetaObject
returned by JValue
to resolve the conversion of the dynamic type to string eventually calls ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type)
最终 returns null
为 JTokenType.Null
值,这与显式转换为字符串所执行的逻辑相同,避免了对 [=22 的所有使用=]:
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"
不会尝试执行类型转换,因此 return 是一个 JValue
:
dynamic d = JsonConvert.DeserializeObject<dynamic>(json);
WriteTypeAndValue((d.phones.personal ?? "default"), "d.phones.personal ?? \"default\"");
// Prints "(d.phones.personal ?? "default")": Newtonsoft.Json.Linq.JValue: ""
但是如果我们通过将动态 return 分配给字符串来调用 Json.NET 到字符串的类型转换,那么转换器将启动并且 return 一个实际的空指针 合并运算符完成其工作并return编辑了一个非空 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");
最后,将类型和值写入控制台的辅助方法:
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
可能如何计算为 false
)。
虽然上述所有答案都是正确的,但如果您有包含许多成员的大型 JSON 结构,解决方法可能会很麻烦。这是由于 JSON.NET 的设计方式所致。
我发现的一个解决方案是使用 JSON.NET 的替代方案,它本身支持空合并运算符,并且可以通过最少的代码更改轻松替换库,如下所述: