2015-03-14 32 views
10

Niedawno wykryłem problem z operatorem koalescencyjnym null podczas korzystania z Json.NET do parsowania JSON jako obiektów dynamicznych. Załóżmy, że jest to mój dynamiczny obiekt:Operator koalescencyjny zwracający wartość null dla właściwości obiektów dynamicznych

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

Jeśli spróbuję użyć? Operator na jednym obszarze D, to zwraca null:

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

Jeśli jednak przypisać właściwość dynamiczną do łańcucha, to działa dobrze:

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

Wreszcie, kiedy wyjście Console.WriteLine(d.phones.personal == null) to wyprowadza True.

Przeprowadziłem obszerny test tych problemów na Pastebin.

+0

Czy możesz to nieco uprościć? Usuń zagnieżdżone obiekty, długość i '+ ='. Wątpię, czy są oni zobowiązani do odtworzenia problemu. – usr

+2

Próbowałem patrzeć na IL, ale patrzenie na IL wokół 'dynamiki 'nigdy nie jest przyjemnością :) – MarcinJuraszek

Odpowiedz

18

Wynika to z niejasnych zachowań Json.NET i operatora ??.

Po pierwsze, kiedy deserializować JSON do dynamic obiektu, co jest rzeczywiście zwrócony jest podklasą LINQ-JSON typu JToken (np JObject lub JValue), która ma niestandardową implementację IDynamicMetaObjectProvider. To znaczy.

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

Rzeczywiście wracają to samo. Tak więc, dla twojego ciągu JSON, jeśli wykonuję

var s1 = JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"]; 
    var s2 = JsonConvert.DeserializeObject<dynamic>(json).phones.personal; 
Oba te wyrażenia oceniają dokładnie ten sam zwracany obiekt dynamiczny. Ale jaki przedmiot jest zwracany? To prowadzi nas do drugiego, niejasnego zachowania Jsona.NET: zamiast reprezentowania wartości pustych ze wskaźnikami null reprezentuje wtedy specjalny JValue z JValue.Type równy JTokenType.Null. Tak więc jeśli robię:

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

Wyjście konsola jest:

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

Tj obiekty te są nie puste, przydzielane są POCO, a ich ToString() zwraca pusty ciąg znaków.

Ale co się stanie, gdy przypiszemy ten typ dynamiczny do łańcucha?

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

Wydruki:

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

Dlaczego różnica? To dlatego, że DynamicMetaObject zwrócony przez JValue rozwiązać konwersję dynamicznego typu STRING ostatecznie nazywa ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type) który ostatecznie zwraca null dla wartości JTokenType.Null, która jest taka sama logika wykonywana przez wyraźnej obsady ciąg unikając wszystkie zastosowania 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: "" 

Teraz, na rzeczywiste pytanie. Jak husterk odnotowała ?? operator Przywraca dynamic gdy jeden z dwóch argumentów jest dynamic, więc d.phones.personal ?? "default" nie próbuje wykonać konwersję typu, a więc powrót jest 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: "" 

Ale jeśli wzywamy Typ konwersji Json.NET za ciąg przypisując dynamiczny powrót na sznurku, a następnie konwerter będzie kopać i powrócić rzeczywisty wskaźnik NULL po operatora koalescencyjnego wykonał swoją pracę i wrócił niepusta JValue:

string tmp; 
    WriteTypeAndValue(tmp = (d.phones.personal ?? "default"), "tmp = (d.phones.personal ?? \"default\")"); 
    // Prints "tmp = (d.phones.personal ?? "default")": System.String: null value 

To wyjaśnia różnicę, którą widzisz.

Aby uniknąć tego problemu, należy wymusić konwersję z dynamiczny ciąg zanim operator koalescencyjny jest stosowana:

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

Wreszcie metoda pomocnika napisać rodzaj i wartość do konsoli:

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

(Na marginesie, istnienie typu zerowego JValue wyjaśnia, w jaki sposób wyrażenie (object)(JValue)(string)null == (object)(JValue)null może oszacować na false).

+1

Niewiarygodna odpowiedź, dzięki. –

2

Myślę, że zorientowali się, jakie są tego powody ... Wygląda jakby null operator koalescencyjny konwertuje dynamiczną właściwość do typu, który pasuje do typu wyjściowego rachunku (w przypadku wykonuje on ToString operacja na wartości d.phones.personal). Operacja ta przekształca wartość JSON "null" na pusty ciąg znaków (a nie rzeczywistą wartość pustą). W związku z tym operator koalescencyjny zerowy widzi wartość jako pusty łańcuch zamiast wartości zerowej, co powoduje niepowodzenie testu, a wartość "domyślna" nie jest zwracana.

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

Również podczas inspekcji dynamiczny obiekt z debugera widać, że pokazuje wartość dla d.phones.personal jako „pusty”, a nie wartość null (patrz zdjęcie poniżej).

enter image description here

Możliwe obejście tego problemu jest bezpiecznie oddania obiektu przed wykonaniem NULL koalescencyjny operację jak w poniższym przykładzie. Uniemożliwi to operatorowi koalescencji zerowej wykonywanie niejawnego rzucania.

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