Проблема:
З давнього часу я переживаю за exceptions
механізм, бо відчуваю, що він насправді не вирішує те, що повинен.
СТВЕРДЖЕННЯ: Поза цією темою тривають дебати, і більшість з них намагаються порівняти exceptions
порівняно з поверненням коду помилки. Це, безумовно, не тут тема.
Намагаючись визначити помилку, я погодився б із CppCoreGuidelines від Bjarne Stroustrup & Herb Sutter
Помилка означає, що функція не може досягти своєї рекламованої мети
ЗАЯВКА: exception
Механізм є мовною семантикою для обробки помилок.
СТВЕРДЖЕННЯ: Для мене функція "не має виправдання" для невиконання завдання: або ми неправильно визначили умови до / після, тому функція не може забезпечити результати, або якийсь конкретний винятковий випадок не вважається достатньо важливим для витрачання часу на розробку вирішення. Враховуючи те, що IMO, різниця між нормальним кодом та обробкою коду помилки є (до впровадження) дуже суб'єктивною лінією.
ВИМОГА: Використання винятків для вказівки, коли умова до чи пост не виконується - це ще одна мета exception
механізму, головним чином для налагодження. Я не націлююсь на це використання exceptions
тут.
У багатьох книгах, навчальних посібниках та інших джерелах вони, як правило, показують поводження з помилками як досить об'єктивну науку, що вирішується, exceptions
і вам просто потрібно, щоб catch
вони мали надійне програмне забезпечення, здатне відновитися з будь-якої ситуації. Але мої кілька років роботи як розробника змушують мене бачити проблему з іншого підходу:
- Програмісти, як правило, спрощують своє завдання, кидаючи винятки, коли конкретний вигляд здається надто рідкісним, щоб його ретельно реалізовували. Типовими випадками цього є: проблеми з пам'яттю, проблеми з повним диском, пошкоджені проблеми з файлами тощо. Це може бути достатньо, але це не завжди вирішується з архітектурного рівня.
- Програмісти, як правило, уважно не читають документацію про винятки в бібліотеках і, як правило, не знають, що і коли кидає функція. Крім того, навіть коли вони знають, вони не дуже їм керують.
- Програмісти, як правило, не ловлять винятки досить рано, і коли вони це роблять, то це здебільшого реєструватися та кидати далі. (див. перший пункт).
Це має два наслідки:
- Помилки, що часто трапляються, виявляються на початку розвитку та налагоджуються (що добре).
- Рідкісні винятки не керуються і змушують систему вийти з ладу (з приємним повідомленням журналу) в будинку користувача. Деякі випадки повідомляється про помилку, або навіть навіть.
Враховуючи це, основною метою механізму помилок має бути IMO:
- Зробити видимим у коді, коли певний випадок не керується.
- Повідомте час виконання проблеми відповідному коду (принаймні абоненту), коли ця ситуація станеться.
- Забезпечує механізми відновлення
Основним недоліком exception
семантичного механізму обробки помилок є IMO: легко зрозуміти, де a throw
знаходиться у вихідному коді, але абсолютно не очевидно, щоб знати, чи може конкретна функція кинути, дивлячись на декларацію. Це приносить всю проблему, яку я представив вище.
Мова не застосовує та перевіряє код помилки так само суворо, як це стосується інших аспектів мови (наприклад, сильні типи змінних)
Спробу рішення
З метою покращення цього я розробив дуже просту систему обробки помилок, яка намагається поставити обробку помилок на той самий рівень важливості, що і звичайний код.
Ідея така:
- Кожна (відповідна) функція отримує посилання на
success
дуже легкий об'єкт, і може встановити його на стан помилки на випадок. Об'єкт дуже легкий, поки не буде збережена помилка з текстом. - Функція рекомендується пропустити своє завдання, якщо наданий об'єкт вже містить помилку.
- Помилка ніколи не повинна перевищувати.
Повний дизайн, очевидно, ретельно враховує кожен аспект (приблизно 10 сторінок), а також як його застосувати до OOP.
Приклад Success
класу:
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;
};
Використання:
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;
Я це використав у багатьох своїх (власних) кодах, і це змусило програміста (мене) думати далі про можливі виняткові випадки та способи їх вирішення (добре). Однак він має криву навчання і не добре інтегрується з кодом, який зараз використовується.
Питання
Я хотів би краще зрозуміти наслідки використання такої парадигми в проекті:
- Чи правильна передумова проблеми? чи я пропустив щось відповідне?
- Чи є рішення гарною архітектурною ідеєю? чи ціна зависока?
Редагувати:
Порівняння між методами:
//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.