Ответ 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
).