Odpowiem z punktu widzenia C ++. Jestem prawie pewien, że wszystkie podstawowe koncepcje można przenieść do C #.
Wygląda na to, że preferowanym stylem jest „zawsze zgłaszaj wyjątki”:
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
Może to stanowić problem dla kodu C ++, ponieważ obsługa wyjątków jest ciężka - powoduje to, że przypadek awarii działa wolno, i sprawia, że przypadek awarii alokuje pamięć (która czasami nawet nie jest dostępna) i ogólnie czyni rzeczy mniej przewidywalnymi. Ciężka waga EH jest jednym z powodów, dla których słyszysz ludzi mówiących na przykład: „Nie używaj wyjątków dla kontroli przepływu”.
Tak więc niektóre biblioteki (takie jak <filesystem>
) używają tego, co C ++ nazywa „podwójnym API” lub tego, co C # nazywa Try-Parse
wzorcem (dzięki Peter za wskazówkę!)
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
bool TryCalculateArea(int x, int y, int& result) {
if (x < 0 || y < 0) {
return false;
}
result = x * y;
return true;
}
int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
// use a2
}
Od razu widać problem z „podwójnymi interfejsami API”: dużo duplikacji kodu, brak wskazówek dla użytkowników, który interfejs API jest „właściwy” do użycia, a użytkownik musi dokonać trudnego wyboru między użytecznymi komunikatami o błędach ( CalculateArea
) i speed ( TryCalculateArea
), ponieważ szybsza wersja bierze nasz użyteczny "negative side lengths"
wyjątek i spłaszcza go w bezużyteczny false
- „coś poszło nie tak, nie pytaj mnie co i gdzie”. (Niektóre podwójne API użyć bardziej wyrazisty rodzaj błędu, jak int errno
i C ++ 's std::error_code
, ale to nadal nie powiedzieć, gdzie wystąpił błąd - po prostu, że nie występują gdzieś).
Jeśli nie możesz zdecydować, jak powinien zachowywać się Twój kod, zawsze możesz podważyć decyzję dzwoniącego!
template<class F>
int CalculateArea(int x, int y, F errorCallback) {
if (x < 0 || y < 0) {
return errorCallback(x, y, "negative side lengths");
}
return x * y;
}
int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });
Zasadniczo to robi twój współpracownik; poza tym, że rozdziela on „moduł obsługi błędów” na zmienną globalną:
std::function<int(const char *)> g_errorCallback;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorCallback("negative side lengths");
}
return x * y;
}
g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);
Przeniesienie ważnych parametrów z jawnych parametrów funkcji do stanu globalnego jest prawie zawsze złym pomysłem. Nie polecam tego. (Fakt, że w twoim przypadku nie jest to stan globalny, ale po prostu państwo członkowskie w całej instancji łagodzi nieco złą sytuację, ale niewiele).
Ponadto współpracownik niepotrzebnie ogranicza liczbę możliwych zachowań związanych z obsługą błędów. Zamiast pozwolić żadnej obsługi błędów lambda, on zdecydował się na tylko dwa:
bool g_errorViaException;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorViaException ? throw Exception("negative side lengths") : 0;
}
return x * y;
}
g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);
Jest to prawdopodobnie „kwaśne miejsce” spośród którejkolwiek z tych możliwych strategii. Odebrałeś całą elastyczność użytkownikowi końcowemu, zmuszając go do korzystania z jednego z twoich dokładnie dwóch zwrotnych poleceń obsługi błędów; i masz wszystkie problemy wspólnego stanu globalnego; i nadal płacisz za tę gałąź warunkową wszędzie.
Wreszcie, powszechnym rozwiązaniem w C ++ (lub dowolnym języku z kompilacją warunkową) byłoby zmuszenie użytkownika do podjęcia decyzji dla całego programu, globalnie, w czasie kompilacji, tak aby niepoddaną ścieżkę kodową można było całkowicie zoptymalizować:
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
return 0;
#else
throw Exception("negative side lengths");
#endif
}
return x * y;
}
// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);
Przykładem tego, co działa w ten sposób, jest assert
makro w C i C ++, które warunkuje jego zachowanie na makrze preprocesora NDEBUG
.