Оператор Null-coalescing, возвращающий значение null для свойств динамических объектов

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

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.

Ответ 1

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

Во-первых, когда вы десериализуете JSON для объекта dynamic, то, что фактически возвращается, является подклассом типа Linq-to-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;

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

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

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

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

т.е. эти объекты не являются нулевыми, они выделяются 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).

Ответ 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";