Najlepsze rozwiązania: zgłaszanie wyjątków z właściwości

111

Kiedy należy zgłosić wyjątek z elementu pobierającego lub ustawiającego właściwość? Kiedy nie jest to właściwe? Czemu? Przydałyby się linki do zewnętrznych dokumentów na ten temat ... Google pojawił się zaskakująco mało.

Jon Seigel
źródło
Również powiązane: stackoverflow.com/questions/1488322/…
Brian Rasmussen
1
Przeczytałem oba te pytania, ale żadne z nich nie odpowiedziało całkowicie IMO.
Jon Seigel
W razie potrzeby. Zarówno poprzednie pytania, jak i odpowiedzi pokazują, że można i docenić zgłaszanie wyjątków od gettera lub settera, dzięki czemu można po prostu „być sprytnym”.
Lex Li

Odpowiedzi:

135

Microsoft ma swoje zalecenia dotyczące projektowania właściwości pod adresem http://msdn.microsoft.com/en-us/library/ms229006.aspx

Zasadniczo zalecają, aby metody pobierające właściwości były lekkimi akcesoriami, które zawsze można bezpiecznie wywołać. Zalecają przeprojektowanie metod pobierających, aby były metodami, jeśli wyjątki są czymś, co musisz wyrzucić. W przypadku ustawiaczy wskazują, że wyjątki są odpowiednią i akceptowalną strategią obsługi błędów.

W przypadku indeksatorów firma Microsoft wskazuje, że zarówno metody pobierające, jak i ustawiające mogą zgłaszać wyjątki. W rzeczywistości robi to wiele indeksatorów w bibliotece .NET. Najczęstszym wyjątkiem jest ArgumentOutOfRangeException.

Istnieje kilka całkiem dobrych powodów, dla których nie chcesz zgłaszać wyjątków w metodach pobierających właściwości:

  • Ponieważ właściwości „wydają się” być polami, nie zawsze jest oczywiste, że mogą zgłaszać (zgodnie z projektem) wyjątek; podczas gdy w przypadku metod programiści są szkoleni, aby oczekiwać i badać, czy wyjątki są oczekiwaną konsekwencją wywołania metody.
  • Metody pobierania są używane przez wiele infrastruktur .NET, takich jak serializatory i wiązania danych (na przykład w WinForms i WPF) - radzenie sobie z wyjątkami w takich kontekstach może szybko stać się problematyczne.
  • Metody pobierające właściwości są automatycznie oceniane przez debugery podczas oglądania lub sprawdzania obiektu. Wyjątek w tym miejscu może być mylący i spowolnić wysiłki związane z debugowaniem. Z tych samych powodów niepożądane jest również wykonywanie innych kosztownych operacji we właściwościach (takich jak dostęp do bazy danych).
  • Właściwości są często używane w konwencji łańcuchowej: obj.PropA.AnotherProp.YetAnother- przy tego rodzaju składni problematyczne staje się podjęcie decyzji, gdzie wstrzyknąć instrukcje przechwytywania wyjątków.

Na marginesie, należy być świadomym, że tylko dlatego, że właściwość nie jest zaprojektowana do zgłaszania wyjątku, nie oznacza to, że tak się nie stanie; może łatwo wywołać kod, który to robi. Nawet prosta czynność przydzielenia nowego obiektu (np. Ciągu znaków) może spowodować wyjątki. Powinieneś zawsze pisać swój kod defensywnie i oczekiwać wyjątków od wszystkiego, co wywołujesz.

LBushkin
źródło
41
Jeśli napotkasz fatalny wyjątek, taki jak „brak pamięci”, nie ma znaczenia, czy otrzymasz wyjątek w nieruchomości, czy w innym miejscu. Jeśli nie masz tego we właściwości, dostaniesz to kilka nanosekund później od następnej rzeczy, która przydziela pamięć. Pytanie nie brzmi: „czy właściwość może zgłosić wyjątek?” Prawie cały kod może zgłosić wyjątek z powodu stanu krytycznego. Pytanie brzmi, czy nieruchomość powinna z założenia zgłaszać wyjątek jako część swojej umowy szczegółowej.
Eric Lippert
1
Nie jestem pewien, czy rozumiem argument w tej odpowiedzi. Na przykład w odniesieniu do wiązania danych - zarówno WinForms, jak i WPF są specjalnie napisane, aby poprawnie obsługiwać wyjątki zgłaszane przez właściwości i traktować je jako błędy walidacji - co jest całkowicie dobrym (niektórzy nawet uważają, że jest to najlepszy) sposób na zapewnienie walidacji modelu domeny .
Pavel Minaev
6
@Pavel - chociaż zarówno WinForms, jak i WPF mogą z wdziękiem odzyskać dane po wyjątkach w metodach dostępu do właściwości, nie zawsze jest łatwo zidentyfikować i odzyskać takie błędy. W niektórych przypadkach (na przykład gdy w WPF ustawiacz szablonu formantu zgłasza wyjątek) wyjątek jest dyskretnie połykany. Może to prowadzić do bolesnych sesji debugowania, jeśli nigdy wcześniej nie spotkałeś się z takimi przypadkami.
LBushkin
1
@Steven: Więc jak bardzo przydała ci się ta lekcja w wyjątkowym przypadku? Jeśli wtedy musiałeś napisać kod obronny, który obsługiwałby wszystkie te wyjątki z powodu jednej awarii i prawdopodobnie zawierał odpowiednie wartości domyślne, dlaczego nie wprowadzić tych wartości domyślnych w swoim catch? Ewentualnie, jeśli wyjątki właściwości są zgłaszane do użytkownika, dlaczego po prostu nie zgłosić oryginalnego „InvalidArgumentException” lub podobnego, aby dostarczyć brakujący plik ustawień?
Zhaph - Ben Duguid
6
Jest powód, dla którego są to wytyczne, a nie zasady; żadna wytyczna nie obejmuje wszystkich szalonych skrajnych przypadków. Prawdopodobnie sam stworzyłbym te metody, a nie właściwości, ale to wezwanie do oceny.
Eric Lippert
34

Nie ma nic złego w rzucaniu wyjątków od seterów. W końcu jaki jest lepszy sposób na wskazanie, że wartość nie jest poprawna dla danej właściwości?

W przypadku metod pobierających jest to generalnie źle widziane i można to dość łatwo wyjaśnić: ogólnie rzecz biorąc, metoda pobierająca właściwości informuje o aktualnym stanie obiektu; w związku z tym jedynym przypadkiem, w którym metoda pobierająca jest uzasadniona, jest sytuacja, gdy stan jest nieważny. Ale ogólnie uważa się również za dobry pomysł, aby zaprojektować swoje klasy w taki sposób, że po prostu nie jest możliwe pobranie początkowo nieprawidłowego obiektu lub wprowadzenie go w stan nieprawidłowy za pomocą normalnych środków (tj. Zawsze należy zapewnić pełną inicjalizację w konstruktorach i spróbuj uczynić metody bezpiecznymi dla wyjątków w odniesieniu do ważności stanu i niezmienników klas). Tak długo, jak trzymasz się tej zasady, twoje pobierające właściwości nigdy nie powinny znaleźć się w sytuacji, w której musiałyby zgłosić nieprawidłowy stan, a zatem nigdy nie rzucać.

Jest jeden wyjątek, o którym wiem, a właściwie jest to raczej poważny: implementacja dowolnego obiektu IDisposable. Disposejest specjalnie pomyślany jako sposób na doprowadzenie obiektu do nieprawidłowego stanu i istnieje nawet specjalna klasa wyjątku ObjectDisposedException, która ma być używana w tym przypadku. Wyrzucanie ObjectDisposedExceptionz dowolnego elementu członkowskiego klasy, w tym metody pobierającej właściwości (i Disposesamego siebie), jest całkowicie normalne po usunięciu obiektu.

Pavel Minaev
źródło
4
Dzięki Pavel. Ta odpowiedź brzmi „dlaczego”, zamiast po prostu powtarzać, że nie jest dobrym pomysłem rzucanie wyjątku od właściwości.
SolutionYogi
1
Nie podoba mi się pogląd, że absolutnie wszyscy członkowie a IDisposablepowinni stać się bezużyteczni po a Dispose. Jeśli wywołanie członka wymagałoby użycia zasobu, który Disposestał się niedostępny (np. Członek odczytywałby dane ze strumienia, który został zamknięty), członek powinien ObjectDisposedExceptionraczej rzucić niż przeciekać, np. ArgumentExceptionJeśli ma się formularz z właściwościami, które reprezentują wartości w niektórych polach, o wiele bardziej pomocne wydaje się zezwolenie na odczytanie takich właściwości po ich usunięciu (w celu uzyskania ostatnio wpisanych wartości) niż wymaganie ...
superkat.
1
... które Disposezostaną odroczone do czasu odczytania wszystkich takich właściwości. W niektórych przypadkach, gdy jeden wątek może używać blokujących odczytów obiektu, podczas gdy inny zamyka go, a dane mogą pojawić się w dowolnym momencie wcześniej Dispose, pomocne może być Disposeodcięcie danych przychodzących, ale zezwolenie na odczytanie wcześniej odebranych danych. Nie należy wymuszać sztucznego rozróżnienia między Closei Disposew sytuacjach, w których w innym przypadku żadne nie musiałoby istnieć.
supercat
Zrozumienie powodu reguły pozwala wiedzieć, kiedy należy ją złamać (Raymond Chen). W tym przypadku widzimy, że jeśli wystąpi nieodwracalny błąd dowolnego typu, nie należy go ukrywać w getterze, ponieważ w takich przypadkach aplikacja musi zakończyć się jak najszybciej.
Ben
Chodziło mi o to, że metody pobierające właściwości generalnie nie powinny zawierać logiki, która pozwalałaby na nieodwracalne błędy. Jeśli tak, być może lepiej będzie jako Get...metoda. Wyjątkiem jest sytuacja, gdy musisz zaimplementować istniejący interfejs, który wymaga podania właściwości.
Pavel Minaev
24

Prawie nigdy nie jest to odpowiednie dla gettera, a czasami odpowiednie dla setera.

Najlepszym źródłem odpowiedzi na tego typu pytania są „Wytyczne dotyczące projektowania ram” autorstwa Cwalina i Abrams; jest dostępna jako oprawiona książka, a duże jej części są również dostępne online.

Z sekcji 5.2: Projekt nieruchomości

UNIKAJ rzucania wyjątków z metod pobierających właściwości. Metody pobierające właściwości powinny być prostymi operacjami i nie powinny mieć warunków wstępnych. Jeśli metoda pobierająca może zgłosić wyjątek, prawdopodobnie powinna zostać przeprojektowana, aby była metodą. Zwróć uwagę, że ta reguła nie dotyczy indeksatorów, w przypadku których oczekujemy wyjątków w wyniku walidacji argumentów.

Należy zauważyć, że ta wskazówka dotyczy tylko metod pobierających właściwości. Można zgłosić wyjątek w programie ustawiającym właściwości.

Eric Lippert
źródło
2
Chociaż (ogólnie) zgadzam się z takimi wytycznymi, uważam, że warto przedstawić dodatkowe informacje na temat tego, dlaczego należy ich przestrzegać - i jakie konsekwencje mogą wyniknąć z ich zignorowania.
LBushkin
3
Jak to się ma do przedmiotów jednorazowego użytku i wskazówek, które powinieneś rozważyć wyrzucenie ObjectDisposedExceptionpo Dispose()wywołaniu obiektu, a następnie coś poprosi o wartość właściwości? Wydaje się, że wskazówką powinno być „unikaj wyrzucania wyjątków z metod pobierających właściwości, chyba że obiekt został usunięty, w takim przypadku należy rozważyć zgłoszenie obiektu ObjectDisposedExcpetion”.
Scott Dorman
4
Projektowanie to sztuka i nauka znajdowania rozsądnych kompromisów w obliczu sprzecznych wymagań. Tak czy inaczej wydaje się rozsądnym kompromisem; Nie zdziwiłbym się, gdyby usunięty przedmiot rzucił się na nieruchomość; nie zdziwiłbym się, gdyby tak się nie stało. Ponieważ używanie usuniętego obiektu jest okropną praktyką programistyczną, nierozsądne byłoby mieć jakiekolwiek oczekiwania.
Eric Lippert
1
Innym scenariuszem, w którym rzucanie wyjątków z wewnątrz pobierających jest całkowicie prawidłowe, jest sytuacja, gdy obiekt używa niezmienników klas do walidacji swojego stanu wewnętrznego, który należy sprawdzić za każdym razem, gdy następuje dostęp publiczny, bez względu na to, czy jest to metoda czy właściwość
Pułapka
2

Jednym fajnym podejściem do wyjątków jest użycie ich do dokumentowania kodu dla siebie i innych programistów w następujący sposób:

Wyjątki powinny dotyczyć wyjątkowych stanów programu. Oznacza to, że możesz je pisać w dowolnym miejscu!

Jednym z powodów, dla których możesz chcieć umieścić je w getterach, jest udokumentowanie API klasy - jeśli oprogramowanie zgłosi wyjątek, gdy tylko programista spróbuje go źle użyć, nie użyje go źle! Na przykład, jeśli masz walidację podczas procesu odczytu danych, kontynuowanie i uzyskiwanie dostępu do wyników procesu może nie mieć sensu, jeśli w danych wystąpiły krytyczne błędy. W takim przypadku możesz chcieć wykonać rzut wyjściowy, jeśli wystąpiły błędy, aby upewnić się, że inny programista sprawdzi ten warunek.

Są sposobem dokumentowania założeń i granic podsystemu / metody / czegokolwiek. W ogólnym przypadku nie należy ich łapać! Dzieje się tak również dlatego, że nigdy nie są wyrzucane, jeśli system współpracuje w oczekiwany sposób: Jeśli zdarzy się wyjątek, oznacza to, że założenia fragmentu kodu nie są spełnione - np. Nie wchodzi w interakcję z otaczającym go światem pierwotnie był do tego przeznaczony. Jeśli złapiesz wyjątek, który został napisany w tym celu, prawdopodobnie oznacza to, że system wszedł w nieprzewidywalny / niespójny stan - może to ostatecznie doprowadzić do awarii lub uszkodzenia danych lub czegoś podobnego, co prawdopodobnie będzie znacznie trudniejsze do wykrycia / debugowania.

Komunikaty o wyjątkach to bardzo zgrubny sposób zgłaszania błędów - nie można ich gromadzić masowo i zawierają tylko ciąg znaków. To sprawia, że ​​nie nadają się do zgłaszania problemów z danymi wejściowymi. Podczas normalnego działania sam system nie powinien przechodzić w stan błędu. W związku z tym zawarte w nich komunikaty powinny być przeznaczone dla programistów, a nie dla użytkowników - błędy w danych wejściowych można wykryć i przekazać użytkownikom w bardziej odpowiednich (niestandardowych) formatach.

Wyjątkiem (haha!) Od tej reguły są rzeczy takie jak IO, gdzie wyjątki nie są pod twoją kontrolą i nie można ich sprawdzić z wyprzedzeniem.

JonnyRaa
źródło
2
W jaki sposób ta ważna i trafna odpowiedź została odrzucona? W StackOverflow nie powinno być polityki, a jeśli ta odpowiedź wydawała się nie trafić w dziesiątkę, dodaj komentarz na ten temat. Głosowanie przeciwne dotyczy odpowiedzi, które są nieistotne lub błędne.
debata
1

Wszystko to jest udokumentowane w MSDN (jak podano w innych odpowiedziach), ale oto ogólna zasada ...

W seterze, jeśli twoja właściwość powinna być zweryfikowana powyżej i poza typem. Na przykład właściwość o nazwie PhoneNumber powinna prawdopodobnie mieć walidację wyrażenia regularnego i powinna zgłaszać błąd, jeśli format jest nieprawidłowy.

W przypadku metod pobierających, prawdopodobnie gdy wartość jest null, ale najprawdopodobniej będzie to coś, co będziesz chciał obsłużyć w kodzie wywołującym (zgodnie ze wskazówkami projektowymi).

David Stratton
źródło
0

To jest bardzo złożone pytanie, a odpowiedź zależy od tego, jak używany jest twój obiekt. Zasadniczo metody pobierające i ustawiające właściwości, które są „późnym wiązaniem” nie powinny zgłaszać wyjątków, natomiast właściwości z wyłącznie „wczesnym wiązaniem” powinny zgłaszać wyjątki, gdy zajdzie taka potrzeba. Przy okazji, narzędzie do analizy kodu Microsoftu, moim zdaniem, definiuje użycie właściwości zbyt wąsko.

„późne wiązanie” oznacza, że ​​właściwości są odnajdywane poprzez odbicie. Na przykład atrybut Serializeable "służy do serializacji / deserializacji obiektu za pomocą jego właściwości. Zgłoszenie wyjątku w tego rodzaju sytuacji powoduje katastrofalne uszkodzenie i nie jest dobrym sposobem używania wyjątków do tworzenia bardziej niezawodnego kodu.

„Wczesne wiązanie” oznacza, że ​​użycie właściwości jest wiązane w kodzie przez kompilator. Na przykład, gdy napisany kod odwołuje się do metody pobierającej właściwość. W takim przypadku można zgłaszać wyjątki, gdy mają one sens.

Obiekt z atrybutami wewnętrznymi ma stan określony przez wartości tych atrybutów. Właściwości wyrażające atrybuty, które są świadome i wrażliwe na stan wewnętrzny obiektu, nie powinny być używane do późnego wiązania. Na przykład, powiedzmy, że masz obiekt, który należy otworzyć, uzyskać do niego dostęp, a następnie zamknąć. W takim przypadku uzyskanie dostępu do właściwości bez wcześniejszego wywołania open powinno spowodować wyjątek. Załóżmy, że w tym przypadku nie zgłosimy wyjątku i pozwolimy kodowi na dostęp do wartości bez zgłaszania wyjątku? Kod będzie wydawał się szczęśliwy, mimo że otrzymał wartość z metody pobierającej, która jest bezsensowna. Teraz umieściliśmy kod, który wywołał metodę pobierającą w złej sytuacji, ponieważ musi wiedzieć, jak sprawdzić wartość, aby zobaczyć, czy nie ma ona sensu. Oznacza to, że kod musi przyjąć założenia dotyczące wartości, jaką uzyskał z metody pobierającej właściwość, aby ją zweryfikować. W ten sposób powstaje zły kod.

Jack D Menendez
źródło
0

Miałem ten kod, w którym nie byłem pewien, który wyjątek rzucić.

public Person
{
    public string Name { get; set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    if (person.Name == null) {
        throw new Exception("Name of person is null.");
        // I was unsure of which exception to throw here.
    }

    Console.WriteLine("Name is: " + person.Name);
}

Zapobiegałem, aby model miał właściwość zerową w pierwszej kolejności, wymuszając ją jako argument w konstruktorze.

public Person
{
    public Person(string name)
    {
        if (name == null) {
            throw new ArgumentNullException(nameof(name));
        }
        Name = name;
    }

    public string Name { get; private set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    Console.WriteLine("Name is: " + person.Name);
}
Fred
źródło