Null-coalescing operator returning null for properties of dynamic objects

前提是你 提交于 2019-12-28 20:35:46

问题


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:

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:

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.

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


回答1:


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

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.

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

    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:

    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.

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:

    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:

    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:

    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:

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).




回答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.

More info: 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).

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


来源:https://stackoverflow.com/questions/29051663/null-coalescing-operator-returning-null-for-properties-of-dynamic-objects

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!