Problem:
Od dłuższego czasu martwię się o exceptions
mechanizm, ponieważ uważam, że tak naprawdę nie rozwiązuje tego, co powinien.
ROSZCZENIE: Na ten temat toczą się długie debaty i większość z nich ma trudności z porównywaniem exceptions
i zwracaniem kodu błędu. To zdecydowanie nie jest tutaj temat.
Próbując zdefiniować błąd, zgodziłbym się z CppCoreGuidelines z Bjarne Stroustrup & Herb Sutter
Błąd oznacza, że funkcja nie może osiągnąć reklamowanego celu
ROSZCZENIE: exception
Mechanizm jest semantycznym językiem do obsługi błędów.
OŚWIADCZENIE: Dla mnie „nie ma usprawiedliwienia” dla funkcji nieosiągnięcia zadania: albo źle zdefiniowaliśmy warunki przed / po, aby funkcja nie mogła zapewnić wyników, albo jakiś szczególny wyjątkowy przypadek nie jest uważany za wystarczająco ważny, aby poświęcić czas na rozwój rozwiązanie. Biorąc pod uwagę, że IMO, różnica między obsługą normalnego kodu a kodem błędu jest (przed wdrożeniem) bardzo subiektywną linią.
ROSZCZENIE: Wykorzystanie wyjątków do wskazania, kiedy warunek wstępny lub końcowy nie jest zachowany, jest kolejnym celem exception
mechanizmu, głównie w celu debugowania. Nie celuję w to użycie exceptions
tutaj.
W wielu książkach, samouczkach i innych źródłach mają tendencję do pokazywania obsługi błędów jako dość obiektywnej nauki, którą można rozwiązać, exceptions
a po prostu potrzebujesz catch
ich do posiadania solidnego oprogramowania, które jest w stanie wyjść z każdej sytuacji. Ale kilka lat pracy jako programisty sprawia, że widzę problem z innego podejścia:
- Programiści mają tendencję do upraszczania swoich zadań, zgłaszając wyjątki, gdy konkretny przypadek wydaje się zbyt rzadki, aby można go było dokładnie wdrożyć. Typowe przypadki to: problemy z brakiem pamięci, problemy z zapełnieniem dysku, problemy z uszkodzonymi plikami itp. Może to być wystarczające, ale nie zawsze jest to podejmowane z poziomu architektury.
- Programiści zwykle nie czytają uważnie dokumentacji dotyczącej wyjątków w bibliotekach i zwykle nie są świadomi, która i kiedy funkcja wyrzuca. Co więcej, nawet jeśli wiedzą, tak naprawdę nimi nie zarządzają.
- Programiści zwykle nie wychwytują wyjątków wystarczająco wcześnie, a kiedy to robią, najczęściej rejestrują i rzucają dalej. (patrz pierwszy punkt).
Ma to dwie konsekwencje:
- Często występujące błędy są wykrywane na wczesnym etapie rozwoju i debugowane (co jest dobre).
- Rzadkie wyjątki nie są zarządzane i powodują awarię systemu (z ładnym komunikatem dziennika) w domu użytkownika. Czasami błąd jest zgłaszany lub nawet nie.
Biorąc to pod uwagę, głównym celem mechanizmu błędów IMO powinno być:
- Widoczne w kodzie, w którym nie jest zarządzany określony przypadek.
- Przekaż środowisko wykonawcze problemu do pokrewnego kodu (przynajmniej wywołującego), gdy taka sytuacja się zdarzy.
- Zapewnia mechanizmy odzyskiwania
Główną wadą exception
semantyki jako mechanizmu obsługi błędów jest IMO: łatwo jest sprawdzić, gdzie throw
jest kod źródłowy, ale absolutnie nie jest oczywiste, czy określona funkcja mogłaby rzucić, patrząc na deklarację. To przynosi cały problem, który przedstawiłem powyżej.
Język nie wymusza i nie sprawdza kodu błędu tak ściśle, jak ma to miejsce w przypadku innych aspektów języka (np. Silne typy zmiennych)
Próba rozwiązania
Aby to poprawić, opracowałem bardzo prosty system obsługi błędów, który stara się ustawić obsługę błędów na tym samym poziomie ważności, co normalny kod.
Chodzi o:
- Każda (odpowiednia) funkcja otrzymuje odniesienie do
success
bardzo lekkiego obiektu i może w razie potrzeby ustawić status błędu. Obiekt jest bardzo lekki, dopóki nie zostanie zapisany błąd tekstu. - Funkcja jest zachęcana do pominięcia swojego zadania, jeśli podany obiekt zawiera już błąd.
- Błąd nigdy nie może zostać zastąpiony.
Pełny projekt oczywiście dokładnie uwzględnia każdy aspekt (około 10 stron), a także sposób zastosowania go do OOP.
Przykład Success
klasy:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Stosowanie:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
Użyłem tego w wielu (własnych) kodach i zmusza to programistę (mnie) do dalszego zastanowienia się nad możliwymi wyjątkowymi przypadkami i jak je rozwiązać (dobrze). Ma jednak krzywą uczenia się i nie integruje się dobrze z kodem, który go teraz używa.
Pytanie
Chciałbym lepiej zrozumieć konsekwencje zastosowania takiego paradygmatu w projekcie:
- Czy przesłanka problemu jest poprawna? lub Czy przegapiłem coś istotnego?
- Czy to dobry pomysł na architekturę? czy cena jest zbyt wysoka?
EDYTOWAĆ:
Porównanie metod:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.