Обработка ошибок является основой надежного программного обеспечения. Топаз предоставляет типы 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 для создания предсказуемых и надежных приложений! 🚀