2015-03-14 2 views
10

Недавно я обнаружил проблему с оператором с нулевым коалесцированием при использовании Json.NET для анализа JSON в качестве динамических объектов. Предположим, что это мой динамический объект:Оператор Null-coalescing, возвращающий значение null для свойств динамических объектов

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

Если я попытаюсь использовать ??? оператор на одном из области D, то она возвращает нуль:

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.

+0

Можете ли вы немного упростить это? Удалите вложенные объекты, длину и '+ ='. Я сомневаюсь, что они должны воспроизвести проблему. – usr

+2

Я пробовал смотреть на ИЛ, но смотреть на ИЛ вокруг «динамика» никогда не бывает удовольствием :) – MarcinJuraszek

ответ

18

Это происходит из-за неясных поведения Json.NET и оператора ??.

Во-первых, когда вы десериализации JSON к dynamic объекта, что на самом деле возвращается в подкласс Linq-на-JSON типа JToken (например, JObject или JValue), который имеет пользовательский реализации 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; 

Оба эти выражения оцениваются точно таким же возвращенным динамическим объектом. Но какой объект возвращается? Это подводит нас к второму неясному поведению Джсона.NET: вместо представления нулевых значений с помощью указателей null, он представляет собой со значением JValue с JValue.Type, равным JTokenType.Null. Таким образом, если я:

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

Выход консоли:

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

Т.е. эти объекты: не null, они выделяются POCOs, а их ToString() возвращает пустую строку.

Но что происходит, когда мы назначаем этот динамический тип строке?

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

Печать:

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

Почему разница? Это происходит потому, что DynamicMetaObject возвращаемого JValue разрешить преобразование динамического типа в строку в конце концов вызывает 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 отметил ?? operator возвращает dynamic, когда один из двух операндов 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 в в строку путем присвоения динамического возврата в строку, то преобразователь будет удар и возвращает реальный указатель нулевого после оператора коалесцирующего выполнил свою работу и вернулся ненулевой 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).

+1

Невероятный ответ, спасибо. –

2

Я думаю, что я понял причину этого ... Похоже, что нулевой оператор коалесцирования преобразует динамическое свойство в тип, соответствующий типу вывода оператора (в вашем случае он выполняет ToString Операция по значению d.phones.personal). Операция Операция ToString преобразует значение «null» JSON как пустую строку (а не фактическое нулевое значение). Таким образом, оператор нулевого коалесцирования видит значение, о котором идет речь, как пустую строку вместо нуля, что приводит к сбою теста, а значение «по умолчанию» не возвращается.

Подробнее:https://social.msdn.microsoft.com/Forums/en-US/94b3ca1c-bbfa-4308-89fa-6b455add9de6/dynamic-improvements-on-c-nullcoalescing-operator?forum=vs2010ctpvbcs

Кроме того, когда вы проверяете динамический объект с помощью отладчика вы можете увидеть, что он показывает значение d.phones.personal как «пустой», а не нулевой (см. рисунок ниже).

enter image description here

Возможный способом решения этой проблемы является безопасно приведением объекта перед выполнением операции нуля коалесцирующим, как в примере ниже. Это будет препятствовать выполнению неявного литья оператора нулевого коалесцирования.

string s = (d.phones.personal as string) ?? "default"; 
Смежные вопросы