【问题标题】:Null-coalescing operator returning null for properties of dynamic objects为动态对象的属性返回 null 的空合并运算符
【发布时间】:2015-05-17 01:58:09
【问题描述】:

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

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 上对这些问题进行了广泛的测试。

【问题讨论】:

  • 你能简化一下吗?删除嵌套对象、长度和+=。我怀疑他们是否需要重现该问题。
  • 我尝试查看 IL,但查看 dynamic 周围的 IL 绝不是一种乐趣 :)

标签: c# json dynamic json.net null-coalescing-operator


【解决方案1】:

这是由于 Json.NET 和 ?? 运算符的晦涩行为造成的。

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

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 指针表示空值,而是用一个特殊的 JValue 表示,JValue.Type 等于 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) 最终返回 null 以获得 JTokenType.Null 值,这与显式转换执行的逻辑相同字符串避免所有使用dynamic:

    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: ""

现在,进入实际问题。正如husterk 所指出的,当两个操作数之一是dynamic?? operator 返回dynamic,因此d.phones.personal ?? "default" 不会尝试执行类型转换,因此返回是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: ""

但是如果我们通过将动态返回分配给字符串来调用 Json.NET 到字符串的类型转换,那么转换器将启动并在合并运算符完成其工作并返回一个非-null 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

【讨论】:

  • 顺便说一句,普通的条件运算符也可以正常工作:d.phones.personal != null ? d.phones.personal : "default"。我假设不等于首先转换为字符串,这就是它起作用的原因
  • 一个有趣的评论:Visual Studio 不知道这一点,并建议将条件运算符切换为 Null Coalescence 直接导致您出现错误。另一方面,Resharper 知道这一点,并且仅在参数不是动态的情况下才建议使用它:) 一个人无法智取 R#
  • 多么棒的答案!恭喜!
【解决方案2】:

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-03-18
    • 1970-01-01
    • 1970-01-01
    • 2022-01-22
    • 2016-04-02
    • 1970-01-01
    • 2019-03-27
    • 1970-01-01
    相关资源
    最近更新 更多