12. Представление ошибок и восстановление после них
Отделяйте ошибки от нормальных значений и очищайте ресурсы от путей сбоя.
Wave Foundation
Результат функции степени отказа
Отсутствующие файлы или ввод за пределами допустимого диапазона происходят в программах естественным образом. Обработка ошибки — это больше, чем просто печать сообщения. Это процесс выявления сбоев, проверки состояния уже выполненной работы, организации приобретенных ресурсов и последующего выбора, продолжать или прекратить работу.
В этой главе мы начнем с представления ошибки небольшой функции и перейдем к структуре результата, variant, раннему возврату и очистке ресурсов.
Маркеры неудач не должны перекрывать значения успеха.
Причина, по которой -1 используется как не найденная при поиске по массиву, заключается в том, что эффективный индекс равен 0 или более. С другой стороны, в вычислениях, где любое целое число может быть нормальным результатом, если -1 обозначено как ошибка, его невозможно отличить от нормального значения -1.
0 также является часто неправильно понимаемым значением. Длина пустой строки 0, первая позиция 0 и количество переданных байтов 0 имеют разные значения для разных функций. Не судите об успехе только потому, что возвращаемое значение не равно нулю.
Возвращайте успех и ценность вместе
struct Division {
ok: bool;
value: i32;
}
fun divide_nonnegative(left: i32, right: i32) -> Division {
if (left < 0 || right <= 0) {
return Division {
ok: false,
value: 0
};
}
return Division {
ok: true,
value: left / right
};
}
fun main() {
var result: Division = divide_nonnegative(0, 3);
if (!result.ok) {
println("invalid input");
return;
}
println("value={}", result.value);
}
Результат выполнения:
value=0
Нормальный результат также может быть 0. Вместо того, чтобы смотреть на value и гадать, был ли он успешным, сначала проверьте ok. Даже если поле value существует в неудачном результате, это не означает, что это значение следует использовать.
Правила ввода для этого примера таковы: левая часть равна 0 или больше, а правая часть положительна. Область действия указана в имени и описании функции, чтобы отличить ее от типичного целочисленного деления signed.
Различать причины ошибок
Добавьте информацию об ошибке для получения дополнительных рекомендаций или восстановления в зависимости от причины сбоя. Существует также способ разделить функцию на функцию, проверяющую входной диапазон, и функцию, вычисляющую его.
struct Check {
ok: bool;
error: i32;
}
fun check_quantity(value: i32) -> Check {
if (value < 1) {
return Check {
ok: false,
error: 1
};
}
if (value > 100) {
return Check {
ok: false,
error: 2
};
}
return Check {
ok: true,
error: 0
};
}
fun main() {
var result: Check = check_quantity(101);
if (!result.ok) {
match (result.error) {
1 => {
println("quantity is too small");
}
2 => {
println("quantity is too large");
}
_ => {
println("invalid quantity");
}
}
}
}
Результат выполнения:
quantity is too large
Значение чисел определяется этой функцией. Ее нельзя считать ошибкой номер 1 или 2 в других библиотеках. В общедоступной версии API именование констант или типов ошибок избавляет вызывающую сторону от необходимости запоминать случайные числа.
Отдельные результаты с помощью variant
Отношения, согласно которым значение успеха и ошибки не могут существовать одновременно, можно выразить как variant.
variant ByteResult {
Value(u8),
OutOfRange(i32)
}
fun to_byte(value: i32) -> ByteResult {
if (value < 0 || value > 255) {
return ByteResult::OutOfRange(value);
}
return ByteResult::Value(value as u8);
}
fun main() {
var result: ByteResult = to_byte(300);
match (result) {
ByteResult::Value(value) => {
println("byte={}", value);
}
ByteResult::OutOfRange(original) => {
println("out of range: {}", original);
}
}
}
Результат выполнения:
out of range: 300
Ошибка содержала исходное входное значение. Вызывающему абоненту проще объяснить проблему, чем просто вернуть false. Характер данных также принимается во внимание, поскольку в журнале не остаются конфиденциальные данные, такие как пароли или токены.
Облегчите чтение обычных маршрутов с помощью раннего возвращения.
При выполнении многоэтапной проверки нет необходимости помещать весь хороший код глубоко внутрь if. Если это не удастся, вы можете немедленно вернуться и продолжить путь успеха, описанный ниже.
fun show_total(quantity: i32, price: i32) {
if (quantity < 1 || quantity > 100) {
println("invalid quantity");
return;
}
if (price < 0 || price > 100000) {
println("invalid price");
return;
}
var total: i32 = quantity * price;
println("total={}", total);
}
fun main() {
show_total(3, 1200);
show_total(0, 1200);
show_total(3, -1);
}
Результат выполнения:
total=3600
invalid quantity
invalid price
После прохождения теста вы можете воспользоваться тем, что quantity и price находятся в пределах указанного диапазона. Диапазон установлен таким образом, чтобы промежуточное умножение также попадало в пределы i32. Добавляя ранний возврат, вам также следует проверить, есть ли у вас уже какие-либо ресурсы на этот момент.
Очистка памяти путей сбоя
import("std::buffer::alloc")::{
Buffer, buffer_init, buffer_free
};
import("std::buffer::write")::{
buffer_append_str
};
fun make_message(allowed: bool) -> i32 {
var data: Buffer;
if (buffer_init(&data, 16) < 0) {
return 1;
}
if (!allowed) {
buffer_free(&data);
return 2;
}
if (buffer_append_str(&data, "ready") < 0) {
buffer_free(&data);
return 3;
}
println("bytes={}", data.len);
if (buffer_free(&data) < 0) {
return 4;
}
return 0;
}
fun main() {
println("status={}", make_message(false));
}
Результат выполнения:
status=2
Отпустите Buffer даже в тех путях сбоя, которые не выводят данные. Вместо того, чтобы превращать каждый сбой в один return, важно четко управлять тем, что принадлежит каждому филиалу.
Существует также API, где сама очистка не удалась. Решите, как вы будете сохранять ошибки и устранять ошибки в исходной работе. Этот небольшой пример сначала возвращает номер ошибки задачи. Более крупные программы могут записывать оба по отдельности.
Частичные успехи не отменяются автоматически.
Если вы записываете в файл несколько байтов, а затем запись завершается неудачно, уже записанные байты не теряются. Другой конец сети также мог получить некоторые данные. Повторение одной и той же задачи с самого начала может привести к дублированию записей.
И наоборот, чтение checked byte cursor сохраняет положение и выходное значение в случае сбоя. Эти функции могут получить дополнительные входные данные и повторить попытку в том же месте. Вместо применения правила «если произойдет сбой, ничего не изменится» к каждому API, проверьте документацию по этой функции.
Устранимые ошибки и ловушки
Неверный путь к файлу или неверный пользовательский ввод могут быть указаны как значение ошибки, чтобы вызывающая сторона могла восстановиться. Ловушка, вызванная недопустимым счетчиком сдвига во время выполнения или преобразованием чисел с плавающей запятой в целое число, представляет собой другой механизм.
Программы, которые должны продолжать выполнение, должны проверять вводимые данные перед опасными операциями. assert также не предназначен для злоупотреблений в качестве средства обработки корректных ошибок пользовательского ввода. Если вам нужно дать пользователю возможность повторно ввести свои данные, верните результаты, чтобы продолжить поток управления.
Упражнение и полное решение
Напишите функцию, которая считывает элемент массива по индексу и завершает работу с ошибкой, когда индекс отрицательный или больше или равен длине. Используйте структуру результата, поскольку ноль может быть допустимым значением элемента.
struct Lookup {
ok: bool;
value: i32;
}
fun at(values: ptr<i32>, length: i32, index: i32) -> Lookup {
if (index < 0 || index >= length) {
return Lookup {
ok: false,
value: 0
};
}
return Lookup {
ok: true,
value: deref values[index]
};
}
fun main() {
var values: array<i32, 3> = [0, 10, 20];
var first: Lookup = at(&values[0], 3, 0);
var outside: Lookup = at(&values[0], 3, 3);
if (first.ok) {
println("value={}", first.value);
}
if (!outside.ok) {
println("out of bounds");
}
}
Результат выполнения:
value=0
out of bounds
Условием вызывающей стороны является то, что указатель и length представляют фактический читаемый массив. Это не функция, которая делает случайные адреса безопасными, просто проверяя индекс. Прочитайте отдельно, за какие проверки отвечает функция и какие условия должна гарантировать вызывающая сторона.