Я отвечу с точки зрения C ++. Я уверен, что все основные понятия могут быть перенесены в C #.
Похоже, ваш предпочтительный стиль - «всегда выбрасывать исключения»
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
Это может быть проблемой для кода C ++, потому что обработка исключений является тяжелой - она заставляет случай сбоя работать медленно и заставляет случай сбоя выделять память (которая иногда даже недоступна) и, как правило, делает вещи менее предсказуемыми. Тяжелый вес EH - одна из причин, по которой вы слышите, как люди говорят что-то вроде «Не используйте исключения для управления потоком».
Поэтому некоторые библиотеки (например, <filesystem>
) используют то, что C ++ называет «двойным API», или то, что C # называет Try-Parse
шаблоном (спасибо Петру за подсказку!)
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
}
Вы можете сразу увидеть проблему с «двойными API»: много дублирования кода, нет рекомендаций для пользователей относительно того, какой API «правильный» для использования, и пользователь должен сделать трудный выбор между полезными сообщениями об ошибках ( CalculateArea
) и speed ( TryCalculateArea
), потому что более быстрая версия берет наше полезное "negative side lengths"
исключение и превращает его в бесполезное false
- «что-то пошло не так, не спрашивайте меня, что или где». (Некоторые сдвоенные интерфейсы используют более выразительный тип ошибки, например, int errno
или Си ++ std::error_code
, но это еще не говорит вам , где произошла ошибка - просто , что это действительно происходит где - то.)
Если вы не можете решить, как должен вести себя ваш код, вы всегда можете довести решение до вызывающей стороны!
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; });
По сути, это то, что делает ваш коллега; за исключением того, что он выделяет «обработчик ошибок» в глобальную переменную:
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);
Перемещение важных параметров из явных параметров функции в глобальное состояние - почти всегда плохая идея. Я это не рекомендую. (Тот факт, что это не глобальное состояние в вашем случае, а просто государство - член всего экземпляра, немного смягчает проблему, но не сильно.)
Кроме того, ваш коллега излишне ограничивает количество возможных способов обработки ошибок. Вместо того, чтобы разрешить любую лямбду обработки ошибок, он решил только два:
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);
Это, вероятно, «кислое пятно» любой из этих возможных стратегий. Вы отняли всю гибкость у конечных пользователей, заставив их использовать один из двух ваших обратных вызовов обработки ошибок; и у вас есть все проблемы общего глобального состояния; и вы все еще платите за эту условную ветку везде.
Наконец, общее решение в C ++ (или любом языке с условной компиляцией) будет заключаться в том, чтобы заставить пользователя принимать решение для всей своей программы, глобально, во время компиляции, чтобы неиспользованный кодовый путь можно было полностью оптимизировать:
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);
Примером того, что работает таким образом, является assert
макрос в C и C ++, который обуславливает свое поведение в макросе препроцессора NDEBUG
.