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 представляют фактический читаемый массив. Это не функция, которая делает случайные адреса безопасными, просто проверяя индекс. Прочитайте отдельно, за какие проверки отвечает функция и какие условия должна гарантировать вызывающая сторона.