Czasami czasami muszę napisać metodę lub właściwość dla biblioteki klas, dla której nie jest niczym wyjątkowym brak prawdziwej odpowiedzi, ale niepowodzenie. Coś nie może zostać określone, nie jest dostępne, nie znaleziono, nie jest obecnie możliwe lub nie ma już dostępnych danych.
Myślę, że istnieją trzy możliwe rozwiązania dla tak stosunkowo nietypowej sytuacji, wskazujące na błąd w C # 4:
- zwraca magiczną wartość, która nie ma żadnego znaczenia w inny sposób (np.
null
i-1
); - zgłosić wyjątek (np.
KeyNotFoundException
); - return
false
i podaj rzeczywistą wartość zwracaną wout
parametrze (np.Dictionary<,>.TryGetValue
).
Tak więc pytania brzmią: w jakiej wyjątkowej sytuacji powinienem rzucić wyjątek? A jeśli nie powinienem rzucać: kiedy zwracana jest magiczna wartość przypisana powyżej zaimplementowania Try*
metody z out
parametrem ? (Dla mnie out
parametr wydaje się brudny, a jego prawidłowe użycie wymaga więcej pracy.)
Szukam odpowiedzi faktycznych, takich jak odpowiedzi dotyczące wytycznych projektowych (nie znam żadnych Try*
metod), użyteczności (ponieważ pytam o bibliotekę klas), zgodności z BCL i czytelności.
W bibliotece klas podstawowych .NET Framework stosowane są wszystkie trzy metody:
- zwraca magiczną wartość, która w innym przypadku nie ma znaczenia:
Collection<T>.IndexOf
zwraca -1,StreamReader.Read
zwraca -1,Math.Sqrt
zwraca NaN,Hashtable.Item
zwraca null;
- rzuć wyjątek:
Dictionary<,>.Item
zgłasza KeyNotFoundException,Double.Parse
zgłasza FormatException; lub
- zwróć
false
i podaj rzeczywistą wartość zwracaną wout
parametrze:
Zauważ, że jak Hashtable
zostało utworzone w czasie, gdy nie było żadnych generycznych w C #, używa object
i dlatego może powrócić null
jako magiczna wartość. Ale w przypadku leków generycznych stosuje się wyjątki Dictionary<,>
, które początkowo nie miały TryGetValue
. Najwyraźniej wgląd się zmienia.
Oczywiście Item
- TryGetValue
i Parse
- TryParse
dwoistość jest tam bez powodu, więc zakładam, że rzucanie wyjątki dla non-wyjątkowych niepowodzeń jest w C # 4 nie zrobił . Jednak Try*
metody nie zawsze istniały, nawet jeśli Dictionary<,>.Item
istniały.
źródło
Odpowiedzi:
Nie sądzę, że twoje przykłady są naprawdę równoważne. Istnieją trzy odrębne grupy, z których każda ma swoje uzasadnienie swojego zachowania.
StreamReader.Read
lub gdy istnieje prosta w użyciu wartość, która nigdy nie będzie prawidłową odpowiedzią (-1 dlaIndexOf
).Podane przykłady są całkowicie jasne dla przypadków 2 i 3. W przypadku wartości magicznych można argumentować, czy jest to dobra decyzja projektowa, czy nie we wszystkich przypadkach.
NaN
Zwrócony przezMath.Sqrt
to szczególny przypadek - wynika pływający standardowego punktu.źródło
Either<TLeft, TRight>
monadą?Próbujesz przekazać użytkownikowi interfejsu API, co musi zrobić. Jeśli rzucisz wyjątek, nic nie zmusi go do złapania go, a tylko czytanie dokumentacji da im znać, jakie są wszystkie możliwości. Osobiście uważam, że powolne i żmudne jest zagłębianie się w dokumentację, aby znaleźć wszystkie wyjątki, które może rzucić pewna metoda (nawet jeśli jest w inteligencji, wciąż muszę je ręcznie skopiować).
Magiczna wartość nadal wymaga przeczytania dokumentacji i ewentualnie odniesienia się
const
do tabeli, aby zdekodować wartość. Przynajmniej nie ma narzutu wyjątku dla tego, co nazywasz zdarzeniem nietypowym.Właśnie dlatego, mimo że
out
czasami nie są mile widziane parametry, wolę tę metodę zeTry...
składnią. Jest to kanoniczna składnia .NET i C #. Użytkownik interfejsu API komunikuje, że musi sprawdzić wartość zwracaną przed użyciem wyniku. Możesz także dołączyć drugiout
parametr z pomocnym komunikatem o błędzie, który ponownie pomaga w debugowaniu. Dlatego głosuję na rozwiązanieTry...
zout
parametrami.Inną opcją jest zwrócenie specjalnego obiektu „wynikowego”, choć uważam to za o wiele bardziej nużące:
Wtedy twoja funkcja wygląda dobrze, ponieważ ma tylko parametry wejściowe i zwraca tylko jedną rzecz. Po prostu zwraca jedną krotkę.
W rzeczywistości, jeśli lubisz takie rzeczy, możesz użyć nowych
Tuple<>
klas, które zostały wprowadzone w .NET 4. Osobiście nie podoba mi się fakt, że znaczenie każdego pola jest mniej wyraźne, ponieważ nie mogę podaćItem1
iItem2
przydatne nazwy.źródło
IMyResult
ponieważ można przekazać bardziej złożony wynik niż tylkotrue
lubfalse
wartość wyniku.Try*()
jest użyteczny tylko w przypadku prostych rzeczy, takich jak konwersja ciągów znaków na int.Jak pokazują twoje przykłady, każdy taki przypadek musi zostać oceniony osobno, a pomiędzy „wyjątkowymi okolicznościami” a „kontrolą przepływu” istnieje znaczne spektrum szarości, zwłaszcza jeśli twoja metoda ma być wielokrotnego użytku i może być stosowana w zupełnie innych wzorach niż pierwotnie został zaprojektowany. Nie oczekuj, że wszyscy tutaj uzgodnimy, co oznacza „nietypowy”, zwłaszcza jeśli od razu omówisz możliwość zastosowania „wyjątków” w celu wdrożenia tego.
Możemy również nie zgadzać się co do tego, który projekt sprawia, że kod jest najłatwiejszy do odczytania i utrzymania, ale założę, że projektant biblioteki ma jasną osobistą wizję tego i musi jedynie zrównoważyć go z innymi rozważaniami.
Krótka odpowiedź
Podążaj za swoimi przeczuciami, chyba że projektujesz dość szybkie metody i oczekujesz możliwości nieprzewidzianego ponownego użycia.
Długa odpowiedź
Każdy przyszły dzwoniący może dowolnie tłumaczyć między kodami błędów i wyjątkami, jak chce w obu kierunkach; To sprawia, że te dwa podejścia projektowe są prawie równoważne, z wyjątkiem wydajności, łatwości debugowania i niektórych ograniczonych kontekstów interoperacyjności. Zwykle sprowadza się to do wydajności, więc skupmy się na tym.
Zasadniczo spodziewaj się, że zgłoszenie wyjątku jest 200 razy wolniejsze niż zwykły zwrot (w rzeczywistości istnieje znaczna wariancja).
Jako kolejna reguła, zgłoszenie wyjątku może często pozwolić na znacznie czystszy kod w porównaniu z najokrutniejszymi magicznymi wartościami, ponieważ nie polegasz na tym, że programista tłumaczy kod błędu na inny kod błędu, ponieważ przechodzi on przez wiele warstw kodu klienta w kierunku punkt, w którym jest wystarczający kontekst do obsługi go w spójny i odpowiedni sposób. (Przypadek szczególny:
null
ma się tutaj lepiej niż inne wartości magiczne ze względu na jego tendencję do automatycznego przekładania się naNullReferenceException
niektóre, ale nie wszystkie rodzaje wad; zwykle, ale nie zawsze, całkiem blisko źródła defektu. )Więc jaka jest lekcja?
W przypadku funkcji, która jest wywoływana tylko kilka razy w trakcie życia aplikacji (np. Inicjalizacja aplikacji), użyj czegokolwiek, co zapewnia czystszy, łatwiejszy do zrozumienia kod. Wydajność nie może być problemem.
Aby użyć funkcji „wyrzucania”, użyj czegokolwiek, co zapewni czystszy kod. Następnie wykonaj profilowanie (w razie potrzeby) i zmień wyjątki, aby zwracać kody, jeśli znajdują się wśród podejrzanych górnych wąskich gardeł na podstawie pomiarów lub ogólnej struktury programu.
W przypadku drogiej funkcji wielokrotnego użytku użyj wszystkiego, co zapewni czystszy kod. Jeśli w zasadzie zawsze trzeba przejść przez sieć w obie strony lub przeanalizować plik XML na dysku, narzut związany z rzuceniem wyjątku jest prawdopodobnie nieistotny. Ważniejsze jest, aby nie stracić szczegółów jakiejkolwiek awarii, nawet przypadkowo, niż wyjątkowo szybko powrócić z „awarii wyjątkowej”.
Lean funkcja wielokrotnego użytku wymaga więcej przemyślenia. Stosując wyjątki, wymuszasz coś w rodzaju 100-krotnego spowolnienia wywołujących, którzy zobaczą wyjątek w połowie (wielu) wywołań, jeśli ciało funkcji działa bardzo szybko. Wyjątki są nadal opcją projektową, ale będziesz musiał zapewnić alternatywę o niskim koszcie dla dzwoniących, którzy nie mogą sobie na to pozwolić. Spójrzmy na przykład.
Podajesz świetny przykład
Dictionary<,>.Item
, który, mówiąc luźno, zmienił się ze zwracanianull
wartości na rzucanieKeyNotFoundException
między .NET 1.1 i .NET 2.0 (tylko jeśli jesteś gotów uważaćHashtable.Item
się za jego praktycznego, nietypowego prekursora). Przyczyna tej „zmiany” nie jest tutaj bez zainteresowania. Optymalizacja wydajności typów wartości (koniec boksu) sprawiła, że oryginalna magiczna wartość (null
) nie była opcją;out
parametry po prostu przywróciłyby niewielką część kosztów wydajności. Ta ostatnia kwestia wydajności jest całkowicie nieistotna w porównaniu z narzutem związanym z rzucaniem aKeyNotFoundException
, ale projekt wyjątku jest tutaj nadal lepszy. Dlaczego?Contains
przed każdym połączeniem z indeksem, a ten wzorzec czyta się całkowicie naturalnie. Jeśli programista chce, ale zapomina zadzwonićContains
, nie mogą wkraść się żadne problemy z wydajnością;KeyNotFoundException
jest wystarczająco głośny, aby zostać zauważonym i naprawiony.źródło
Contains
przed wywołaniem do indeksatora może spowodować wyścig, w którym nie musi występowaćTryGetValue
.ConcurrentDictionary
dla dostępu wielowątkowego iDictionary
dostępu jednowątkowego dla optymalnej wydajności. Oznacza to, że nieużywanie opcji `` Zawiera '' NIE czyni wątku kodu bezpiecznym dla tej konkretnejDictionary
klasy.Nie powinieneś dopuścić do niepowodzenia.
Wiem, że jest falujący i idealistyczny, ale wysłuchaj mnie. Podczas projektowania istnieje wiele przypadków faworyzowania wersji bez trybów awaryjnych. Zamiast „FindAll”, który zawodzi, LINQ używa klauzuli where, która po prostu zwraca pusty wyliczalny. Zamiast mieć obiekt, który należy zainicjować przed użyciem, pozwól konstruktorowi zainicjować obiekt (lub zainicjować, gdy wykryty zostanie niezainicjowany). Kluczem jest usunięcie gałęzi awarii w kodzie konsumenta. To jest problem, więc skup się na nim.
Inną strategią jest
KeyNotFound
sscenario. W prawie każdej bazie kodu, nad którą pracowałem od 3.0, istnieje coś takiego jak ta metoda rozszerzenia:Nie ma dla tego trybu realnej awarii.
ConcurrentDictionary
ma podobnyGetOrAdd
wbudowany.To powiedziawszy, zawsze będą chwile, w których będzie to po prostu nieuniknione. Wszystkie trzy mają swoje miejsce, ale wolałbym pierwszą opcję. Pomimo wszystkiego, co składa się na niebezpieczeństwo zerowe, jest dobrze znane i pasuje do wielu scenariuszy „nie znaleziono przedmiotu” lub „wynik nie ma zastosowania”, które składają się na zestaw „nie wyjątkowa porażka”. Zwłaszcza, gdy tworzysz typy wartości dopuszczających wartości zerowe, znaczenie „to może zawieść” jest bardzo wyraźne w kodzie i trudno je zapomnieć / zepsuć.
Druga opcja jest wystarczająca, gdy użytkownik robi coś głupiego. Daje ci ciąg znaków w niewłaściwym formacie, próbuje ustawić datę na 42 grudnia ... coś, co chcesz, aby wybuchło szybko i efektownie podczas testowania, aby zidentyfikować i naprawić zły kod.
Ostatnia opcja jest tą, której coraz bardziej nie lubię. Nasze parametry są niezręczne i zwykle naruszają niektóre z najlepszych praktyk podczas tworzenia metod, takich jak skupianie się na jednej rzeczy i brak efektów ubocznych. Dodatkowo, nasz parametr ma zwykle znaczenie tylko podczas sukcesu. To powiedziawszy, są one niezbędne dla niektórych operacji, które są zwykle ograniczone przez problemy związane ze współbieżnością lub względy wydajnościowe (gdzie na przykład nie chcesz robić drugiej podróży do bazy danych).
Jeśli zwracana wartość i parametr wyjściowy nie są trywialne, preferowana jest sugestia Scotta Whitlocka dotycząca obiektu wynikowego (jak
Match
klasa Regex ).źródło
out
parametry są całkowicie ortogonalne w stosunku do kwestii skutków ubocznych. Modyfikowanieref
parametru jest efektem ubocznym, a modyfikowanie stanu obiektu, który przekazujesz za pomocą parametru wejściowego, jest efektem ubocznym, aleout
parametr jest po prostu niewygodnym sposobem, aby funkcja zwracała więcej niż jedną wartość. Brak efektu ubocznego, tylko wielokrotne zwracane wartości.Zawsze preferuj wyjątek. Ma jednolity interfejs wśród wszystkich funkcji, które mogą zawieść, i wskazuje awarię tak głośno, jak to możliwe - bardzo pożądana właściwość.
Zauważ, że
Parse
iTryParse
tak naprawdę nie są tym samym, z wyjątkiem trybów awarii. Fakt, żeTryParse
może również zwracać wartość, jest nieco ortogonalny. Rozważ sytuację, w której na przykład sprawdzasz poprawność niektórych danych wejściowych. Tak naprawdę nie obchodzi cię, jaka jest wartość, o ile jest ona ważna. I nie ma nic złego w oferowaniu pewnego rodzajuIsValidFor(arguments)
funkcji. Ale nigdy nie może być podstawowym trybem działania.źródło
Jak zauważyli inni, magiczna wartość (w tym logiczna wartość zwrotna) nie jest tak świetnym rozwiązaniem, z wyjątkiem znacznika „końca zakresu”. Powód: semantyka nie jest wyraźna, nawet jeśli zbadamy metody obiektu. Musisz przeczytać pełną dokumentację dla całego obiektu aż do „o tak, jeśli zwraca -42, co oznacza bla bla bla”.
Tego rozwiązania można użyć z przyczyn historycznych lub ze względu na wydajność, ale w innym przypadku należy go unikać.
Pozostawia to dwa ogólne przypadki: sondowanie lub wyjątki.
W tym przypadku ogólna zasada jest taka, że program nie powinien reagować na wyjątki, z wyjątkiem sytuacji, gdy program postępuje, gdy program / przypadkowo / narusza jakiś warunek. Należy zastosować sondowanie, aby upewnić się, że tak się nie stanie. Dlatego wyjątek oznacza albo, że odpowiednie sondowanie nie zostało przeprowadzone z wyprzedzeniem, albo że wydarzyło się coś zupełnie nieoczekiwanego.
Przykład:
Chcesz utworzyć plik z podanej ścieżki.
Należy użyć obiektu File, aby z wyprzedzeniem ocenić, czy ta ścieżka jest zgodna z prawem do tworzenia lub zapisywania plików.
Jeśli twój program w jakiś sposób nadal próbuje pisać na ścieżce, która jest nielegalna lub w inny sposób niemożliwa do zapisu, powinieneś otrzymać ekscytację. Może się to zdarzyć z powodu wyścigu (inny użytkownik usunął katalog lub ustawił go jako tylko do odczytu, po wystąpieniu problemu)
Zadanie obsługi nieoczekiwanej awarii (sygnalizowanej przez wyjątek) i sprawdzenie, czy warunki są odpowiednie dla operacji z wyprzedzeniem (sondowanie), zwykle będą miały inną strukturę i dlatego powinny korzystać z różnych mechanizmów.
źródło
Myślę, że
Try
wzorzec jest najlepszym wyborem, gdy kod wskazuje tylko, co się stało. Nienawidzę param i lubię przedmiot zerowalny. Stworzyłem następującą klasęwięc mogę napisać następujący kod
Wygląda na zerowy, ale nie tylko dla typu wartości
Inny przykład
źródło
try
catch
sieją spustoszenie w twoim występie, jeśli dzwoniszGetXElement()
i nie udaje ci się wiele razy.public struct Nullable<T> where T : struct
główna różnica w ograniczeniu. btw najnowsza wersja jest tutaj github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…Jeśli interesuje Cię trasa „magicznej wartości”, jeszcze innym sposobem na rozwiązanie tego problemu jest przeciążenie celu klasy Lazy. Chociaż Lazy ma na celu odroczenie tworzenia instancji, nic tak naprawdę nie uniemożliwia korzystania z opcji Like lub Option. Na przykład:
źródło