За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью. С другой стороны, отказ от проверок кодов возврата тоже плохо. Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объеденить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки. Проще проиллюстрировать псевдокодом:
int plus2([неявно ExecutionFlow executionFlow], int x)
{
if (x < 10)
return RangeError("bla-bla"); // Выход из функции и установка ошибки executionFlow.setError(RangeError("bla-bla"))
return x + 2; // Просто выход и возврат значения, неявно оборачивается в ReturnValue<int>
}
void foo() { ... }
Вызывающий код:
var y = foo(5);
if (y.success) // проверка, также устанавливает в объекте ReturnValue<int> и ExecutionFlow флажок checked = true
int z = bar(y); // неявно приводится к int, если перед приведением не проверить success, то программа пишет стек и завершается
if (!foo().success)
print(executionFlow.stackTrace());
var error = executionFlow.error();
print(error.message())
return error
foo(); // не проверили success, флажок не установился, значит при вызове следующей функции (любой) будет стек и аварийное завершение
Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Еще дополнение — нельзя просто написать foo.success и забить, надо обязательно проверить if, за этим следит компилятор и чтобы жизнь медом не казалось if не должен быть пустым. Чтобы просто проигнорировать проверку надо вместо success вызвать что-то вроде foo().IWantToSkipThisCheck
Пока что не встречал ситуаций, когда мне не хотелось бы использовать исключения. Поэтому считаю коды возврата отвратительной идеей. Про асинхронный код:
Здравствуйте, MTD, Вы писали:
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Зависит от окружения. Для шарпа, скажем, почти всегда сработает TryXxx + парный метод с исключениями. Все остальные способы требуют гораздо больше телодвижений и ведут к адскому лапшекоду — приходится или городить обёртки для чужого api, или поддерживать несколько стилей обработки ошибок.
Я очень сомневаюсь, что получится совместить явную проверку результатов выполнения и компактность кода. Т.е. или ищем язык с автоматом проверяемыми предусловиями, или велкам в копипасту if(error) по всему коду.
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью.
Это два разных подхода с совершенно различными целями и средствами. В случае кодов — это обычно ветвление бизнес-логики, в результате которого нужно продолжить сценарий по какой-то отдельной ветке. В случае реальной ошибке — это обработка через исключения, в котором необходимо корректно убить само приложение которое в неизвестно каком состоянии (с записью ошибки в лог), либо максимальная очистка контекста вызова в случае серверного приложения (с записью ошибки в лог).
Что исключения что коды ошибок не решают проблему, а порождают новые.
Если код описывает последовательность действий, при условии что всё гладко то код простой.
Если что-то пошло не так, то надо выполнять действия которые обычно вне компетенции данного кода и вне знаний вызывающего.
Поэтому любая попытка обрабатывать коды внутри приводит к увеличению сложности, а снаружи к увеличению кол-ва абстракций.
А еще код наровит выделять разные ресурсы, которые кто-то должен освобождать и останавливать (если дочерние потоки или всякие подписки).
Соответственно языку следовало бы иметь неявно ExecutionFlow, который определял бы определял политику реакции на не штатные ситуации. И через него же выделять ресурсы, что память что дочерние потоки исполнения. И операторы которые явно задают как реагировать на нештатное поведение. И политику и мониторинг выделения ресурсов туда же засунуть, например выполнять не более 100мс (или даже в тактах) и памяти не более 10Мб и не более 4 потоков.
value=method1(args) or method2(args);
action1(); // если код возврата никем не обрабатывается то компилятор автоматически делает примерно такое
// { var res=action1(); if (res && executionFlow.NoErrorAllowed && !executionFlow.DisableThisError(here)) return executionFlow.ReturnError(here,res,"action1" ... ); }
action2() or action2b();
action3() or ignore;
action4()
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью. С другой стороны, отказ от проверок кодов возврата тоже плохо. Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объединить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки. Проще проиллюстрировать псевдокодом:
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Право на жизнь имеет любой подход. Erlang знатно обрабатывает ошибки.
Здравствуйте, MTD, Вы писали:
MTD>Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок.
Всё давно придумано. В Rust, насколько мне известно, нет исключений. В языках с поддержкой алгебраических типов данных это выражается типами-суммами Option<T> (aka Maybe) и Result<T, E>. В обычных языках это можно эмулировать а-ля «коды ошибок на стероидах». Возвращаемые данные нужно явно unwrap'ить (ну или лучше использовать монадический bind) — в любом случае нельзя «пропустить» проверку. В случае ошибки её контекст тоже сохраняется, то есть это не просто целочисленный код. Механизм исключений в языке при этом в каком-то виде может быть (раскрутка стека), просто они неперехватываемые (panic).
Здравствуйте, vsb, Вы писали:
vsb>Почему это не асинхронный? something() возвращает асинхронный результат.
Потому что ты синхронно дожидаешься результата. Если бы ты продолжил выполнение другого кода с неблокирующимися вызовами, то да код стал бы асинхронным и внезапно стало бы непонятно, как работать с исключениями.
Здравствуйте, MTD, Вы писали:
vsb>>Почему это не асинхронный? something() возвращает асинхронный результат.
MTD>Потому что ты синхронно дожидаешься результата.
Ничего я не дожидаюсь.
> Если бы ты продолжил выполнение другого кода с неблокирующимися вызовами, то да код стал бы асинхронным и внезапно стало бы непонятно, как работать с исключениями.
Это асинхронный код с неблокирующимся вызовом. Он отдаёт управление сразу по достижении слова await.
Здравствуйте, MTD, Вы писали:
MTD>Ты что-то путаешь — await означает ждать и так он себя в известных мне языках и ведет, кстати, сейчас мы про какой язык говорим?
await — это генерация компилятором конечного автомата с выходом из метода и освобождения потока и входом при окончании исполнения асинхронного метода.
Здравствуйте, GlebZ, Вы писали:
GZ>await — это генерация компилятором конечного автомата с выходом из метода и освобождения потока и входом при окончании исполнения асинхронного метода.
Да понятно, в приведенном коде точка синхронизации — получение х, пока его не получим на следующую строчку не попадем. Я не прав?
GZ>>await — это генерация компилятором конечного автомата с выходом из метода и освобождения потока и входом при окончании исполнения асинхронного метода.
MTD>Да понятно, в приведенном коде точка синхронизации — получение х, пока его не получим на следующую строчку не попадем. Я не прав?
Безусловно. Остаток процедуры будет запущен после выполнения асинхронно функции и при наличии кванта времени в диспетчере. Поэтому это действительно асинхронное неблокирующеся исполнение.
Здравствуйте, GlebZ, Вы писали:
GZ>Остаток процедуры будет запущен после выполнения асинхронно функции
Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова. В настоящем асинхронном коде таких явных точек синхронизации нет, после выполнения операции вызывается обработчик — это позволяет лучше нагрузить процессор не тратя время на ожидание.
Здравствуйте, MTD, Вы писали:
GZ>>Остаток процедуры будет запущен после выполнения асинхронно функции
MTD>Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова.
Всё наоборот. Это асинхронный код, который притворяется синхронным.
> В настоящем асинхронном коде таких явных точек синхронизации нет, после выполнения операции вызывается обработчик — это позволяет лучше нагрузить процессор не тратя время на ожидание.
Это не точка синхронизации. Это указание кода, который будет выполнен при получении результата. С нагрузкой процессора тут проблем нет, поток будет выполнять следующий обработчик.
Собственно поинт в том, что если в языке есть удобные средства для работы с асинхронным кодом (в JavaScript, Kotlin, вроде в C# тоже такое есть), то исключения прекрасно подходят для работы с ошибками. Если таких средств нет, с асинхронным кодом работать очень неудобно в любом случае.
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto
Вас же, надеюсь, никто заставляет использовать исключения в качестве неявного goto?
MTD>кроме того использование их в многопоточном и асинхронном коде становится болью.
Хз, у нас вроде не становится. В чем боль? Task'и, PLinq, класс Parallel — оборачивают исключение в AggregateException и пробрасывают его в вызывающий код.
Может не надо ничего фантазировать, а надо просто научиться пользоваться исключениями и многопоточностью?
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью. С другой стороны, отказ от проверок кодов возврата тоже плохо. Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объеденить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки. Проще проиллюстрировать псевдокодом:
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Поздравляю! Вы изобрели монаду!
Типа Error Monad.
Языки в которых такое есть: Haskell
F#
OCaml
Idris
Насчёт первых двух уверен на 100%, насчёт остальных — поменьше, поскольку не работаю с ними.
Наверняка есть и другие языки с поддержкой монад вообще и монады ошибок в частности.
Здравствуйте, vsb, Вы писали:
GZ>>>Остаток процедуры будет запущен после выполнения асинхронно функции MTD>>Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова. vsb>Всё наоборот. Это асинхронный код, который притворяется синхронным.
Семантически этот код — именно синхронный. То, что компилятор где-то внутри генерит — это просто деталь реализации и не более. Если я возьму другой компилятор, который будет создавать синхронный код в этом месте, то не изменится НИЧЕГО — ни код, ни поведение программы.