Co w C # dzieje się, gdy wywołasz metodę rozszerzenia obiektu zerowego?

328

Czy metoda jest wywoływana z wartością zerową, czy też daje wyjątek odwołania zerowego?

MyObject myObject = null;
myObject.MyExtensionMethod(); // <-- is this a null reference exception?

Jeśli tak jest, nigdy nie będę musiał sprawdzać parametru „this” pod kątem wartości null?

tpower
źródło
O ile oczywiście nie masz do czynienia z ASP.NET MVC, który zgłosi ten błąd Cannot perform runtime binding on a null reference.
Mrchief

Odpowiedzi:

386

To zadziała dobrze (bez wyjątku). Metody rozszerzeń nie używają wywołań wirtualnych (tzn. Używa instrukcji iloczynu „call”, a nie „callvirt”), więc nie ma sprawdzania wartości zerowej, chyba że sam napiszesz ją w metodzie rozszerzenia. Jest to przydatne w kilku przypadkach:

public static bool IsNullOrEmpty(this string value)
{
    return string.IsNullOrEmpty(value);
}
public static void ThrowIfNull<T>(this T obj, string parameterName)
        where T : class
{
    if(obj == null) throw new ArgumentNullException(parameterName);
}

itp

Zasadniczo połączenia do połączeń statycznych są bardzo dosłowne - tj

string s = ...
if(s.IsNullOrEmpty()) {...}

staje się:

string s = ...
if(YourExtensionClass.IsNullOrEmpty(s)) {...}

gdzie oczywiście nie ma kontroli zerowej.

Marc Gravell
źródło
1
Marc, mówisz o połączeniach „wirtualnych” - ale to samo dotyczy wywołań niewirusowych metodami instancji. Myślę, że słowo „wirtualny” jest tutaj niewłaściwe.
Konrad Rudolph
6
@Konrad: To zależy od kontekstu. Kompilator w języku C # zwykle używa callvirt nawet w przypadku metod innych niż wirtualne, właśnie w celu uzyskania kontroli zerowej.
Jon Skeet
Miałem na myśli różnicę między instrukcjami call i callvirt il. W jednym wydaniu faktycznie próbowałem utworzyć dwie strony Opcodes, ale redaktor zablokował linki ...
Marc Gravell
2
Naprawdę nie rozumiem, w jaki sposób takie użycie metod rozszerzenia może być przydatne. To, że można to zrobić, nie oznacza, że ​​jest słuszne, a jak wspomniano poniżej Binary Worrier, wydaje mi się, że co najmniej jest aberracją.
Pułapka
3
@ Trap: ta funkcja jest świetna, jeśli lubisz programować w stylu funkcjonalnym.
Roy Tinker,
50

Dodatek do poprawnej odpowiedzi od Marc Gravell.

Możesz otrzymać ostrzeżenie z kompilatora, jeśli jest oczywiste, że ten argument jest pusty:

default(string).MyExtension();

Działa dobrze w czasie wykonywania, ale generuje ostrzeżenie "Expression will always cause a System.NullReferenceException, because the default value of string is null".

Stefan Steinegger
źródło
32
Dlaczego miałby to ostrzegać „zawsze powoduje wyjątek System.NullReferenceException”. Kiedy tak naprawdę nigdy nie będzie?
tpower
46
Na szczęście my, programiści, zależy nam tylko na błędach, a nie na ostrzeżeniach: p
JulianR
7
@JulianR: Tak, niektórzy tak, niektórzy nie. W naszej konfiguracji kompilacji wersji ostrzeżenia traktujemy jako błędy. Więc to po prostu nie działa.
Stefan Steinegger
8
Dzięki za notatkę; Wezmę to do bazy danych błędów i zobaczymy, czy możemy to naprawić w C # 4.0. (Bez obietnicy - ponieważ jest to nierealistyczna sprawa narożna i jedynie ostrzeżenie, możemy ją naprawić).
Eric Lippert
3
@Stefan: Ponieważ jest to błąd, a nie „prawdziwe” ostrzeżenie, możesz użyć instrukcji #pragma, aby ukryć ostrzeżenie, aby kod przeszedł kompilację wydania.
Martin RL,
24

Jak już odkryłeś, ponieważ metody rozszerzenia są po prostu gloryfikowanymi metodami statycznymi, będą one wywoływane z nullprzekazanymi referencjami, bez NullReferenceExceptionrzucania. Ale ponieważ dla wywołującego wyglądają one jak instancyjne metody, powinny również tak się zachowywać . Następnie przez większość czasu należy sprawdzić thisparametr i zgłosić wyjątek, jeśli jest null. Nie można tego robić, jeśli metoda wyraźnie dba o nullwartości, a jej nazwa wskazuje ją należycie, jak w poniższych przykładach:

public static class StringNullExtensions { 
  public static bool IsNullOrEmpty(this string s) { 
    return string.IsNullOrEmpty(s); 
  } 
  public static bool IsNullOrBlank(this string s) { 
    return s == null || s.Trim().Length == 0; 
  } 
}

Jakiś czas temu napisałem też o tym blogu .

Jordão
źródło
3
Zagłosowałem to, ponieważ jest to poprawne i ma dla mnie sens (i dobrze napisane), podczas gdy ja również wolę użycie opisane w odpowiedzi przez @Marc Gravell.
qxotk
17

Wartość pusta zostanie przekazana do metody rozszerzenia.

Jeśli metoda spróbuje uzyskać dostęp do obiektu bez sprawdzania, czy ma wartość NULL, wówczas tak, zgłosi wyjątek.

Facet napisał tutaj metody rozszerzeń „IsNull” i „IsNotNull”, które sprawdzają, czy odwołanie zostało przekazane null, czy nie. Osobiście uważam, że jest to aberracja i nie powinna była ujrzeć światła dziennego, ale jest to całkowicie poprawna wersja c #.

Binarny Worrier
źródło
18
Rzeczywiście, dla mnie to jak zadawanie zwłok „Czy żyjesz” i otrzymywanie odpowiedzi „nie”. Zwłoki nie potrafią odpowiedzieć na żadne pytanie, podobnie jak nie powinieneś być w stanie „wywołać” metody na obiekcie zerowym.
Binary Worrier
14
Nie zgadzam się z logiką Binary Worriera, ponieważ przydaje się możliwość wywoływania rozszerzeń bez martwienia się o zerowe odwołania, ale +1 dla analogicznej wartości komediowej :-)
Tim Abell
10
W rzeczywistości czasami nie wiesz, czy ktoś nie żyje, więc wciąż pytasz, a osoba może odpowiedzieć: „nie, po prostu odpoczywam z zamkniętymi oczami”
nurchi
7
Kiedy potrzebujesz połączyć kilka operacji (powiedzmy 3+), możesz (zakładając, że nie ma efektów ubocznych) przekształcić kilka wierszy nudnego kodu sprawdzania wartości zerowej w elegancko połączoną jednowarstwową z „bezpiecznymi zerami” metodami rozszerzenia. (Podobne do sugerowanego „.?” - operatora, ale wprawdzie nie tak eleganckie.) Jeśli nie jest oczywiste, rozszerzenie jest „zerowe”, zwykle poprzedzam metodę „bezpiecznym”, więc jeśli na przykład jest to kopia metoda może mieć nazwę „SafeCopy” i zwróci wartość null, jeśli argument będzie miał wartość null.
AnorZaken,
3
Śmiałem się tak mocno z odpowiedzią @BinaryWorrier hahahaha, widziałem siebie kopiącego ciało, aby sprawdzić, czy jest martwe, czy nie hahaha Tak więc w MOJEJ wyobraźni, który sprawdził, czy ciało było martwe, czy nie byłem mną, a nie samym ciałem, wdrożenie do czek był we mnie, aktywnie kopiąc go, by zobaczyć, czy się poruszy. Więc ciało nie wie, czy jest martwe, czy nie, WHO sprawdza, wie, teraz możesz argumentować, że możesz „podłączyć” do ciała sposób, aby powiedział ci, czy jest martwy czy nie, i moim zdaniem jest to, co rozszerzenie jest dla.
Zorkind,
7

Jak zauważyli inni, wywołanie metody rozszerzenia w odwołaniu null powoduje, że argument ten ma wartość null i nic innego się nie wydarzy. To rodzi pomysł na użycie metod rozszerzenia do napisania klauzul ochronnych.

Możesz przeczytać ten artykuł, aby uzyskać przykłady: Jak zmniejszyć złożoność cyklomatyczną: klauzula ochronna Krótka wersja to:

public static class StringExtensions
{
    public static void AssertNonEmpty(this string value, string paramName)
    {
        if (string.IsNullOrEmpty(value))
            throw new ArgumentException("Value must be a non-empty string.", paramName);
    }
}

Jest to metoda rozszerzenia klasy łańcuchowej, którą można wywołać w odwołaniu zerowym:

((string)null).AssertNonEmpty("null");

Wywołanie działa dobrze tylko dlatego, że środowisko wykonawcze z powodzeniem wywoła metodę rozszerzenia przy zerowym odwołaniu. Następnie możesz użyć tej metody rozszerzenia do implementacji klauzul ochronnych bez niechlujnej składni:

    public IRegisteredUser RegisterUser(string userName, string referrerName)
    {

        userName.AssertNonEmpty("userName");
        referrerName.AssertNonEmpty("referrerName");

        ...

    }
Zoran Horvat
źródło
3

Metoda rozszerzenia jest statyczna, więc jeśli nie podoba ci się ten obiekt MyObject, nie powinno to stanowić problemu, szybki test powinien to zweryfikować :)

Fredrik Leijon
źródło
-1

Jest kilka złotych zasad, kiedy chcesz, aby były czytelne i pionowe.

  • warto powiedzieć z Eiffela, że ​​konkretny kod zamknięty w metodzie powinien działać w przypadku niektórych danych wejściowych, że kod jest wykonalny, jeśli zostaną spełnione pewne warunki wstępne i zapewni oczekiwany wynik

W twoim przypadku - DesignByContract jest zepsuty ... będziesz wykonywać logikę na instancji zerowej.

Ruslander
źródło