Некоторые личности считают, что одной из величайших ошибок при дизайне некоторых языков программирования, было – придумать пустой указатель (0, NULL, nullptr). С одной стороны, это удобно, с другой стороны это очень неудобно по причине необходимости постоянно проверять, а не является ли указатель пустым. И так как в этих языках спохватились поздно, то проверка полностью ложится на программиста или можно использовать некоторый синтаксический сахар, но главное в том, что на уровне самого языка эти проверки не производятся или заставляют программиста выполнять лишние действия. Однако есть исключения, например, Rust. Об этом мы сегодня и поговорим.
Благодаря возможности хранить значения для вариантов в перечислениях, а перечисления как мы знаем это тип данных, который хранит значение одного из своих вариантов, мы можем создать перечисление с двумя вариантами – правильно и неправильно. К каждому из этих вариантов мы можем прибить значение определенного типа. Для правильного варианта пусть это будет тип нужного нам значения (к примеру i32, [T,n], Vec<T>, String, и т. д.), который мы можем задавать, например, через генерик переменную. Для неправильного варианта, в принципе тоже самое, но по умолчанию пусть будет строка с описанием, что пошло не так. Такое перечисление можно использовать самостоятельно как тип возвращаемого значения, который либо имеет правильный вариант с нужным нам значением, либо имеет неправильный с описанием ошибки или пустой. Такое перечисление мы можем инициализировать при возврате из функции и проверять на правильность при получении. Инструмент есть, но все нужно обрабатывать самому. В чем тогда разница с описанными выше языками?
#[derive(Debug)]
pub enum Res<T, E> {
Thing(T),
Error(E),
}
impl<T, E> Res<T, E> {
pub fn unwr(self) -> T {
// multi cases
// match self {
// Res::Thing(i) => i,
// _ => panic!()
// }
// one case
if let Res::Thing(i) = self {
i
} else {
panic!()
}
}
}
pub fn divide(a: i32, b: i32) -> Res<i32, String> {
if b == 0 {
return Res::Error("Divide by zero".to_string());
}
Res::Thing(a / b)
}
fn main() {
let a = divide(4, 0);
let b = divide(4, 2);
println!("a = {:?}", a);
println!("b = {:?}", b.unwr());
}
А в том, что в самом Rust и в его стандартной библиотеке этот прием используется повсеместно. Например, обработка неправильного варианта такого перечисления не приводит к неопределенному поведению, а вызывает аварийное завершение, что позволяет избежать ошибок на ранней стадии. Более того для удобства есть два таких перечисления. Первое это Option, которое имеет вариант Some(T) и вариант None. Второе это Result, у которого есть вариант Ok(T) и вариант Err(T). Option можно использовать например как возврат из функции поиска, которая либо что-то нашла, либо нет и оба этих варианта правильные, т.е. отсутствие искомого не является ошибкой работы функции. Например, метод pop(), который удаляет элемент из вектора, возвращает такое перечисление, которое имеет значение элемента, завернутое в правильный вариант, либо ничего при пустом векторе. В свою очередь Result можно использовать тогда, когда мы ждем успешное завершение, но можем получить ошибку, которую нужно перехватить. Например, функция связи с сервером должна соединиться, но при отсутствии сети, должна сообщить об ошибке.
Самый простой способ обработать Result это использовать оператор “?”. При правильном варианте происходит распаковка значения (которое мы можем присвоить переменной или вызвать метод его типа через "." ), а при ошибке, происходит аварийное завершение приложения. Рассмотрим различные варианты работы с Result в примере:
fn foo_vec(mut v: Vec<i32>) -> Result<Vec<i32>, &'static str> {
if !v.is_empty() {
for i in 0..v.len() {
v[i] += 1;
}
println!("\nvec in foo: {:?}", v);
Ok(v)
} else {
println!("\nvec in foo []");
Err("error! vec is empty")
}
}
fn main() -> Result<(), &'static str> {
let v3 = vec![1, 2, 3];
let v4 = foo_vec(v3);
println!("vec is not empty, result: {:?}", v4);
//vec in foo: [2, 3, 4]
//vec is not empty, result: Ok([2, 3, 4])
let v6 = vec![];
let v7 = foo_vec(v6);
println!("vec is empty, result: {:?}", v7);
//vec in foo []
//vec is empty, result: Err("error! vec is empty")
if v7.is_err() {
println!("\nvec is empty, result: {:?}", v7.unwrap_err());
} else {
println!("\nvec is not empty, result: {:?}", v7.unwrap());
}
//vec is empty, result: "error! vec is empty"
let v8 = vec![5, 5, 5];
let v9 = foo_vec(v8.clone())?; //?
println!("? >> vec[0]: {:?}", v9[0]);
//vec in foo: [6, 6, 6]
//? >> vec[0]: 6
let v10 = match foo_vec(v8) { //match
Ok(v) => v,
Err(e) => return Err(e),
};
println!("match >> vec: {:?}", v10);
//vec in foo: [6, 6, 6]
//match >> vec: [6, 6, 6]
Ok(()) //() - empty tuple, like void in C++
}
Перегуд В.
Комментариев нет:
Отправить комментарий