Я відповім з точки зору 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
.