2015-03-14 8 views
10

Ho trovato recentemente un problema con l'operatore null-coalescing mentre si utilizzava Json.NET per analizzare JSON come oggetti dinamici. Supponiamo che questo sia il mio oggetto dinamico:Operatore NULL coalescente che restituisce null per le proprietà degli oggetti dinamici

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

Se provo ad usare il ?? operatore su un campo di d, restituisce null:

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

Tuttavia, se assegno una proprietà dinamica di una stringa, allora funziona bene:

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

Infine, quando l'uscita Console.WriteLine(d.phones.personal == null) emette True.

Ho effettuato un test approfondito di questi problemi su Pastebin.

+0

Potete semplificarlo un po '? Rimuovi gli oggetti nidificati, la lunghezza e '+ ='. Dubito che siano tenuti a riprodurre il problema. – usr

+2

Ho provato a guardare IL, ma guardare IL attorno a 'dynamic' non è mai un piacere :) – MarcinJuraszek

risposta

18

Ciò è dovuto al oscuri comportamenti Json.NET e l'operatore ??.

primo luogo, quando si deserializzare JSON a un oggetto dynamic, di fatto viene restituito è una sottoclasse della Linq to JSON tipo JToken (ad esempio JObject o JValue) che ha un'implementazione personalizzata IDynamicMetaObjectProvider. Cioè

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

In realtà stanno restituendo la stessa cosa. Quindi, per la stringa JSON, se lo faccio

var s1 = JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"]; 
    var s2 = JsonConvert.DeserializeObject<dynamic>(json).phones.personal; 

Entrambe queste espressioni valutano esattamente lo stesso oggetto dinamico restituito. Ma quale oggetto viene restituito? Questo ci porta al secondo oscuro comportamento di Json.NET: anziché rappresentare valori nulli con i puntatori null, rappresenta quindi con uno specialecon JValue.Type uguale a JTokenType.Null. Così se faccio:

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

L'uscita della console è:

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

Vale a dire questi oggetti sono non null, vengono assegnati POCO e il loro ToString() restituisce una stringa vuota.

Ma, cosa succede quando assegniamo quel tipo dinamico a una stringa?

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

Stampe:

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

Perché la differenza? È perché il DynamicMetaObject restituito da JValue per risolvere la conversione di tipo dinamico a stringa casualmente chiamate ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type) che alla fine restituisce null per un valore JTokenType.Null, che è la stessa logica eseguita dal cast esplicito a stringa evitando tutti gli usi di 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: "" 

Ora, alla domanda reale. Come osservato husterk i rendimenti ?? operatordynamic quando uno dei due operandi è dynamic, quindi d.phones.personal ?? "default" non tenta di eseguire una conversione di tipo, quindi il ritorno è un 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: "" 

Ma se si invoca conversione del tipo di Json.NET a stringa assegnando il ritorno dinamica di una stringa, il convertitore entreranno in e restituire un puntatore nullo effettivo dopo che l'operatore coalescenza ha svolto il proprio lavoro e restituito un non nullo JValue:

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

Questo spiega la differenza che si sta vedendo.

Per evitare questo problema, forzare la conversione da dinamico a stringa prima viene applicato l'operatore coalescenza:

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

Infine, il metodo di supporto per scrivere il tipo e il valore alla 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)); 
} 

(Per inciso, l'esistenza del tipo nullo JValue spiega come l'espressione (object)(JValue)(string)null == (object)(JValue)null potrebbe valutare su false).

+1

Risposta incredibile, grazie. –

2

Penso di aver capito il motivo di ciò ... Sembra che l'operatore null coalescente converta la proprietà dinamica in un tipo che corrisponde al tipo di output dell'istruzione (nel tuo caso esegue un ToString operazione sul valore di d.phones.personal). L'operazione ToString converte il valore JSON "null" in una stringa vuota (e non un valore null effettivo). Pertanto, l'operatore null coalescing vede il valore in questione come una stringa vuota anziché null che causa il fallimento del test e il valore "predefinito" non viene restituito.

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

Inoltre, quando si controlla l'oggetto dinamico con il debugger si può vedere che mostra il valore per d.phones.personal come "vuoto" e non nullo (vedi immagine sotto).

enter image description here

Una possibile soluzione per questo problema è quello di gettare in sicurezza l'oggetto prima di eseguire l'nullo coalescenza operazione come nell'esempio riportato di seguito. Ciò impedirà all'operatore null coalescing di eseguire il casting implicito.

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