Dlaczego C # pozwala na „rzucanie null”?

85

Podczas pisania jakiegoś szczególnie złożonego kodu obsługi wyjątków ktoś zapytał, czy nie musisz się upewnić, że obiekt wyjątku nie jest null? Odpowiedziałem, że oczywiście nie, ale zdecydowałem się spróbować. Najwyraźniej możesz zgłosić null, ale nadal jest gdzieś zamieniony w wyjątek.

Dlaczego jest to dozwolone?

throw null;

W tym fragmencie na szczęście „ex” nie jest zerowe, ale czy kiedykolwiek mogło tak być?

try
{
  throw null;
}
catch (Exception ex)
{
  //can ex ever be null?

  //thankfully, it isn't null, but is
  //ex is System.NullReferenceException
}
cięty
źródło
8
Czy na pewno nie widzisz wyjątku .NET (odwołanie o wartości NULL) do własnego rzutu?
micahtan
4
Można by pomyśleć, że kompilator przynajmniej zgłosi ostrzeżenie o próbie bezpośredniego „wyrzucenia wartości null”;
Andy White
Nie, nie jestem pewien. Framework może bardzo dobrze próbować zrobić coś z moim obiektem, a kiedy jest pusty, framework zgłasza „Wyjątek zerowego odwołania” .. ale ostatecznie chcę mieć pewność, że „ex” nigdy nie może być zerowe
quip
1
@Andy: Ostrzeżenie może mieć sens w innych sytuacjach w języku, ale w throwprzypadku instrukcji, której celem jest przede wszystkim zgłoszenie wyjątku, ostrzeżenie nie dodaje dużej wartości.
Mehrdad Afshari
@Mehrdad - tak, ale wątpię, czy jakikolwiek programista celowo „rzuciłby null” i chciałby zobaczyć wyjątek NullReferenceException. Może powinien to być pełny błąd kompilacji, na przykład nieprawidłowe porównanie = w instrukcji if.
Andy White

Odpowiedzi:

96

Ponieważ specyfikacja języka oczekuje tam wyrażenia typu System.Exception(w związku z tym nulljest poprawna w tym kontekście) i nie ogranicza tego wyrażenia do wartości innej niż null. Ogólnie rzecz biorąc, nie ma możliwości wykrycia, czy wartość tego wyrażenia jest, nullczy nie. Musiałby rozwiązać problem zatrzymania. Środowisko wykonawcze i tak będzie musiało zająć się nullsprawą. Widzieć:

Exception ex = null;
if (conditionThatDependsOnSomeInput) 
    ex = new Exception();
throw ex; 

Mogliby oczywiście uczynić konkretny przypadek odrzucenia nulldosłownego nieważnym, ale to niewiele by pomogło, więc po co marnować miejsce na specyfikację i zmniejszać spójność z niewielkimi korzyściami?

Zastrzeżenie (zanim zostanie uderzony przez Erica Lipperta): To jest moja własna spekulacja na temat uzasadnienia tej decyzji projektowej. Oczywiście nie byłem na spotkaniu projektowym;)


Odpowiedź na drugie pytanie, czy zmienna wyrażenia przechwycona w klauzuli catch może kiedykolwiek mieć wartość null: Podczas gdy specyfikacja C # milczy na temat tego, czy inne języki mogą powodować nullpropagowanie wyjątku, definiuje sposób propagowania wyjątków:

Klauzule catch, jeśli istnieją, są sprawdzane w kolejności pojawiania się, aby zlokalizować odpowiednią procedurę obsługi wyjątku. Pierwsza klauzula catch, która określa typ wyjątku lub typ podstawowy typu wyjątku, jest uważana za dopasowanie. Ogólna klauzula catch jest uważana za dopasowanie dla dowolnego typu wyjątku. […]

Bo nullpogrubione stwierdzenie jest fałszywe. Tak więc, chociaż opiera się wyłącznie na tym, co mówi specyfikacja C #, nie możemy powiedzieć, że bazowe środowisko wykonawcze nigdy nie zwróci wartości null, możemy być pewni, że nawet jeśli tak jest, będzie to obsługiwane tylko przez catch {}klauzulę ogólną .

W przypadku implementacji języka C # w interfejsie CLI możemy odwołać się do specyfikacji ECMA 335. Ten dokument definiuje wszystkie wyjątki, które CLI zgłasza wewnętrznie (z których żaden nie jest null) i wspomina, że ​​obiekty wyjątków zdefiniowane przez użytkownika są generowane przez throwinstrukcję. Opis tej instrukcji jest praktycznie identyczny z throwinstrukcją C # (z wyjątkiem tego, że nie ogranicza typu obiektu do System.Exception):

Opis:

throwInstrukcja zgłasza wyjątek (typ obiektu O) w stos i opróżnia stos. Aby uzyskać szczegółowe informacje na temat mechanizmu wyjątków, patrz Partycja I.
[Uwaga: Podczas gdy CLI pozwala na wyrzucenie dowolnego obiektu, CLS opisuje konkretną klasę wyjątku, która powinna być używana do współdziałania języków. notatka końcowa]

Wyjątki:

System.NullReferenceExceptionjest wyrzucany, jeśli objjest null.

Poprawność:

Prawidłowy CIL zapewnia, że ​​obiekt jest zawsze albo nullodwołaniem do obiektu (tj. Typu O).

Uważam, że są one wystarczające, aby stwierdzić, że wychwycone wyjątki nigdy nie są null.

Mehrdad Afshari
źródło
Przypuszczam, że najlepszym rozwiązaniem byłoby zakazanie wyraźnych wyrażeń, takich jak throw null;.
FrustratedWithFormsDesigner
7
W rzeczywistości jest całkowicie możliwe (w środowisku CLR) wyrzucenie obiektu, który nie dziedziczy po System.Exception. O ile wiem, nie można tego zrobić w języku C #, ale można to zrobić za pomocą języka IL, C ++ / CLR itp. Więcej informacji można znaleźć na stronie msdn.microsoft.com/en-us/library/ms404228.aspx .
technophile
@technophile: Tak. Mówię tutaj o języku. W C # nie można zgłosić wyjątku innych typów, ale można go przechwycić przy użyciu catch { }klauzuli ogólnej .
Mehrdad Afshari
1
Chociaż nie miałoby sensu, aby kompilator odmówił zaakceptowania throwargumentu, którego argument może być wartością zerową, nie oznacza to, że throw nullmusiałby być legalny. Kompilator może nalegać, aby throwargument miał rozpoznawalny typ klasy. Wyrażenie takie jak (System.InvalidOperationException)nullpowinno być poprawne w czasie kompilacji (wykonanie powinno spowodować a NullReferenceException), ale nie oznacza to, że nietypowe nullpowinno być dopuszczalne.
supercat
@supercat: to prawda, ale wartość null w tym przypadku jest niejawnie wpisywana jako wyjątek (ponieważ jest używana w kontekście, w którym wyjątek jest wymagany). wartość null nie jest poprawna w czasie wykonywania, więc zgłaszany jest wyjątek NullReferenceException (jak stwierdzono w odpowiedzi Anon.). Zgłoszony wyjątek nie jest tym w instrukcji „throw”.
John B. Lambe
29

Najwyraźniej możesz zgłosić null, ale nadal jest gdzieś zamieniony w wyjątek.

Próba rzucenia nullobiektu powoduje (całkowicie niepowiązany) wyjątek odwołania zerowego.

Pytanie, dlaczego możesz rzucać, nulljest jak pytanie, dlaczego możesz to zrobić:

object o = null;
o.ToString();
Zaraz.
źródło
5

Zaczerpnięte stąd :

Jeśli użyjesz tego wyrażenia w kodzie C #, spowoduje to zgłoszenie wyjątku NullReferenceException. Dzieje się tak, ponieważ instrukcja rzucania wymaga obiektu typu Exception jako pojedynczego parametru. Ale ten sam obiekt jest pusty w moim przykładzie.

Fitzchak Yitzchaki
źródło
5

Chociaż może nie być możliwe zgłoszenie wartości null w C #, ponieważ rzut wykryje to i przekształci go w wyjątek NullReferenceException, JEST możliwe, aby otrzymać wartość null ... Tak się składa, że ​​otrzymuję to teraz, co powoduje mój catch (który nie był Oczekiwanie, że `` ex '' ma wartość null), aby doświadczyć zerowego wyjątku odniesienia, który następnie powoduje śmierć mojej aplikacji (ponieważ był to ostatni przechwycenie).

Tak więc, chociaż nie możemy wyrzucić null z C #, zaświat może zgłosić wartość null, więc twój najbardziej zewnętrzny catch (Exception ex) lepiej przygotuj się na jego otrzymanie. Po prostu FYI.

Brian Kennedy
źródło
3
Ciekawy. Czy mógłbyś opublikować jakiś kod, a może uniknąć tego, co jest w twojej głębi poniżej, co to robi?
Peter Lillevold
Nie mogę ci powiedzieć, czy jest to błąd CLR ... trudno jest wyśledzić, co w wnętrzności .NET wyrzuciło null i dlaczego, biorąc pod uwagę, że nie dostajesz wyjątku ze stosem wywołań lub innymi wskazówkami do kontynuowania. Wiem tylko, że mój najbardziej zewnętrzny catch sprawdza teraz zerowy argument Exception przed jego użyciem.
Brian Kennedy
3
Podejrzewam, że jest to błąd CLR lub z powodu złego, nieweryfikowalnego kodu. Prawidłowy CIL wyrzuci tylko obiekt inny niż null lub null (który stanie się NullReferenceException).
Demi
Używam również usługi, która zwraca zerowy wyjątek. Czy kiedykolwiek zorientowałeś się, w jaki sposób wyrzucany był zerowy wyjątek?
themiDdlest
2

Myślę, że może nie możesz - kiedy próbujesz rzucić null, nie może, więc robi to, co powinien w przypadku błędu, czyli rzuca zerowy wyjątek odwołania. Więc tak naprawdę nie wyrzucasz null, nie możesz wyrzucić null, co powoduje rzut.

Steve Cooper
źródło
2

Próba odpowiedzi „… na szczęście„ ex ”nie jest zerowa, ale czy kiedykolwiek mogłaby tak być?”:

Ponieważ prawdopodobnie nie możemy zgłosić wyjątków, które są zerowe, klauzula catch również nigdy nie będzie musiała przechwytywać wyjątku, który jest pusty. Tak więc ex nigdy nie może być zerowy.

Teraz widzę, że to pytanie faktycznie zostało już zadane .

Peter Lillevold
źródło
@Brian Kennedy mówi nam w swoim komentarzu powyżej, że możemy złapać null.
ProfK,
@ Tvde1 - Myślę, że różnica polega na tym, że tak, możesz wykonać throw null. Ale to nie spowoduje zgłoszenia wyjątku, który jest zerowy . Zamiast tego, ponieważ throw nullpróbuje wywołać metody w odwołaniu o wartości null, zmusza to następnie środowisko uruchomieniowe do wyrzucenia niezerowej instancji NullReferenceException.
Peter Lillevold
1

Pamiętaj, że wyjątek zawiera szczegółowe informacje o tym, gdzie został zgłoszony. Biorąc pod uwagę, że konstruktor nie ma pojęcia, gdzie ma zostać rzucony, ma sens tylko to, że metoda rzucania wprowadza te szczegóły do ​​obiektu w miejscu, w którym znajduje się rzut. Innymi słowy, środowisko CLR próbuje wstrzyknąć dane do wartości null, co wyzwala NullReferenceException.

Nie jestem pewien, czy tak właśnie się dzieje, ale wyjaśnia to zjawisko.

Zakładając, że to prawda (i nie mogę wymyślić lepszego sposobu, aby ex był zerowy, niż rzucanie null;), oznaczałoby to, że ex nie może być zerowy.

Guvante
źródło
0

W starszym C #:

Rozważ tę składnię:

public void Add<T> ( T item ) => throw (hashSet.Add ( item ) ? null : new Exception ( "The item already exists" ));

Myślę, że jest o wiele krótszy niż to:

public void Add<T> ( T item )
{
    if (!hashSet.Add ( item ))
        throw new Exception ( "The item already exists" );
}
Arutyun Enfendzhyan
źródło
Co nam to mówi?
bornfromanegg
Nawiasem mówiąc, nie jestem pewien, jaka jest twoja definicja krótszego, ale twój drugi przykład zawiera mniej znaków.
bornfromanegg
@bornfromanegg no the 1st jest inline, więc jest zdecydowanie krótszy. Próbowałem powiedzieć, że możesz zgłosić błąd w zależności od stanu. Tradycyjnie chciałbyś użyć drugiego przykładu, ale ponieważ „rzucanie null” niczego nie rzuca, możesz wstawić drugi przykład, aby wyglądał jak pierwszy przykład. Również pierwszy przykład ma 8 znaków mniej niż drugi
Arutyun Enfendzhyan
„Throw null” zgłasza wyjątek NullReference. Co oznacza, że ​​Twój pierwszy przykład zawsze zgłosi wyjątek.
bornfromanegg
tak, niestety teraz tak. Ale tak się nie stało wcześniej
Arutyun Enfendzhyan