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

时间:2022-01-22 11:46:58

I have recently found a problem with the null-coalescing operator while using Json.NET to parse JSON as dynamic objects. Suppose this is my dynamic object:

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

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

If I try to use the ?? operator on one of the field of d, it returns null:

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

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

However, if I assign a the dynamic property to a string, then it works fine:

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

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

Finally, when I output Console.WriteLine(d.phones.personal == null) it outputs True.

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

I have made an extensive test of these issues on Pastebin.

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

2 个解决方案

#1


18  

This is due to obscure behaviors of Json.NET and the ?? operator.

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

Firstly, when you deserialize JSON to a dynamic object, what is actually returned is a subclass of the Linq-to-JSON type JToken (e.g. JObject or JValue) which has a custom implementation of IDynamicMetaObjectProvider. I.e.

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

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

Are actually returning the same thing. So, for your JSON string, if I do

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

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

Both these expressions evaluate to exactly the same returned dynamic object. But what object is returned? That gets us to the second obscure behavior of Json.NET: rather than representing null values with null pointers, it represents then with a special JValue with JValue.Type equal to JTokenType.Null. Thus if I do:

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

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

The console output is:

控制台输出是:

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

I.e. these objects are not null, they are allocated POCOs, and their ToString() returns an empty string.

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

But, what happens when we assign that dynamic type to a string?

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

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

Prints:

打印:

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

Why the difference? It is because the DynamicMetaObject returned by JValue to resolve the conversion of the dynamic type to string eventually calls ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type) which eventually returns null for a JTokenType.Null value, which is the same logic performed by the explicit cast to string avoiding all uses of dynamic:

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

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

Now, to the actual question. As husterk noted the ?? operator returns dynamic when one of the two operands is dynamic, so d.phones.personal ?? "default" does not attempt to perform a type conversion, thus the return is a JValue:

现在,到实际的问题。正如赫斯特克注意到的那样?当两个操作数之一是动态的时,运算符返回动态,所以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: ""

But if we invoke Json.NET's type conversion to string by assigning the dynamic return to a string, then the converter will kick in and return an actual null pointer after the coalescing operator has done its work and returned a non-null 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

This explains the difference you are seeing.

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

To avoid this behavior, force the conversion from dynamic to string before the coalescing operator is applied:

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

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

Finally, the helper method to write the type and value to the console:

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

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));
}

(As an aside, the existence of the null-type JValue explains how the expression (object)(JValue)(string)null == (object)(JValue)null might possibly evaluate to false).

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

#2


2  

I think that I figured out the reason for this... It looks as though the null coalescing operator converts the dynamic property into a type that matches the output type of the statement (in your case it performs a ToString operation on the value of d.phones.personal). The ToString operation converts the "null" JSON value to be an empty string (and not an actual null value). Thus, the null coalescing operator sees the value in question as an empty string instead of null which causes the test to fail and the "default" value is not returned.

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

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

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

Also, when you inspect the dynamic object with the debugger you can see that it shows the value for d.phones.personal as "Empty" and not null (see image below).

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

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

A possible workaround for this issue is to safely cast the object prior to performing the null coalescing operation as in the sample below. This will prevent the null coalescing operator from performing the implicit casting.

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

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

#1


18  

This is due to obscure behaviors of Json.NET and the ?? operator.

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

Firstly, when you deserialize JSON to a dynamic object, what is actually returned is a subclass of the Linq-to-JSON type JToken (e.g. JObject or JValue) which has a custom implementation of IDynamicMetaObjectProvider. I.e.

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

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

Are actually returning the same thing. So, for your JSON string, if I do

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

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

Both these expressions evaluate to exactly the same returned dynamic object. But what object is returned? That gets us to the second obscure behavior of Json.NET: rather than representing null values with null pointers, it represents then with a special JValue with JValue.Type equal to JTokenType.Null. Thus if I do:

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

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

The console output is:

控制台输出是:

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

I.e. these objects are not null, they are allocated POCOs, and their ToString() returns an empty string.

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

But, what happens when we assign that dynamic type to a string?

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

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

Prints:

打印:

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

Why the difference? It is because the DynamicMetaObject returned by JValue to resolve the conversion of the dynamic type to string eventually calls ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type) which eventually returns null for a JTokenType.Null value, which is the same logic performed by the explicit cast to string avoiding all uses of dynamic:

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

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

Now, to the actual question. As husterk noted the ?? operator returns dynamic when one of the two operands is dynamic, so d.phones.personal ?? "default" does not attempt to perform a type conversion, thus the return is a JValue:

现在,到实际的问题。正如赫斯特克注意到的那样?当两个操作数之一是动态的时,运算符返回动态,所以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: ""

But if we invoke Json.NET's type conversion to string by assigning the dynamic return to a string, then the converter will kick in and return an actual null pointer after the coalescing operator has done its work and returned a non-null 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

This explains the difference you are seeing.

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

To avoid this behavior, force the conversion from dynamic to string before the coalescing operator is applied:

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

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

Finally, the helper method to write the type and value to the console:

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

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));
}

(As an aside, the existence of the null-type JValue explains how the expression (object)(JValue)(string)null == (object)(JValue)null might possibly evaluate to false).

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

#2


2  

I think that I figured out the reason for this... It looks as though the null coalescing operator converts the dynamic property into a type that matches the output type of the statement (in your case it performs a ToString operation on the value of d.phones.personal). The ToString operation converts the "null" JSON value to be an empty string (and not an actual null value). Thus, the null coalescing operator sees the value in question as an empty string instead of null which causes the test to fail and the "default" value is not returned.

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

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

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

Also, when you inspect the dynamic object with the debugger you can see that it shows the value for d.phones.personal as "Empty" and not null (see image below).

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

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

A possible workaround for this issue is to safely cast the object prior to performing the null coalescing operation as in the sample below. This will prevent the null coalescing operator from performing the implicit casting.

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

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