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" 值。

更多信息: 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(见下图)。

此问题的可能解决方法是在执行空合并操作之前安全地转换对象,如下例所示。这将阻止 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 nullJTokenType.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 的替代方案,它本身支持空合并运算符,并且可以通过最少的代码更改轻松替换库,如下所述: