Wywołanie metody, jeśli nie ma wartości null w C #


106

Czy da się jakoś skrócić to stwierdzenie?

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

ponieważ często to piszę i robi się to dość irytujące. Jedyne, co przychodzi mi do głowy, to zaimplementowanie wzorca Null Object , ale nie jest to to, co mogę zrobić za każdym razem i na pewno nie jest to rozwiązanie do skrócenia składni.

I podobny problem z wydarzeniami, gdzie

public event Func<string> MyEvent;

a następnie wywołaj

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

41
Rozważaliśmy dodanie nowego operatora do C # 4: „obj.?SomeMethod ()” oznaczałoby „wywołanie SomeMethod, jeśli obj nie ma wartości null, w przeciwnym razie zwróci wartość null”. Niestety nie mieściło się to w naszym budżecie, więc nigdy go nie wdrożyliśmy.
Eric Lippert

@Eric: Czy ten komentarz jest nadal aktualny? Widziałem gdzieś, że jest dostępny w wersji 4.0?
CharithJ

@CharithJ: Nie. Nigdy nie został wdrożony.
Eric Lippert

3
@CharithJ: Jestem świadomy istnienia zerowego operatora koalescencji. Nie robi tego, czego chce Darth; Chce operatora dostępu do elementu zniesionego do wartości null. (A tak przy okazji, twój poprzedni komentarz podaje niepoprawną charakterystykę zerowego operatora koalescencji. Chodziło Ci o to, że "v = m == null? Y: m. Wartość można zapisać v = m ?? y".)
Eric Lippert

4
Dla nowszych czytelników: C # 6.0 implementuje?., Więc x? .Y? .Z? .ToString () zwróci wartość null, jeśli x, y lub z są puste, lub zwróci wartość z.ToString (), jeśli żadna z nich nie jest równa null.
David

Odpowiedzi:


162

Począwszy od C # 6, możesz po prostu użyć:

MyEvent?.Invoke();

lub:

obj?.SomeMethod();

?.Jest operatorem null rozmnożeniowego, oraz powoduje .Invoke()się zwarcie przy operandzienull . Dostęp do operandu jest możliwy tylko raz, więc nie ma ryzyka wystąpienia problemu „zmiany wartości między sprawdzaniem a wywołaniem”.

===

Przed C # 6 nie: nie ma magii zerowej, z jednym wyjątkiem; metody rozszerzające - na przykład:

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

teraz to jest ważne:

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

W przypadku wydarzeń ma to tę zaletę, że usuwa również warunek wyścigu, tj. Nie potrzebujesz zmiennej tymczasowej. Więc normalnie potrzebujesz:

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

ale z:

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

możemy użyć po prostu:

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

1
Nie mam co do tego konfliktu. W przypadku procedury obsługi akcji lub zdarzenia - która jest zwykle bardziej niezależna - ma to pewien sens. Nie przerwałbym jednak hermetyzacji dla zwykłej metody. Oznacza to utworzenie metody w oddzielnej, statycznej klasie i nie sądzę, aby utrata enkapsulacji i ogólna degradacja czytelności / organizacji kodu była warta niewielkiej poprawy lokalnej czytelności
tvanfosson

@tvanfosson - rzeczywiście; ale chodzi mi o to, że jest to jedyny znany mi przypadek, w którym to zadziała. A samo pytanie porusza temat delegatów / wydarzeń.
Marc Gravell

Ten kod kończy się generowaniem gdzieś anonimowej metody, co naprawdę psuje ślad stosu każdego wyjątku. Czy można nadać metodom anonimowe nazwy? ;)
siostra

Źle jak na 2015 / C # 6.0 ...? czy on jest solencją. ReferenceTHatMayBeNull? .CallMethod () nie wywoła metody, gdy ma wartość null.
TomTom

1
@mercu powinno to być ?.- w wersji VB14 i nowszych
Marc Gravell

27

Co szukasz jest Null warunkowe (nie „koalescencyjny”) operator: ?.. Jest dostępny od C # 6.

Twój przykład to obj?.SomeMethod();. Jeśli obiekt ma wartość null, nic się nie dzieje. Gdy metoda ma argumenty, np obj?.SomeMethod(new Foo(), GetBar());. Argumenty nie są obliczane, jeśli objjest zerowe, co ma znaczenie, jeśli ocena argumentów miałaby skutki uboczne.

Łańcuch jest możliwy: myObject?.Items?[0]?.DoSomething()


1
To jest fantastyczne. Warto zauważyć, że jest to funkcja C # 6 ... (co byłoby sugerowane z twojego oświadczenia VS2015, ale nadal warte odnotowania). :)
Kyle Goode

10

Szybka metoda rozszerzenia:

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

przykład:

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

lub alternatywnie:

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

przykład:

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

Próbowałem to zrobić za pomocą metod rozszerzających i otrzymałem prawie ten sam kod. Jednak problem z tą implementacją polega na tym, że nie będzie działać z wyrażeniami, które zwracają typ wartości. Musiałem więc mieć na to drugą metodę.
orad

4

Zdarzenia można zainicjować z pustym domyślnym delegatem, który nigdy nie jest usuwany:

public event EventHandler MyEvent = delegate { };

Nie jest konieczne sprawdzanie wartości zerowej.

[ Aktualizacja , dzięki Bevan za wskazanie tego]

Pamiętaj jednak o możliwym wpływie na wydajność. Szybki mikro-test porównawczy, który przeprowadziłem, wskazuje, że obsługa zdarzenia bez subskrybentów jest 2-3 razy wolniejsza przy użyciu wzorca „domyślny delegat”. (Na moim dwurdzeniowym laptopie 2,5 GHz oznacza to 279 ms: 785 ms na zebranie 50 milionów niezasubskrybowanych wydarzeń). W przypadku punktów aktywnych aplikacji może to być kwestia do rozważenia.


1
Więc unikasz sprawdzania wartości null, wywołując pustego delegata ... wymierne poświęcenie zarówno pamięci, jak i czasu, aby zaoszczędzić kilka naciśnięć klawiszy? YMMV, ale dla mnie kiepski kompromis.
Bevan

Widziałem również testy porównawcze, które pokazują, że wywołanie wydarzenia z wieloma delegatami zasubskrybowanymi przez wielu delegatów jest znacznie droższe niż z jednym.
Greg D


2

Ten artykuł Iana Griffithsa podaje dwa różne rozwiązania problemu, które, jak stwierdza, są zgrabnymi sztuczkami, których nie powinieneś używać.


3
A mówiąc jako autor tego artykułu, dodam, że zdecydowanie nie powinieneś go używać teraz, gdy C # 6 rozwiązuje to po wyjęciu z pudełka za pomocą zerowych operatorów warunkowych (?. I. []).
Ian Griffiths

2

Metoda przedłużania ceracji, taka jak ta sugerowana, nie rozwiązuje problemów związanych z warunkami wyścigu, ale raczej je ukrywa.

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

Jak stwierdzono, ten kod jest eleganckim odpowiednikiem rozwiązania z tymczasową zmienną, ale ...

Problem z obojgiem polega na tym, że możliwe jest wywołanie abonenta wydarzenia PO wypisaniu się z wydarzenia . Jest to możliwe, ponieważ anulowanie subskrypcji może nastąpić po skopiowaniu wystąpienia delegata do zmiennej tymczasowej (lub przekazaniu jako parametru w powyższej metodzie), ale przed wywołaniem delegata.

Generalnie zachowanie kodu klienta jest w takim przypadku nieprzewidywalne: stan komponentu nie mógł już pozwolić na obsługę powiadomienia o zdarzeniu. Możliwe jest napisanie kodu klienta w taki sposób, aby go obsłużyć, ale spowodowałoby to niepotrzebną odpowiedzialność po stronie klienta.

Jedynym znanym sposobem zapewnienia bezpieczeństwa wątków jest użycie instrukcji lock dla nadawcy zdarzenia. Gwarantuje to, że wszystkie subskrypcje \ unsubscriptions \ invocation są serializowane.

Aby być dokładniejszym, blokada powinna być zastosowana do tego samego obiektu synchronizacji, który jest używany w metodach dostępu do zdarzenia add \ remove, czyli domyślnie „this”.


To nie jest sytuacja wyścigu, o której mowa. Warunek wyścigu to if (MyEvent! = Null) // MyEvent nie jest teraz zerowe MyEvent.Invoke (); // MyEvent jest teraz zerowe, zdarzają się złe rzeczy Więc przy tym stanie wyścigu nie mogę napisać procedury obsługi zdarzeń asynchronicznych, która gwarantuje działanie. Jednak z twoim „stanem wyścigu” mogę napisać program obsługi zdarzeń asynchronicznych, który gwarantuje działanie. Oczywiście połączenia do abonenta i abonenta muszą być synchroniczne lub wymagany jest tam kod blokady.
jyoung

W rzeczy samej. Moja analiza tego problemu została opublikowana tutaj: blogs.msdn.com/ericlippert/archive/2009/04/29/…
Eric Lippert


1

Może nie lepiej, ale moim zdaniem bardziej czytelne jest stworzenie metody rozszerzającej

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

1
co to return obj == nullznaczy. Co zwróci
Hammad Khan

1
To znaczy, że jeśli objtak , to myślę, że nullmetoda wróci true.
Joel

1
Jak to w ogóle odpowiada na pytanie?
Kugel

Późna odpowiedź, ale czy na pewno to zadziała? Czy możesz wywołać metodę rozszerzenia na obiekcie, który jest null? Jestem prawie pewien, że to nie działa z własnymi metodami typu, więc wątpię, czy zadziałałoby również z metodami rozszerzającymi. Uważam, że najlepszym sposobem, aby sprawdzić, czy dany przedmiot jest null to obj is null. Niestety, aby sprawdzić, czy obiekt nie jest nie nullwymaga owijania w nawiasach, co jest niekorzystne.
natiiix

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.