Обработка ошибок

Освойте мощную систему обработки ошибок Топаза. Изучите безопасные подходы к обработке ошибок с использованием типов Result, Option, сопоставления образцов и стратегий оптимизации обработки исключений.

Обработка ошибок является основой надежного программного обеспечения. Топаз предоставляет типы Result и Option для типобезопасной обработки ошибок. 🛡️

Принцип «сначала Result»
В Топазе v4 используйте Result вместе с ?/try. throw сохраняется лишь как совместимая оболочка для внешних API.

🎯 Тип Result

Основное использование Result

// Result<ТипУспеха, ТипОшибки>
function разделить(числитель: int, знаменатель: int) -> Result<float, string> {
    if знаменатель == 0 {
        return Err("Нельзя делить на ноль")
    }
    return Ok(числитель / знаменатель)
}

// Обработка Result
let результат = разделить(10, 2)
match результат {
    case Ok(значение) => print("Результат: {значение}")
    case Err(сообщениеОшибки) => print("Ошибка: {сообщениеОшибки}")
}

// Различные случаи ошибок
function читатьФайл(путь: string) -> Result<string, string> {
    if путь.length() == 0 {
        return Err("Путь к файлу пуст")
    }
    
    if !путь.endsWith(".txt") {
        return Err("Поддерживаются только текстовые файлы")
    }
    
    if !файлСуществует(путь) {
        return Err("Файл не найден: {путь}")
    }
    
    // Фактическая логика чтения файла
    return Ok("Содержимое файла")
}

Цепочки Result

// Преобразование успешного значения с помощью map
let результат = разделить(10, 2)
    .map(значение => значение * 2)
    .map(значение => "Результат: {значение}")

match результат {
    case Ok(сообщение) => print(сообщение)     // "Результат: 10"
    case Err(ошибка) => print("Ошибка: {ошибка}")
}

// Цепочка обработки с andThen
function квадратныйКорень(значение: float) -> Result<float, string> {
    if значение < 0 {
        return Err("Нельзя вычислить квадратный корень отрицательного числа")
    }
    return Ok(Math.sqrt(значение))
}

let финальныйРезультат = разделить(100, 4)
    .andThen(значение => квадратныйКорень(значение))
    .map(значение => "Квадратный корень: {значение}")

print(финальныйРезультат)  // Ok("Квадратный корень: 5")

// Восстановление после ошибки с orElse
function использоватьПоУмолчанию(ошибка: string) -> Result<float, string> {
    print("Использование значения по умолчанию: {ошибка}")
    return Ok(0.0)
}

let восстановленныйРезультат = разделить(10, 0)
    .orElse(использоватьПоУмолчанию)
    
print(восстановленныйРезультат)  // Ok(0.0)

Сложные типы ошибок

// Структурированные типы ошибок
enum ОшибкаФайла {
    НеНайден(путь: string),
    ОтказВДоступе(путь: string),
    ОшибкаЧтения(причина: string),
    ОшибкаФормата(ожидалось: string, получено: string)
}

function читатьФайлКонфигурации(путь: string) -> Result<{ [string]: any }, ОшибкаФайла> {
    if !файлСуществует(путь) {
        return Err(ОшибкаФайла.НеНайден(путь: путь))
    }
    
    if !естьПравоЧтения(путь) {
        return Err(ОшибкаФайла.ОтказВДоступе(путь: путь))
    }
    
    match читатьСодержимоеФайла(путь) {
        case Ok(содержимое) => {
            match JSON.parse(содержимое) {
                case Ok(конфигурация) => return Ok(конфигурация)
                case Err(_) => return Err(ОшибкаФайла.ОшибкаФормата(
                    ожидалось: "JSON",
                    получено: "Неверный формат"
                ))
            }
        }
        case Err(причина) => return Err(ОшибкаФайла.ОшибкаЧтения(причина: причина))
    }
}

// Специфическая обработка ошибок
match читатьФайлКонфигурации("config.json") {
    case Ok(конфигурация) => {
        print("Конфигурация загружена успешно")
        print("Тема: {конфигурация.theme}")
    }
    case Err(ОшибкаФайла.НеНайден(путь: путь)) => {
        print("Файл конфигурации не найден: {путь}")
        создатьКонфигурациюПоУмолчанию()
    }
    case Err(ОшибкаФайла.ОтказВДоступе(путь: путь)) => {
        print("Нет разрешения на чтение файла: {путь}")
    }
    case Err(ОшибкаФайла.ОшибкаФормата(ожидалось: ожидалось, получено: получено)) => {
        print("Ошибка формата - Ожидалось: {ожидалось}, Получено: {получено}")
    }
    case Err(ОшибкаФайла.ОшибкаЧтения(причина: причина)) => {
        print("Не удалось прочитать файл: {причина}")
    }
}

🔍 Тип Option

Основное использование Option

// Option<T> - значение может существовать или не существовать
function найтиПользователя(id: int) -> Option<{ имя: string, возраст: int }> {
    let пользователи = [
        { id: 1, имя: "ТопазДев", возраст: 25 },
        { id: 2, имя: "КодМастер", возраст: 30 }
    ]
    
    for пользователь in пользователи {
        if пользователь.id == id {
            return Some({ имя: пользователь.имя, возраст: пользователь.возраст })
        }
    }
    
    return None
}

// Обработка Option
match найтиПользователя(1) {
    case Some(пользователь) => print("Пользователь найден: {пользователь.имя}, {пользователь.возраст} лет")
    case None => print("Пользователь не найден")
}

// Поиск элемента в массиве
function первыйЭлемент<T>(массив: [T]) -> Option<T> {
    if массив.length() > 0 {
        return Some(массив[0])
    }
    return None
}

let числа = [1, 2, 3, 4, 5]
match первыйЭлемент(числа) {
    case Some(первый) => print("Первый элемент: {первый}")
    case None => print("Массив пуст")
}

Цепочки методов Option

// Преобразование значения с помощью map
let результат = найтиПользователя(1)
    .map(пользователь => пользователь.имя.toUpperCase())
    .map(имя => "Привет, {имя}!")

match результат {
    case Some(приветствие) => print(приветствие)
    case None => print("Пользователь не найден")
}

// Цепочка Option с andThen
function проверитьВзрослость(пользователь: { имя: string, возраст: int }) -> Option<string> {
    if пользователь.возраст >= 18 {
        return Some(пользователь.имя)
    }
    return None
}

let взрослыйПользователь = найтиПользователя(1)
    .andThen(проверитьВзрослость)
    .map(имя => "{имя} совершеннолетний")

// Предоставление значения по умолчанию с unwrapOr
let имяПользователя = найтиПользователя(999)
    .map(пользователь => пользователь.имя)
    .unwrapOr("Неизвестный пользователь")

print("Имя пользователя: {имяПользователя}")  // "Имя пользователя: Неизвестный пользователь"

// Фильтрация по условию
let молодойПользователь = найтиПользователя(1)
    .filter(пользователь => пользователь.возраст < 30)
    .map(пользователь => "{пользователь.имя} молод")

Комбинирование Option и Result

// Преобразование Option в Result
function требуемыйПользователь(id: int) -> Result<{ имя: string, возраст: int }, string> {
    return найтиПользователя(id)
        .okOr("Пользователь не найден: ID {id}")
}

// Преобразование Result в Option
let опциональныйРезультат = разделить(10, 0).ok()  // Ok становится Some, Error становится None

match опциональныйРезультат {
    case Some(значение) => print("Деление успешно: {значение}")
    case None => print("Деление не удалось (сообщение об ошибке игнорируется)")
}

// Сложная обработка
function удвоитьВозрастПользователя(id: int) -> Result<int, string> {
    return требуемыйПользователь(id)
        .map(пользователь => пользователь.возраст * 2)
}

match удвоитьВозрастПользователя(1) {
    case Ok(удвоенныйВозраст) => print("Удвоенный возраст: {удвоенныйВозраст}")
    case Err(ошибка) => print("Ошибка: {ошибка}")
}

🔗 Распространение ошибок и досрочный возврат

Оператор ? (распространение ошибок)

// Автоматическое распространение ошибок с оператором ?
function сложноеВычисление(a: int, b: int, c: int) -> Result<float, string> {
    let первое = разделить(a, b)?          // Автоматический возврат при ошибке
    let второе = квадратныйКорень(первое)?  // Автоматический возврат при ошибке
    let финальное = разделить(второе, c)?   // Автоматический возврат при ошибке
    
    return Ok(финальное)
}

// Код выше эквивалентен:
function сложноеВычисление_ручное(a: int, b: int, c: int) -> Result<float, string> {
    let первое = match разделить(a, b) {
        case Ok(значение) => значение
        case Err(ошибка) => return Err(ошибка)
    }
    
    let второе = match квадратныйКорень(первое) {
        case Ok(значение) => значение
        case Err(ошибка) => return Err(ошибка)
    }
    
    let финальное = match разделить(второе, c) {
        case Ok(значение) => значение
        case Err(ошибка) => return Err(ошибка)
    }
    
    return Ok(финальное)
}

// Оператор ? также можно использовать с Option
function суммаПервогоИВторого(массив: [int]) -> Option<int> {
    let первый = первыйЭлемент(массив)?
    let второй = первыйЭлемент(массив.slice(1))?
    return Some(первый + второй)
}

Блок try (экспериментальный)

// Группировка нескольких операций с блоком try
function обработатьДанные(путьКФайлу: string) -> Result<{ общее: int, среднее: float }, string> {
    try {
        let содержимое = читатьФайл(путьКФайлу)?
        let числа = содержимое.split(",")
            .map(s => parseInt(s.trim()))
            .collect()
        
        let общее = числа.sum()
        let среднее = разделить(общее, числа.length())?
        
        Ok({ общее: общее, среднее: среднее })
    }
}

// Вложенная обработка try
function вложеннаяОбработка() -> Result<string, string> {
    let результат1 = try {
        let a = разделить(10, 2)?
        let b = квадратныйКорень(a)?
        Ok(b)
    }?
    
    let результат2 = try {
        let c = разделить(результат1, 3)?
        Ok(c * 2)
    }?
    
return Ok("Финальный результат: {результат2}")
}

defer (охранники области видимости)

v4

Регистрируйте действия очистки для выполнения при выходе из текущей области. Порядок LIFO. Выполняется при любом выходе: обычном завершении, return, раннем возврате через ? и при ошибках.

Политика: ошибки внутри defer по умолчанию логируются и не распространяются; оставшиеся отложенные действия продолжают выполняться.

function обработатьФайл(путь: string) -> Result<string, string> {
    let файл = File.open(путь)?
    defer { файл.close() }

    let блокировка = файл.lockExclusive()?  // захват ресурса
    defer { блокировка.release() }          // освобождение гарантировано

    let содержимое = файл.read()?           // работа с ресурсом
    Ok(содержимое)
}

🎨 Паттерны обработки ошибок

Добавление контекста ошибки

// Добавление контекстной информации к ошибкам
function загрузитьДанныеПользователя(id: int) -> Result<{ имя: string, настройки: any }, string> {
    let пользователь = требуемыйПользователь(id)
        .mapErr(ошибка => "Поиск пользователя не удался (ID: {id}): {ошибка}")?
    
    let настройки = читатьФайлКонфигурации("user_{id}.json")
        .mapErr(ошибка => "Загрузка настроек пользователя не удалась (ID: {id}): {ошибка}")?
    
    return Ok({ имя: пользователь.имя, настройки: настройки })
}

// Пошаговая обработка ошибок
function пошаговаяОбработка() -> Result<string, string> {
    print("Шаг 1: Проверка данных...")
    let результатПроверки = проверитьДанные().mapErr(ошибка => "Шаг 1 не удался: {ошибка}")?
    
    print("Шаг 2: Преобразование данных...")
    let результатПреобразования = преобразоватьДанные(результатПроверки).mapErr(ошибка => "Шаг 2 не удался: {ошибка}")?
    
    print("Шаг 3: Сохранение данных...")
    let результатСохранения = сохранитьДанные(результатПреобразования).mapErr(ошибка => "Шаг 3 не удался: {ошибка}")?
    
    return Ok("Все шаги завершены: {результатСохранения}")
}

Обработка частичных сбоев

// Разрешение частичных сбоев в нескольких операциях
function пакетнаяОбработка(списокФайлов: [string]) -> { успешные: [string], неудачные: [{ файл: string, ошибка: string }] } {
    let mut списокУспешных = []
    let mut списокНеудачных = []
    
    for файл in списокФайлов {
        match обработатьФайл(файл) {
            case Ok(результат) => {
                списокУспешных.push(результат)
                print("Успех: {файл}")
            }
            case Err(ошибка) => {
                списокНеудачных.push({ файл: файл, ошибка: ошибка })
                print("Неудача: {файл} - {ошибка}")
            }
        }
    }
    
    return { успешные: списокУспешных, неудачные: списокНеудачных }
}

// Обработка массивов Result
function собратьВсеРезультаты<T, E>(результаты: [Result<T, E>]) -> Result<[T], [E]> {
    let mut успешныеЗначения = []
    let mut ошибки = []
    
    for результат in результаты {
        match результат {
            case Ok(значение) => успешныеЗначения.push(значение)
            case Err(ошибка) => ошибки.push(ошибка)
        }
    }
    
    if ошибки.length() > 0 {
        return Err(ошибки)
    }
    
    return Ok(успешныеЗначения)
}

let результатыВычислений = [
    разделить(10, 2),   // Ok(5)
    разделить(8, 4),    // Ok(2)
    разделить(6, 0)     // Err("Нельзя делить на ноль")
]

match собратьВсеРезультаты(результатыВычислений) {
    case Ok(значения) => print("Все вычисления успешны: {значения}")
    case Err(ошибки) => print("Некоторые вычисления не удались: {ошибки}")
}

Паттерн повторных попыток

// Логика повторных попыток
function повторнаяПопытка<T, E>(
    операция: function() -> Result<T, E>,
    максПопыток: int,
    задержка: int = 1000
) -> Result<T, E> {
    let mut попытки = 0
    
    while попытки < максПопыток {
        попытки += 1
        
        match операция() {
            case Ok(результат) => {
                if попытки > 1 {
                    print("Успех на попытке {попытки}")
                }
                return Ok(результат)
            }
            case Err(ошибка) => {
                print("Попытка {попытки} не удалась: {ошибка}")
                
                if попытки < максПопыток {
                    print("Повтор через {задержка}мс...")
                    Thread.sleep(задержка)
                } else {
                    return Err(ошибка)
                }
            }
        }
    }
    
    return Err("Превышено максимальное количество попыток")
}

// Повторная попытка сетевого запроса
let ответ = повторнаяПопытка(
    операция: () => сетевойЗапрос("https://api.example.com/data"),
    максПопыток: 3,
    задержка: 2000
)

match ответ {
    case Ok(данные) => print("Данные получены: {данные}")
    case Err(ошибка) => print("Окончательная неудача: {ошибка}")
}

🛡️ Типобезопасность и проверка во время компиляции

Принуждение к обработке всех случаев ошибок

// Компилятор принуждает к обработке всех случаев
enum СетеваяОшибка {
    СбойПодключения,
    Таймаут,
    ОшибкаАутентификации,
    ОшибкаСервера(код: int)
}

function обработатьСетевойЗапрос(результат: Result<string, СетеваяОшибка>) -> string {
    match результат {
        case Ok(данные) => "Успех: {данные}"
        case Err(СетеваяОшибка.СбойПодключения) => "Не удается подключиться к сети"
        case Err(СетеваяОшибка.Таймаут) => "Истекло время ожидания запроса"
        case Err(СетеваяОшибка.ОшибкаАутентификации) => "Требуется аутентификация"
        case Err(СетеваяОшибка.ОшибкаСервера(код: код)) => "Ошибка сервера (код: {код})"
        // Ошибка компиляции, если не обработаны все случаи!
    }
}

// Тип Never для невозможных ситуаций
function никогдаНеСломается() -> Result<string, Never> {
    return Ok("Всегда успешно")
}

// Never не нужно сопоставлять
let результат = никогдаНеСломается()
let значение = match результат {
    case Ok(значение) => значение
    // Случай Error невозможен, поэтому может быть опущен
}

Пользовательские трейты ошибок

// Трейт для обогащения информации об ошибках
trait ИнформацияОбОшибке {
    function сообщение() -> string
    function код() -> int
    function восстановимая() -> bool
}

enum ОшибкаПриложения: ИнформацияОбОшибке {
    СбойПодключенияКБазеДанных,
    ОтказВДоступеКФайлу,
    НеверныйВвод(поле: string, значение: string),
    НедоступнаВнешняяУслуга
}

impl ИнформацияОбОшибке for ОшибкаПриложения {
    function сообщение() -> string {
        match self {
            case ОшибкаПриложения.СбойПодключенияКБазеДанных => "Не удается подключиться к базе данных"
            case ОшибкаПриложения.ОтказВДоступеКФайлу => "Нет разрешения на доступ к файлу"
            case ОшибкаПриложения.НеверныйВвод(поле: поле, значение: значение) => "Неверный ввод - {поле}: {значение}"
            case ОшибкаПриложения.НедоступнаВнешняяУслуга => "Внешняя служба недоступна"
        }
    }
    
    function код() -> int {
        match self {
            case ОшибкаПриложения.СбойПодключенияКБазеДанных => 1001
            case ОшибкаПриложения.ОтказВДоступеКФайлу => 1002
            case ОшибкаПриложения.НеверныйВвод(_, _) => 1003
            case ОшибкаПриложения.НедоступнаВнешняяУслуга => 1004
        }
    }
    
    function восстановимая() -> bool {
        match self {
            case ОшибкаПриложения.СбойПодключенияКБазеДанных => true
            case ОшибкаПриложения.НедоступнаВнешняяУслуга => true
            case _ => false
        }
    }
}

// Интегрированная обработка ошибок
function логироватьОшибку(ошибка: ОшибкаПриложения) {
    print("[Ошибка {ошибка.код()}] {ошибка.сообщение()}")
    
    if ошибка.восстановимая() {
        print("Можно попытаться восстановить")
    } else {
        print("Невосстановимая ошибка")
    }
}

🎯 Оптимизация и лучшие практики

1. Обращение с ошибками как со значениями

// Хорошо: Явная обработка ошибок
function безопасныйВводПользователя() -> Result<int, string> {
    let ввод = input("Введите число: ")
    
    match parseInt(ввод) {
        case Ok(число) if число >= 0 => Ok(число)
        case Ok(_) => Err("Отрицательные числа не допускаются")
        case Err(_) => Err("Не является допустимым числом: {ввод}")
    }
}

// Плохо: Выброс исключений (не рекомендуется в Топазе)
function опасныйВводПользователя() -> int {
    let ввод = input("Введите число: ")
    let число = parseInt(ввод).unwrap()  // Может вызвать панику!
    return число
}

2. Оптимизация распространения ошибок

// Эффективное распространение ошибок
function конвейерДанных(ввод: string) -> Result<string, string> {
    ввод
        .trim()
        .nonEmpty().okOr("Ввод пуст")?
        .validate().mapErr(ошибка => "Проверка не удалась: {ошибка}")?
        .transform().mapErr(ошибка => "Преобразование не удалось: {ошибка}")?
        .process().mapErr(ошибка => "Обработка не удалась: {ошибка}")
}

// Предотвращение глубокой вложенности с помощью досрочных возвратов
function регистрироватьПользователя(данные: { имя: string, email: string, возраст: int }) -> Result<string, string> {
    // Ранняя проверка
    if данные.имя.length() == 0 {
        return Err("Требуется имя")
    }
    
    if !данные.email.contains("@") {
        return Err("Требуется действительный email")
    }
    
    if данные.возраст < 0 {
        return Err("Требуется действительный возраст")
    }
    
    // Обработка только после прохождения всех проверок
    let пользователь = создатьПользователя(данные)?
    let id = сохранитьВБазуДанных(пользователь)?
    
    return Ok("Регистрация пользователя завершена: {id}")
}

3. Логирование и мониторинг ошибок

// Структурированное логирование ошибок
function обработатьСЛогированием<T, E>(
    результат: Result<T, E>,
    контекст: string,
    уровеньЛога: string = "error"
) -> Result<T, E> {
    match результат {
        case Ok(значение) => {
            if уровеньЛога == "debug" {
                print("[DEBUG] {контекст}: Успех")
            }
            return Ok(значение)
        }
        case Err(ошибка) => {
            print("[{уровеньЛога.toUpperCase()}] {контекст}: {ошибка}")
            
            // Собрать метрики
            увеличитьМетрикуОшибок(контекст, уровеньЛога)
            
            return Err(ошибка)
        }
    }
}

// Пример использования
let результат = запросКБазеДанных("SELECT * FROM users")
    |> обработатьСЛогированием(контекст: "Запрос списка пользователей", уровеньЛога: "error")

// Агрегация ошибок и оповещения
function системаОповещенийОбОшибках() {
    let недавниеОшибки = собратьЛогиОшибок(последниеЧасы: 1)  // 1 час
    
    if недавниеОшибки.length() > 100 {
        отправитьОповещение("Обнаружен высокий уровень ошибок: {недавниеОшибки.length()} ошибок/час")
    }
    
    let критическиеОшибки = недавниеОшибки.filter(ошибка => ошибка.уровень == "critical")
    if критическиеОшибки.length() > 0 {
        отправитьСрочноеОповещение("Произошли критические ошибки", критическиеОшибки)
    }
}

Обработка ошибок в Топазе типобезопасна и явна. Используйте Result и Option для создания предсказуемых и надежных приложений! 🚀