2016-05-24 22 views
21

ho questi metodi di estensione e tipo enum:Perché devo posizionare() attorno all'espressione null-condizionale per usare il metodo corretto di overload?

public static bool IsOneOf<T>(this T thing, params T[] things) 
{ 
    return things.Contains(thing); 
} 

public static bool IsOneOf<T>(this T? thing, params T[] things) where T : struct 
{ 
    return thing.HasValue && things.Contains(thing.Value); 
} 

public enum Color { Red, Green, Blue } 

La prima if seguito compila; il secondo no:

if ((x.Y?.Color).IsOneOf(Color.Red, Color.Green)) 
; 
if (x.Y?.Color.IsOneOf(Color.Red, Color.Green)) 
; 

Esse variano solo dal gruppo di parentesi in più. Perché devo farlo?

In un primo momento ho sospettato che stava facendo un cast implicito doppia da bool? a bool e poi di nuovo a bool?, ma quando rimuovo il primo metodo di estensione, si lamenta non c'è cast implicito da bool a bool?. Ho quindi controllato l'IL e non ci sono stati lanci. Decompilazione torna a C# rendimenti qualcosa che assomiglia a:

if (!(y != null ? new Color?(y.Color) : new Color?()).IsOneOf<Color>(new Color[2] 
{ 
    Color.Red, 
    Color.Green 
})); 

che va bene per la versione del CLR sto correndo, e quello che mi aspetto. Quello che non mi aspettavo è che x.Y?.Color.IsOneOf(Color.Red, Color.Green) non venga compilato.

Cosa sta succedendo? È semplicemente il modo in cui è stata implementata la lingua che richiede lo ()?

Aggiornamento

Ecco un tappo schermata che mostra l'errore nel contesto. Questo mi sta diventando ancora più confuso. L'errore ha effettivamente senso; ma quello che non (nella mia mente ora) è perché la linea 18 non avrebbe lo stesso problema.

enter image description here

+3

Questo è sorprendente per me; tu * potresti * aver trovato un bug.Quello che trovo più fastidioso qui è che la risoluzione del sovraccarico sta scegliendo il secondo metodo per il caso con parens, e il primo metodo per il caso senza parens. Non mi è chiaro perché i genitori dovrebbero fare la differenza lì. –

+0

Detto questo, vorrei riprendere i metodi di estensione qui. I metodi di estensione che estendono tutti i tipi sono spesso confusi. La cosa più comune da fare è estendere 'IEnumerable ' e farlo nell'altro modo: 'var colors = new [] {Color.Red, Color.Green}; if (colors.Contains (whatever)) ... ' –

+0

Inoltre, noto che se si desidera estendere tutto T, * è * legale da confrontare con null. 'public static bool IsOneOf (questa cosa T, params T [] cose) { return thing! = null && things.Contains (cosa); } 'è legale. –

risposta

9

Prima di tutto, questo comportamento mi sembra intenzionale. È raro che qualcuno aggiunga metodi di estensione a tipi annullabili, ed è molto comune che le persone uniscano accessi ai membri normali e nulli in un'unica espressione, quindi la lingua preferisce quest'ultima.

considerare i seguenti esempi:

class B { bool c; } 
class A { B b; } 
... 

A a; 
var output = a?.b.c; // infers bool?, throws NPE if (a != null && a.b == null) 
// roughly translates to 
// var output = (a == null) ? null : a.b.c; 

mentre

A a; 
var output = (a?.b).c; // infers bool, throws NPE if (a == null || a.b == null) 
// roughly translates to 
// var output = ((a == null) ? null : a.b).c; 

e poi c'è

A a; 
var output = a?.b?.c; // infers bool?, *cannot* throw NPE 
// roughly translates to 
// var output = (a == null) ? null : (a.b == null) ? null : a.b.c; 

// and this is almost the same as 
// var output = (a?.b)?.c; // infers bool?, cannot throw NPE 
// Only that the second `?.` is forced to evaluate every time. 

Il design obiettivo qui sembra essere aiutando la distinzione tra a?.b.c e a?.b?.c. Se a è nullo, ci aspettiamo di ottenere un NPE in nessuno dei casi. Perché? Perché c'è un condizionale null direttamente dopo a. Quindi la parte .c deve essere valutata solo se a non è nullo, rendendo l'accesso membro dipendente dal precedente risultato condizionale nullo. Aggiungendo le parentesi esplicite, (a?.b).c, imponiamo al compilatore di provare ad accedere a .c di (a?.b) indipendentemente da a come null, impedendogli di "cortocircuitare" l'intera espressione su null. (usando @JamesBuck -s words)

Nel tuo caso, x.Y?.Color.IsOneOf(Color.Red, Color.Green) è come a?.b.c. Chiamerà la funzione con la firma bool IsOneOf(Color red) (quindi il sovraccarico in cui il parametro non è annullabile e ho rimosso la parte generica) solo quando x.Y non era nullo, quindi avvolgendo il tipo dell'espressione in Nullable per gestire il caso quando x.Y è nullo. E poiché per il momento valuta bool? anziché bool, non può essere utilizzato come test in un'istruzione if.

0

Se si inserisce la seguente riga vedrai che .NET è la creazione di un tipo dinamico System.Nullable<UserQuery+Color> per questo nulla espressione condizionale all'interno delle parentesi.

Console.WriteLine((x.Y?.Color).GetType().ToString());

Se non metti le parentesi probabilmente cerca di valutare come espressione normale, quindi l'errore.

+1

Non sono sicuro di come lo hai ottenuto. Ho provato 'Console.WriteLine (xY? .Color.GetType())' e 'Console.WriteLine ((xY? .Color) .GetType())' e ho ottenuto 'ConsoleApplication1.Color' per entrambi ('ConsoleApplication1' è il namespace in cui tutti questi tipi vivono nel mio progetto di test). – Kit

2

Il significato di x.Y?.Color.IsOneOf(Color.Red, Color.Green) è piuttosto indipendente dal sovraccarico del metodo. Gli pseudooperami ?. sono x.Y e Color.IsOneOf(Color.Red, Color.Green), anche se quest'ultimo non è un'espressione valida. Innanzitutto, viene valutato x.Y. Chiama il risultato tmp. Se tmp == null, l'intera espressione è null. Se tmp != null, l'intera espressione viene valutata su tmp.Color.IsOneOf(Color.Red, Color.Green).

Quando IsOneOf è un metodo di istanza, questo è praticamente sempre il comportamento che si desidera, motivo per cui è quel comportamento che ha finito per entrare nella specifica e nel compilatore.

Quando IsOneOf è un metodo di estensione, come nel tuo esempio, potrebbe non essere il comportamento desiderato, ma il comportamento è lo stesso, per coerenza e poiché c'è una soluzione facilmente disponibile che hai già trovato

The associativity and why the current result was picked are explained in a thread on the Roslyn CodePlex site.

+0

Sembra che tu stia dicendo che '? .' e' .' non hanno la stessa precedenza. Non ero in giro per il design della funzione, ma ciò mi sorprenderebbe; Mi chiedo che cosa abbia motivato questa decisione? Dovrei controllare le note di progettazione. –

+0

@EricLippert Penso che abbiano la stessa precedenza, ma hanno ragione-associativa. Se fosse stato specificato in un altro modo, non ci sarebbe un buon modo per scrivere ciò che ora possiamo scrivere come 'a? .bc', dove non c'è e non dovrebbe essere un controllo' nullo' sul valore di 'b'. – hvd

+0

@EricLippert Trovato il link pertinente, aggiunto alla mia risposta. – hvd

1

Ok, ho capito, ma io vado con Tamas' answer. Penso che il mio abbia un valore qui, però, come un aiuto pensante quando si lavora con condizionali nulli.

La risposta è stata tipo di fissando nella cattura dello schermo nella mia interrogazione

Type Conversion Error

ero tunnel visioning sulla conversione Color/Color?, ma il problema reale è che non può essere bool? Invia implicitamente a bool.

In una lunga espressione "punteggiata" che contiene l'operatore ?., si deve pensare in termini di cosa, e il suo tipo, dopo il punto più a destra. Quella cosa, in questo caso, è il metodo di estensione che restituisce un bool, ma l'uso di ?. "lo modifica" in modo efficace a bool?. In altre parole, si consideri la cosa più giusta come un tipo di riferimento che potrebbe essere null o Nullable<T>.

Wow, questo mi ha fatto andare.