Одним из факторов, влияющих на надёжность программного обеспечения, является способ обрабатывать ошибки, возникающие в процессе выполнения. Создатели Rust не стали повторять популярные методы, а выбрали другой способ, позволяющий описывать и обрабатывать ошибки более явно. В статье мы рассмотрим реализацию данного подхода, а также полезные библиотеки, упрощающие обработку ошибок.
Содержание
Что делать с ошибкой?
Для начала, порассуждаем о возможных вариантах действий при возникновении ошибки в ходе выполнения программы. Вариантов у нас, в конечном счёте, всего три:
Завершить работу программы. Это самый простой вариант, не требующий больших усилий от разработчика. Он применим в случаях, когда ошибка не позволяет программе корректно выполнять свои функции. В качестве примера можно рассмотреть приложение, представляющее собой обёртку над некоторой динамической библиотекой. Скажем, графический интерфейс. Приложение поставляется с этой библиотекой и не несёт какой-либо пользы в отрыве от неё. Разумно предположить, что приложение не должно работать без этой библиотеки. Поэтому, вполне обосновано, при ошибке загрузки библиотеки, прерывать работу приложения.
Обработать ошибку. Чтобы программа могла продолжить выполнение после возникновения ошибки, требуется отреагировать на эту ошибку так, чтобы корректная часть программы могла далее выполнять свои функции, потеряв, возможно, доступ к некоторым возможностям. Рассмотрим приложение, использующее модули в виде динамических библиотек. В данном случае, отсутствие библиотеки модуля, необходимого для выполнения выбранного пользователем действия - это повод отменить выполнение действия, а не прерывать программу. Как вариант, сообщим пользователю об отсутствии требуемого модуля и предложим другие варианты работы.
Пропустить ошибку на более высокий уровень. Далеко не всегда, в момент получения ошибки, есть возможность однозначно выбрать способ её обработки. В таких случаях можно передать ответственность по обработке ошибки выше по иерархии вызовов. Например, подсистема загрузки конфигурационных файлов может использоваться сразу в нескольких других системах приложения. Поэтому не разумно обрабатывать случай отсутствия запрошенного файла внутри неё, одинаково для всех обратившихся. Более подходящий вариант - предоставить каждой клиентской системе самой решать, как действовать в случае ошибки загрузки конфигурации.
Ошибки, после которых приложение должно завершить работу называют неустранимыми. Остальные - устранимыми. Тип конкретной ошибки не зависит от самой ошибки (некорректный ввод, файл не найден, ...). Он зависит от решения разработчика: стоит ли продолжать работу программы при этой ошибке, или программа больше ничего не может сделать. Нужно искать компромисс, исходя из требований к надёжности системы и имеющимися ресурсами для разработки, так как восстановление после ошибки требует от разработчика некоторых усилий. В лучшем случае, достаточно просто сообщить о ней пользователю и продолжить работу. Но бывают ситуации, когда для восстановления от ошибки требуется создать целую резервную систему.
Механизм обработки ошибок в Rust требует явно указывать, как вы классифицируете каждую ошибку. Для того чтобы разобраться, как этот механизм устроен, давайте рассмотрим некоторые особенности синтаксиса Rust, которые в нём применяются.
Немного о синтаксисе Rust
Механизм обработки ошибок включает себя две особенности языка Rust: перечисления с данными и трейты.
Трейты
Трейты схожи с концепцией интерфейсов в других языках. Их можно реализовывать на типах, расширяя их функционал. Также, функции могут накладывать ограничение на трейты принимаемых аргументов. Ограничения проверяются при компиляции. Например:
fn main() {
let int_value: i32 = 42;
let float_value: f32 = 42.0;
print_value(int_value);
// Строка ниже ломает компиляцию, так как для float не реализован трейт Print
// print_value(float_value);
}
trait Print {
fn print(&self);
}
impl Print for i32 {
fn print(&self) {
println!("Printing i32: {}", self)
}
}
fn print_value<T: Print>(value: T) {
value.print()
}
В данном примере мы определили трейт Print
и реализовали его для встроенного целочисленного типа i32
. Также, мы определили функцию print_value()
, принимающую обобщённый (generic) аргумент value
, ограничив варианты его типа только теми, которые реализуют трейт Print
. Поэтому в main()
мы можем вызвать print_value()
только с i32
аргументом.
Более того, при определённых условиях, можно создавать трейт объекты (trait objects). Это динамический объекты, которые могут быть созданы из любого типа, реализующего данный трейт. Конкретная реализация метода трейта выбирается динамически (dynamic dispatch). Например:
trait Animal {
fn says(&self);
}
struct Cat {}
struct Dog {}
impl Animal for Cat {
fn says(&self) {
println!("Meow")
}
}
impl Animal for Dog {
fn says(&self) {
println!("Woof")
}
}
fn main() {
let cat = Cat{};
let dog = Dog{};
say_something(&cat);
say_something(&dog);
}
fn say_something(animal: &dyn Animal) {
animal.says()
}
В данном коде нет необходимости делать функцию say_something()
обобщённой, так как конкретная реализация, скрытая за трейт объектом разрешается во время выполнения программы, а не при компиляции.
Также, стоит упомянуть о том, что трейты могут наследоваться. То что трейт Mammal
унаследован от трейта Animal
означает, что реализовать трейт Mammal
может только тип, реализующий Animal
.
trait Animal {}
trait Mammal: Animal {}
struct Cat {}
struct Dog {}
impl Animal for Cat {}
impl Mammal for Cat {}
impl Mammal for Dog {}
Данный код не компилируется, так как мы пытаемся реализовать трейт Mammal
на типе Dog
, не реализовав Animal
, от которого Mammal
унаследован.
Перечисления с данными
Данный элемент синтаксиса позволяет привязать данные разных типов к разным вариантам перечисления. Например, вы можете принимать в качестве аргумента IP адрес, не уточняя версию:
enum IpAddr {
IPv4(u32),
IPv6(String),
}
fn connect(addr: IpAddr) {
match addr {
IpAddr::IPv4(integer_address) => {...}
IpAddr::IPv6(string_address) => {...}
}
}
Ключевое слово match
позволяет описать действия для различных вариантов перечисления и их содержимого.
Перечисления могут быть обобщенными:
struct DateFormatA {}
struct DateFormatB {}
enum Date<DateFormat> {
InFormat(DateFormat),
AsOffset(u32)
}
fn in_format_a() -> Date<DateFormatA> {
Date::InFormat(DateFormatA {})
}
fn in_format_b() -> Date<DateFormatB> {
Date::AsOffset(42)
}
fn main() {
let _a = in_format_a();
let _b = in_format_b();
}
Разобравшись с типажами и перечислениями, можно переходить к механизму обработки ошибок.
Обработка ошибок в Rust
В Rust есть два перечисления на которых строится, практически, вся обработка ошибок: Option
и Result
. Рассмотрим их подробнее.
Option
Определение:
pub enum Option<T> {
None,
Some(T),
}
Семантика его проста: либо мы имеем некоторые данные, либо они отсутствуют. Таким образом, возвращая из функции Option
мы, тем самым, выражаем мысль, что, возможно, мы не получим ожидаемый результат.
Result
Определение:
pub enum Result<T, E> {
Ok(T),
Err(E),
}
В отличие от Option
, Result
позволяет установить не только отсутствие данных, но и причину, в связи с которой они отсутствуют.
Рассмотрим теперь, как в Rust выразить три действия при ошибке, которые мы перечислили в начале статьи:
Завершить работу приложения.
Обработать ошибку.
Пропустить ошибку на более высокий уровень.
Завершаем работу приложения
Rust требует от разработчика явно демонстрировать своё намерение прервать программу в случае ошибки. Аварийное завершение работы программы в Rust называется паникой. Вызвать её можно с помощью макроса panic!()
, позволяющего указать сообщения об ошибке для вывода.
fn main() {
let broken = true;
if broken {
panic!("Program is broken!")
}
}
Так как для обработки ошибок, обычно, используются Option
и Result
, для завершения работы программы нужно писать что-то вроде:
match opt {
Some(value) => value,
None => panic!("No value in option!")
};
match res {
Ok(value) => value,
Err(error) => panic!("Error happaned: {}!", error)
};
Для удобства, Option
и Result
содержат ассоциированную функцию unwrap()
, позволяющую не повторять приведённый выше код. Если перечисление находится в состоянии успеха, то unwrap()
достаёт данные из перечисления и позволяет с ними работать. В случае ошибки, unwrap()
вызывает панику. У unwrap()
есть аналог, позволяющий добавить произвольный текст к выводу: expect()
.
fn main() {
let settings = read_settings().unwrap();
let _service = create_service(settings).expect("Can't create service");
}
fn read_settings() -> Option<String> {
// Some settings loading code
None // Settings is not found
}
struct Service {}
#[derive(Debug)] // Generate implementation for Debug trait, required by .expect()
enum CreateServiceError {
BadSettings,
InternalError,
}
fn create_service(_settings: String) -> Result<String, CreateServiceError> {
// Some service creation code
let bad_settings = true;
if bad_settings {
Err(CreateServiceError::BadSettings)
} else {
Err(CreateServiceError::InternalError)
}
}
Обрабатываем ошибку
Вызывая функцию, которая может не сработать, мы получаем в качестве результата Option
или Result
. Если нам известно, что делать в случае неудачи, мы должны выразить свои намерения через конструкции языка. Рассмотрим пример:
fn main() {
let some_settings = String::from("some settings");
let s1 = match load_settings() {
Some(s) => s,
None => some_settings.clone(),
};
let s2 = load_settings().unwrap_or_default();
let s3 = load_settings().unwrap_or(some_settings);
let s4 = load_settings().unwrap_or_else(|| {String::from("new string")});
println!("s1: {}", s1);
println!("s2: {}", s2);
println!("s3: {}", s3);
println!("s4: {}", s4);
}
fn load_settings() -> Option<String> {
None
}
В данном примере мы используем разные способы замены строки настроек, в случае неудачи при её получении:
s1 - явно сопоставляем
Option
с шаблоном и указываем альтернативу.s2 - используем функцию
unwrap_or_default()
, которая в случае отсутствия данных возвращает значение по умолчанию (пустую строку).s3 - используем
unwrap_or()
, возвращающую свой аргумент в случае отсутствия данных.s4 - используем
unwrap_or_else()
, возвращающую результат вызова переданного в неё функтора в случае отсутствия данных. Такой подход позволяет вычислять значение резервного варианта не заранее, а только в случае пустогоOption
.
Перечисление Result
предоставляет аналогичные методы.
Пропускаем ошибку выше
Для начала, сделаем это вручную. Для Option
:
fn main() {
let module = init_module().unwrap();
}
struct Module {
settings: String,
dll: Dll,
}
struct Dll {}
fn init_module() -> Option<Module> {
let settings = match load_settings() {
Some(s) => s,
None => return None,
};
let dll = match load_dll() {
Some(dll) => dll,
None => return None,
};
Some(Module { settings, dll })
}
fn load_settings() -> Option<String> {
None
}
fn load_dll() -> Option<Dll> {
None
}
И для Result
:
fn main() {
let module = init_module();
}
struct Module {
settings: String,
dll: Dll,
}
struct Dll {}
enum InitModuleError {
SettingsError(LoadSettingsError),
DllError(LoadDllError),
}
fn init_module() -> Result<Module, InitModuleError> {
let settings = match load_settings() {
Ok(s) => s,
Err(e) => return Err(InitModuleError::SettingsError(e)),
};
let dll = match load_dll() {
Ok(dll) => dll,
Err(e) => return Err(InitModuleError::DllError(e)),
};
Ok(Module { settings, dll })
}
struct LoadSettingsError {}
fn load_settings() -> Result<String, LoadSettingsError> {
Err(LoadSettingsError {})
}
struct LoadDllError {}
fn load_dll() -> Result<Dll, LoadDllError> {
Err(LoadDllError {})
}
Как видно в примерах, такой подход требует большого количества match
конструкций. Это усложняет код, ухудшает его читабельность и добавляет разработчику дополнительной рутинной работы. Во избежание всего этого, создатели языка ввели оператор ?
. Расположенный после Option
или Result
, он заменяет собой match
конструкцию. В случае наличия значения, он возвращает его для дальнейшего использования. В случае ошибки, возвращает её из функции. Воспользуемся им в наших примерах. Для Option
всё очевидно:
fn main() {
let module = init_module().unwrap();
}
struct Module {
settings: String,
dll: Dll,
}
struct Dll {}
fn init_module() -> Option<Module> {
let settings = load_settings()?;
let dll = load_dll()?;
Some(Module { settings, dll })
}
fn load_settings() -> Option<String> {
None
}
fn load_dll() -> Option<Dll> {
None
}
Для Result всё обстоит немного сложнее. Ведь в случае, если происходит LoadDllError
, то компилятору нужно как-то преобразовать её в InitModuleError
для возврата из функции. Для этого оператор ?
пытается найти способ преобразования для этих ошибок. Для того, чтобы создать такой способ, в стандартной библиотеке существует трейт From
. Воспользуемся им:
fn main() {
let module = init_module();
}
struct Module {
settings: String,
dll: Dll,
}
struct Dll {}
enum InitModuleError {
SettingsError(LoadSettingsError),
DllError(LoadDllError),
}
impl From<LoadSettingsError> for InitModuleError {
fn from(e: LoadSettingsError) -> InitModuleError {
InitModuleError::SettingsError(e)
}
}
impl From<LoadDllError> for InitModuleError {
fn from(e: LoadDllError) -> InitModuleError {
InitModuleError::DllError(e)
}
}
fn init_module() -> Result<Module, InitModuleError> {
let settings = load_settings()?;
let dll = load_dll()?;
Ok(Module { settings, dll })
}
struct LoadSettingsError {}
fn load_settings() -> Result<String, LoadSettingsError> {
Err(LoadSettingsError {})
}
struct LoadDllError {}
fn load_dll() -> Result<Dll, LoadDllError> {
Err(LoadDllError {})
}
Иными словами, Rust требует явно описывать способы преобразования ошибок друг в друга при передаче их верхним уровням иерархии вызовов.
Динамические ошибки
В случае, если нет необходимости использовать конкретный тип ошибки, а достаточно просто иметь текстовое сообщение о ней, то можно передавать ошибку в виде трейт объекта std::error::Error
, завёрнутого в умный указатель Box
(подробнее). Трейт Error
определён так:
trait Error: Debug + Display {...}
Как видно из определения, он требует реализации трейтов Debug
и Display
. Таким образом, Rust вводит требования для всех типов реализующих Error
: уметь выводить отладочную и текстовую информацию о себе. Рассмотрим на примере:
use std::fmt;
use std::error::Error;
fn main() {
init_module().unwrap();
}
struct Module {
settings: String,
dll: Dll,
}
struct Dll {}
fn init_module() -> Result<Module, Box<dyn Error>> {
let settings = load_settings()?;
let dll = load_dll()?;
Ok(Module { settings, dll })
}
#[derive(Debug)]
struct LoadSettingsError {}
impl Error for LoadSettingsError {}
impl fmt::Display for LoadSettingsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "load settings error")
}
}
fn load_settings() -> Result<String, Box<dyn Error>> {
Err(Box::new(LoadSettingsError {}))
}
#[derive(Debug)]
struct LoadDllError {}
impl Error for LoadDllError {}
impl fmt::Display for LoadDllError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "load dll error")
}
}
fn load_dll() -> Result<Dll, Box<dyn Error>> {
Err(Box::new(LoadDllError {}))
}
Как видно из примера, при использовании динамических ошибок, нет необходимости создавать промежуточные типы ошибок, объединяющие несколько типов ошибок нижнего уровня. У такого подхода есть свои недостатки. Во-первых, отсутствует возможность определить конкретный тип ошибки, произошедшей на нижнем уровне. Во-вторых, снижается производительность, так как для создания ошибки требуется аллокация в куче, а, при выводе сообщения об ошибке, используется динамическая диспетчеризация.
Полезные библиотеки
Рассмотрим две популярные библиотеки, упрощающие обработку ошибок: thiserror и anyhow.
thiserror
Данная библиотека предоставляет макросы, позволяющие упростить рутинные действия: описание способов конвертации ошибок через From
, и реализация трейтов Error
и Display
. Рассмотрим на примере:
use thiserror::Error; // 1.0.29
fn main() {
let module = init_module().unwrap();
}
struct Module {
settings: String,
dll: Dll,
}
struct Dll {}
#[derive(Debug, Error)]
enum InitModuleError {
#[error("init module settings error")]
SettingsError(#[from] LoadSettingsError),
#[error("init module dll error")]
DllError(#[from] LoadDllError),
}
fn init_module() -> Result<Module, InitModuleError> {
let settings = load_settings()?;
let dll = load_dll()?;
Ok(Module { settings, dll })
}
#[derive(Debug, Error)]
#[error("load settings error")]
struct LoadSettingsError {}
fn load_settings() -> Result<String, LoadSettingsError> {
Err(LoadSettingsError {})
}
#[derive(Debug, Error)]
#[error("load dll error")]
struct LoadDllError {}
fn load_dll() -> Result<Dll, LoadDllError> {
Err(LoadDllError {})
}
В данном примере, трейт Error
реализуется автоматически с помощью макроса #[derive(Error)]
. Используя макрос #[error("text to display")]
генерируем реализацию трейта Display. Макрос #[from]
создаёт реализацию трейта From для конвертации ошибки нижнего уровня в ошибку текущего.
Данные макросы значительно сокращают объём boilerplate кода для обработки ошибок.
anyhow
Данную библиотеку удобно использовать, когда единственное, что интересует нас в ошибке - её текстовое описание. anyhow предоставляет структуру Error
. В неё может быть сконвертирован любой объект, реализующий трейт std::Error
, что значительно упрощает распространение ошибки по иерархии вызовов. Помимо этого, anyhow::Error
позволяет добавлять текстовое описание контекста, в котором произошла ошибка. Эта библиотека сочетается с thiserror. Пример:
use thiserror::Error; // 1.0.29
use anyhow; // 1.0.43;
use anyhow::Context; // 1.0.43;
fn main() {
let module = init_module().unwrap();
}
struct Module {
settings: String,
dll: Dll,
}
struct Dll {}
fn init_module() -> anyhow::Result<Module> {
let dll = load_dll().context("module initialization")?;
let settings = load_settings()?;
Ok(Module { settings, dll })
}
#[derive(Debug, Error)]
#[error("load settings error")]
struct LoadSettingsError {}
fn load_settings() -> Result<String, LoadSettingsError> {
Err(LoadSettingsError {})
}
fn load_dll() -> anyhow::Result<Dll> {
anyhow::bail!("load dll error")
}
Макрос anyhow::bail!()
в примере создаёт anyhow::Error
с заданным описанием и возвращает её из функции. Псевдоним anyhow::Result
определяется так:
type Result<T, E = Error> = Result<T, E>;
Заключение
В начале статьи мы рассмотрели три возможных варианта действий, при получении ошибки: завершить работу программы, обработать ошибку и передать ошибку вверх по иерархии вызовов. Далее, разобравшись с особенностями синтаксиса, мы разобрались на примерах, как выразить наши намерения по отношению к ошибке на языке Rust. Мы увидели, что любой из вариантов поведения должен быть выражен явно. Такой подход повышает надёжность приложения, так как не позволяет разработчику случайно проигнорировать ошибку. С другой стороны, явное описание своих намерений требует дополнительных усилий. Минимизировать эти усилия позволяют библиотеки thiserror и anyhow.
Благодарю за внимание. Поменьше вам ошибок!
Статья написана в преддверии старта курса Rust Developer. Приглашаю всех желающих на бесплатный урок, в рамках которого на примере построения простого веб сервиса рассмотрим популярный веб-фреймворк actix-web в связке с MongoDB + Redis и другие полезные библиотеки для backend разработки.
Комментарии (23)
onyxmaster
06.10.2021 10:52+1Плюс вам за рекомендацию anyhow, на мой взгляд отлично сбалансированная библиотека для небольших проектов.
zueve
06.10.2021 17:51+2А мне не показалась она удобной. Пробрасывать ошибки конечно легко, но вот обрабатывать выше уже не удобно
onyxmaster
06.10.2021 17:57Мне понравилось что всё просто, предположу что для проекта где кода побольше, а архитектура потолще это не самый оптимальный вариант, да.
DarkEld3r
06.10.2021 19:15+2Дык, в описании библиотеки прямо говорится:
Use Anyhow if you don't care what error type your functions return, you just want it to be easy. This is common in application code. Use thiserror if you are a library that wants to design your own dedicated error type(s) so that on failures the caller gets exactly the information that you choose.
То есть,
anyhow
крайне удобная штука, если мы просто где-то верхнем уровне ошибки пишем в лог и всё. Для библиотек или если требуется активно и по-разному обрабатывать ошибки, то лучше что-то другое.
ertaquo
06.10.2021 11:05Знаете, чем мне не нравится передача ошибок (как в Go и Rust) и нравятся исключения?
Тем, что при передаче ошибок приложение становится гораздо сложнее отлаживать. Теряется информация, откуда именно пришла ошибка. При использовании исключений можно вывести stack trace до места, где возникло это самое исключение.
Тем, что невозможно заранее узнать, какие ошибки может возвращать функция. В принципе, с исключениями тоже не всегда можно узнать, какая функция какое исключение может кинуть, но обычно для этого есть если не языковые конструкции (какthrows
в Java), то хотя бы комментарии (как в PHP).
Использованиеpanic
в чем-то похоже на исключение, но не позволяет указать его тип — просто некая глобальная ошибка, которую можно где-то отловить, и все, что про нее известно, это ее текстовое описание.ozkriff
06.10.2021 11:15+2у растовых ошибок часто есть source поле - а зная тип оригинальной ошибки обычно не сложно поставить брейкпоинт на конструктор этого типа ошибок. подход не во всех случаях работает, конечно, но часто пригождается
domix32
06.10.2021 11:36+2Многие либы которые помогают с обработкой ошибок позволяют явно указывать контекст ошибки, включая стактрейс. А есть и вовсе либы которые позволяют делать что-то исключениеподобное
ozkriff
06.10.2021 11:38только стоит помнить, что это уже не такие дешевые ошибки будут
domix32
06.10.2021 13:40Ну, это логично — либо ты читаешь понятные ошибки и знаешь куда смотреть, но платишь за это ресурсами комплюктера, либо отважно тычешь дебаггером по коду и платишь за это временем разработчика.
DarkEld3r
06.10.2021 12:57При использовании исключений можно вывести stack trace до места, где возникло это самое исключение.
Всё-таки раст язык более низкоуровневый и с джавой или C# сравнивать не совсем честно. В С++(23) стектрейсы только хотят завезти.
Плюс, как уже сказали, библиотеки (вроде упомянутой anyhow) в какой-то степени решают эту проблему. Из минусов — сообщения надо добавлять руками, из плюсов — контекст может быть даже более удобным, чем "сырой" стектрейс.
В принципе, с исключениями тоже не всегда можно узнать, какая функция какое исключение может кинуть, но обычно для этого есть если не языковые конструкции (как throws в Java), то хотя бы комментарии (как в PHP).
Разве в джаве это не признали неудачным решением? Мне казалось, что все пользуются RuntimeException. Нет?
А насчёт PHP можно подробнее, а то я, конечно, полез гуглить, но не понял проверяется ли как-то соответствие комментариев действительности. Что будет, если документация врёт? А если перечислены не все исключения? В общем, если всё держится на внимательности разработчика, то такое себе.
Использование panic в чем-то похоже на исключение, но не позволяет указать его тип
Справедливости ради, позволяет. Можно использовать
panic_any
,catch_unwind
,Any::downcast_ref
иresume_unwind
чтобы эмулировать исключения. Другое дело, что это будет громоздко, неудобно, да и в целом не рекомендуется.
Cykooz
06.10.2021 15:20+3Тем, что невозможно заранее узнать, какие ошибки может возвращать функция.
В Rust как раз можно узнать какие ошибки возвращаются, ведь тип ошибки явно указывается в сигнатуре функции (если это не anyhow или трейт-объект). Ну или скажем так - можно узнать какие ошибки точно не будет возвращать функция, т.к. не факт, что она будет использовать все возможные значения enum-а, который используется как тип ошибки. Но это идеал к которому надо стремиться.
onyxmaster
06.10.2021 18:00При использовании того же anyhow и with_context вполне можно получить «человеческий» стектрейс. Отсутствие спецификации исключений на мой взгляд это серьёзный недостатков, и ни checked exceptions, ни комментарии (допустим у вас нет исходников, да и смотреть каждый раз на комментарии, которые ещё и измениться могут, а вы этого не заметите) тут не особо помогают.
charypopper
07.10.2021 09:53В go вы можете обернуть ошибку, и добавить дополнительную инфу к ошибке и получить тот же стектрейс
(Это с go 1.13 подробнее )
apro
07.10.2021 16:42+2С другой стороны в C++ исключения создают ощущение что каждая функция это обработчик прерываний (конечно если нужно написать надежный код, так конечно можно забить). Для каждой строчки нужно умать, а что если здесь вылетит исключение. При этом ведь даже неизвестно будет или нет исключение, если не используется
noexcept
, документация может врать и нужно читать исходники. И всякие конструкторы,operator=
тоже могут и бывают написаны без раздумий над тем, а что будет если внутри них произойдет исключение, что превращает даже банальныйswap
в непредсказумую фигню в которой могут "утечь" ресурсы.Так что после этого предскауемые ошибки, которые понятно где возникают и как обрабатываются и всегда можно сказать может эта функция вернуть ошибку и компилятор заставит ее обработать, это прямо глоток свежего воздуха.
С языком в котором есть GC все конечно проще. Но если имеешь дело с файлами, сокетами и т.д., то часто ждать пока GC их закроет не лучший вариант. И возвращаемся в хождению по минному полю.
charypopper
12.10.2021 22:49в go (яп с gc) есть функция отложенного вызова defer - она выполнится перед выходом из функции. Стандартный способ работы (например с бд, но также можно и со всем остальным - файлы, сокет, тп), выглядит так:
err := db.Open(ctx) if err != nil { return err } defer db.Close() // some code with db
При этом сначала выполнится код после defer , потом (перед выходом из функции) LIFO выполняются defer, и не надо контролировать закрытие больше
DarkEld3r
14.10.2021 19:01+1Минус такого подхода в том, что надо каждый раз не забыть написать соответствующий defer. А без GC (с RAII) можно просто предоставить объект "подключение к базе" (или сокет, файл и т.д.), который в деструкторе будет делать то, что нужно. В итоге пользовательский код становится проще и забыть "закрыть ресурс" нельзя.
zueve
06.10.2021 17:50Отличная статья, спасибо. После rust-book осталось ощущение что тема ошибок не до конца раскрыта. thiserror - хорош. Странно, что его нет в стандартной библиотеке, писать boilrerplate для преобразования ошибок очень утомительно, и не сильно читабельно.
DarkEld3r
06.10.2021 19:17+2К такой обработке ошибок не с первой итерации пришли. Если бы торопились в стандартную библиотеку добавлять, то сейчас там был бы кривой
error_chain
.zueve
06.10.2021 20:23Интересто, а ведь
std::error::Error
для этих же целей задумывался, но как-то не прижился...ozkriff
06.10.2021 20:40+3Почему не прижился? Это базовый типаж, им все пользуются - в том числе и вышеупомянутый thiserror.
А про текущий статус поисков идеально ржавого способа работать с ошибками можно почитать https://blog.rust-lang.org/inside-rust/2021/07/01/What-the-error-handling-project-group-is-working-towards.html
sargon5000
Кирилл, статья интересная спасибо. Но с запятыми у вас всё плохо, читать тяжело.
F3kilo Автор
Спасибо за положительный отзыв о содержании статьи. Что касается формы... Приложу дополнительные усилия для проверки.