Chiamata al metodo se non null in C #


106

È possibile in qualche modo abbreviare questa affermazione?

if (obj != null)
    obj.SomeMethod();

perché lo scrivo molto spesso e diventa piuttosto fastidioso. L'unica cosa a cui riesco a pensare è implementare il pattern Null Object , ma non è quello che posso fare ogni volta e certamente non è una soluzione per abbreviare la sintassi.

E un problema simile con gli eventi, dove

public event Func<string> MyEvent;

e quindi invocare

if (MyEvent != null)
    MyEvent.Invoke();

41
Abbiamo preso in considerazione l'aggiunta di un nuovo operatore in C # 4: "obj.?SomeMethod ()" significherebbe "chiamare SomeMethod se obj non è null, altrimenti restituire null". Sfortunatamente non rientrava nel nostro budget, quindi non l'abbiamo mai implementato.
Eric Lippert

@ Eric: questo commento è ancora valido? Ho visto da qualche parte che è disponibile con 4.0?
CharithJ

@CharithJ: No. Non è mai stato implementato.
Eric Lippert

3
@ CharithJ: sono consapevole dell'esistenza dell'operatore di coalescenza nullo. Non fa quello che vuole Darth; vuole un operatore di accesso membro sollevato a nullable. (E a proposito, il tuo commento precedente fornisce una caratterizzazione errata dell'operatore di coalescenza nullo. Volevi dire "v = m == null? Y: m.Value può essere scritto v = m ?? y".)
Eric Lippert

4
Per i lettori più recenti: C # 6.0 implementa?., Quindi x? .Y? .Z? .ToString () restituirà null se x, yo z sono null o restituirà z.ToString () se nessuno di essi è null.
David

Risposte:


162

Da C # 6 in poi, puoi semplicemente usare:

MyEvent?.Invoke();

o:

obj?.SomeMethod();

Il ?.è l'operatore null-moltiplicazione, e causano la .Invoke()di essere cortocircuitato quando l'operando è null. L'operando è accessibile solo una volta, quindi non c'è il rischio del problema di "cambio di valore tra check e invoke".

===

Prima di C # 6, no: non esiste alcuna magia null-safe, con un'eccezione; metodi di estensione, ad esempio:

public static void SafeInvoke(this Action action) {
    if(action != null) action();
}

ora questo è valido:

Action act = null;
act.SafeInvoke(); // does nothing
act = delegate {Console.WriteLine("hi");}
act.SafeInvoke(); // writes "hi"

In caso di eventi, questo ha il vantaggio di rimuovere anche la race condition, ovvero non serve una variabile temporanea. Quindi normalmente avresti bisogno di:

var handler = SomeEvent;
if(handler != null) handler(this, EventArgs.Empty);

ma con:

public static void SafeInvoke(this EventHandler handler, object sender) {
    if(handler != null) handler(sender, EventArgs.Empty);
}

possiamo usare semplicemente:

SomeEvent.SafeInvoke(this); // no race condition, no null risk

1
Sono un po 'in conflitto su questo. Nel caso di un'azione o di un gestore di eventi, che in genere è più indipendente, questo ha un senso. Tuttavia, non spezzerei l'incapsulamento per un metodo normale. Ciò implica la creazione del metodo in una classe statica separata e non credo che la perdita dell'incapsulamento e il degrado della leggibilità / organizzazione del codice in generale valga il piccolo miglioramento della leggibilità locale
tvanfosson

@tvanfosson - in effetti; ma il punto è che questo è l'unico caso in cui so dove funzionerà. E la domanda stessa solleva il tema dei delegati / eventi.
Marc Gravell

Quel codice finisce per generare un metodo anonimo da qualche parte, che rovina davvero la traccia dello stack di qualsiasi eccezione. È possibile dare nomi anonimi ai metodi? ;)
sisve

Sbagliato come per 2015 / C # 6.0 ...? è lui la soluzione. ReferenceTHatMayBeNull? .CallMethod () non chiamerà il metodo quando è null.
TomTom

1
@mercu dovrebbe essere ?.- in VB14 e superiori
Marc Gravell

27

Quello che stai cercando è il Null condizionale (non "coalescenza") Operatore: ?.. È disponibile a partire da C # 6.

Il tuo esempio sarebbe obj?.SomeMethod();. Se obj è nullo, non accade nulla. Quando il metodo ha argomenti, ad esempio obj?.SomeMethod(new Foo(), GetBar());gli argomenti non vengono valutati se objè nullo, che importa se la valutazione degli argomenti avrebbe effetti collaterali.

E il concatenamento è possibile: myObject?.Items?[0]?.DoSomething()


1
È fantastico. Vale la pena notare che questa è una funzionalità C # 6 ... (il che sarebbe sottinteso dall'istruzione VS2015, ma comunque degno di nota). :)
Kyle Goode

10

Un metodo di estensione rapido:

    public static void IfNotNull<T>(this T obj, Action<T> action, Action actionIfNull = null) where T : class {
        if(obj != null) {
            action(obj);
        } else if ( actionIfNull != null ) {
            actionIfNull();
        }
    }

esempio:

  string str = null;
  str.IfNotNull(s => Console.Write(s.Length));
  str.IfNotNull(s => Console.Write(s.Length), () => Console.Write("null"));

o in alternativa:

    public static TR IfNotNull<T, TR>(this T obj, Func<T, TR> func, Func<TR> ifNull = null) where T : class {
        return obj != null ? func(obj) : (ifNull != null ? ifNull() : default(TR));
    }

esempio:

    string str = null;
    Console.Write(str.IfNotNull(s => s.Length.ToString());
    Console.Write(str.IfNotNull(s => s.Length.ToString(), () =>  "null"));

Ho provato a farlo con metodi di estensione e sono finito con più o meno lo stesso codice. Tuttavia, il problema con questa implementazione è che non funzionerà con espressioni che restituiscono un tipo di valore. Quindi doveva avere un secondo metodo per quello.
orad

4

Gli eventi possono essere inizializzati con un delegato predefinito vuoto che non viene mai rimosso:

public event EventHandler MyEvent = delegate { };

Nessun controllo null necessario.

[ Aggiornamento , grazie a Bevan per averlo segnalato]

Sii consapevole del possibile impatto sulle prestazioni, però. Un rapido micro benchmark che ho fatto indica che la gestione di un evento senza sottoscrittori è 2-3 volte più lenta quando si utilizza il pattern "delegato predefinito". (Sul mio laptop dual core da 2,5 GHz ciò significa 279 ms: 785 ms per la raccolta di 50 milioni di eventi non iscritti.). Per gli hot spot dell'applicazione, questo potrebbe essere un problema da considerare.


1
Quindi, eviti un controllo nullo invocando un delegato vuoto ... un sacrificio misurabile sia di memoria che di tempo per risparmiare alcune sequenze di tasti? YMMV, ma per me, uno scarso compromesso.
Bevan

Ho anche visto benchmark che mostrano che è molto più costoso invocare un evento con più delegati iscritti rispetto a uno solo.
Greg D


2

Questo articolo di Ian Griffiths offre due diverse soluzioni al problema che, conclude, sono trucchi accurati che non dovresti usare.


3
E parlando come l'autore di quell'articolo, aggiungerei che non dovresti assolutamente usarlo ora che C # 6 risolve questo problema fuori dagli schemi con gli operatori condizionali nulli (?. E. []).
Ian Griffiths

2

Cerating extention metodo come quello suggerito non risolve realmente i problemi con le condizioni di gara, ma piuttosto li nasconde.

public static void SafeInvoke(this EventHandler handler, object sender)
{
    if (handler != null) handler(sender, EventArgs.Empty);
}

Come affermato, questo codice è l'equivalente elegante della soluzione con variabile temporanea, ma ...

Il problema è che è possibile che l'abbonato all'evento possa essere chiamato DOPO che si è cancellato dall'evento . Ciò è possibile perché l'annullamento della sottoscrizione può avvenire dopo che l'istanza del delegato viene copiata nella variabile temporanea (o passata come parametro nel metodo precedente), ma prima che venga richiamato il delegato.

In generale, il comportamento del codice client è imprevedibile in questo caso: lo stato del componente non può già consentire di gestire la notifica degli eventi. È possibile scrivere il codice del client nel modo in cui gestirlo, ma porrebbe al client una responsabilità inutile.

L'unico modo noto per garantire la sicurezza del thread è utilizzare l'istruzione lock per il mittente dell'evento. Ciò garantisce che tutte le sottoscrizioni \ unsubscriptions \ invocation siano serializzate.

Per essere più accurati, il blocco dovrebbe essere applicato allo stesso oggetto di sincronizzazione utilizzato nei metodi della funzione di accesso agli eventi aggiungi \ rimuovi che è il valore predefinito "questo".


Questa non è la condizione di gara a cui si fa riferimento. La condizione della competizione è if (MyEvent! = Null) // MyEvent non è null ora MyEvent.Invoke (); // MyEvent ora è nullo, accadono cose brutte Quindi, con questa condizione di competizione, non posso scrivere un gestore di eventi asincrono che sia garantito per funzionare. Tuttavia, con la tua 'race condition', posso scrivere un gestore di eventi asincrono che è garantito per funzionare. Ovviamente le chiamate all'abbonato e all'annullamento dell'abbonamento devono essere sincrone, altrimenti è richiesto un codice di blocco.
jyoung

Infatti. La mia analisi di questo problema è pubblicata qui: blogs.msdn.com/ericlippert/archive/2009/04/29/…
Eric Lippert


1

Forse non meglio ma a mio avviso più leggibile è creare un metodo di estensione

public static bool IsNull(this object obj) {
 return obj == null;
}

1
cosa return obj == nullsignifica. Cosa tornerà
Hammad Khan

1
Significa che se objè nullil metodo tornerà true, credo.
Joel

1
Come fa questo anche a rispondere alla domanda?
Kugel

Risposta in ritardo, ma sei sicuro che funzionerebbe? Puoi chiamare un metodo di estensione su un oggetto che è null? Sono abbastanza sicuro che questo non funzioni con i metodi del tipo, quindi dubito che funzionerebbe anche con i metodi di estensione. Credo che il modo migliore per verificare se un oggetto è null è obj is null. Sfortunatamente, per verificare se un oggetto non lo è, non è nullnecessario racchiuderlo tra parentesi, il che è un peccato.
natiiix

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.