Skip to content

Instantly share code, notes, and snippets.

@evgenyigumnov
Last active December 10, 2022 04:15
Show Gist options
  • Save evgenyigumnov/f37b2e6a90eaf0b41fec376d2272af75 to your computer and use it in GitHub Desktop.
Save evgenyigumnov/f37b2e6a90eaf0b41fec376d2272af75 to your computer and use it in GitHub Desktop.
Концепция:
Система владения устанавливает время жизни каждого значения, что делает ненужным сборку мусора
в ядре языка и обеспечивает надежные, но вместе с тем
гибкие интерфейсы для управления такими ресурсами, как сокеты и описатели
файлов.
Передача (move) позволяет передать значение от одного владельца другому, а заимствование (borrowing)
– использовать значение временно, не изменяя владельца
Cargo:
cargo new --bin projectName
cargo clean
cargo run
cargo build
Функции:
fn gcd(mut n: u64, mut m: u64) -> u64 {
assert!(n != 0 && m != 0);
while m != 0 {
if m < n {
let t = m;
m = n;
n = t;
}
m = m % n;
}
n
}
По умолчанию, после того как переменная инициализирована, ее значение
нельзя изменять, но, поместив ключевое слово mut (сокращение от
«mutable», произносится «мьют») перед параметрами n и m, мы разрешаем изменять их в теле
функции.
Предложение let служит для объявления локальной переменной, в нашей
функции это переменная t.
В Rust имеется предложение return, но в функции gcd в нем нет необходимости.
Если тело функции заканчивается выражением, за которым не следует точка с
запятой, то значение выражения и будет значением функции.
На самом деле любой блок, заключенный в фигурные скобки, может выступать в роли выражения.
Тесты:
#[test]
fn test_gcd() {
assert_eq!(gcd(14, 15), 1);
assert_eq!(gcd(2 * 3 * 5 * 11 * 17,
3 * 7 * 11 * 13 * 19),
3 * 11);
}
cargo test
use std::io::Write;
use std::str::FromStr;
Объявление use вводит в область видимости характеристику Write и FromStr
Растущий вектор:
let mut numbers = Vec::new();
numbers.push(1);
let r = u64::from_str("123").expect("error parsing string")
from_str возвращает Result а в нем два варианта
Ok(e)
Err(e)
expect возвращает v в случае Ок или заврешает приложение с тектом ошибки
unwrap делает тоже самое только без указания текста ошибки
let mut d = numbers[0];
for m in &numbers[1..] {
d = gcd(d, *m);
}
Поэтому в процессе обхода мы говорим Rust, что владение элементами вектора
должно оставаться у numbers, а мы только заимствуем элементы для нужд цикла.
Оператор & в выражении &numbers[1..] заимствует ссылку на элементы вектора,
начиная со второго. В цикле for перебираются элементы, на которые указывает
ссылка, так что m последовательно заимствует каждый элемент. Оператор *
в выражении *m разыменовывает m, т. е. дает значение, на которое m ссылается, –
в данном случае это следующее число типа u64, которое мы хотим передать gcd.
Наконец, поскольку переменная numbers владеет вектором, Rust автоматически
освобождает его, когда numbers выходит из области видимости в конце main.
Правила, касающиеся владения и ссылок, – ключ к управлению памятью
и безопасной конкурентности в Rust
let x = 80; // связывание владельца x со значением 80
let mut y = 50; // изменяемое связывание
let z = &x; // неизменяемая ссылка на неизменяемое связывание
let w = &mut y; // неизменяемая ссылка на изменяемое связывание
let r = &mut y; // ошибка: нельзя создавать вторую ссылку на изменяемое связывание
*w = 90 // y = 90
*z = 30 // ошибка: попытка изменения через ссылку на неизменяемое связывание
Файл Cargo.toml
[dependencies]
iron = "0.5.1"
mime = "0.2.3"
router = "0.5.1"
urlencoded = "0.5.0"
В каждой строке раздела [dependencies] указываются имя одного крейта из
имеющихся на сайте crates.io и нужная нам версия.
extern crate iron;
#[macro_use] extern crate mime;
use iron::prelude::*;
use iron::status;
В начале идут две директивы extern crate, которые предоставляют в
распоряжение нашей программы крейты iron и mime, прописанные в файле Cargo.toml.
Атрибут #[macro_use] перед директивой extern crate mime предупреждает Rust, что мы
планируем использовать макросы, экспортируемые этим крейтом
fn get_form(_request: &mut Request) -> IronResult<Response> {
Данная конкретная функция не использует параметра _request, но
ниже мы встретим функцию, где он используется. А пока скажем, что если имя
параметра начинается знаком _, то Rust понимает, что этот параметр может не
использоваться, и не выдает предупреждение.
let s = r#"
<html>
</html>
"#;
println!("{}", s);
let x = 10;
match x {
1 | 2 => println!("один или два"),
3 => println!("три"),
4..=10 => println!("от четырёх до десяти"), // Отработает эта ветка, ведь 10 принадлежит данному диапазону.
_ => println!("что угодно, не соответствующее условиям выше"), // "_" соответствует любому значению
}
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 0, y: 0 };
match point {
Point { x: 0, y } => println!("x - ноль, y равен {}", y), // так как "x" равен нулю, отработает эта ветка.
Point { x, y: 0 } => println!("x равен {}, y - ноль", x),
Point { x, y } => println!("x = {}, y = {}", x, y),
}
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
let color = Color::Hsv(0, 0, 100);
match color {
Color::Rgb(0, 0, 0) | Color::Hsv(0, 0, 0) => println!("чёрный"),
Color::Rgb(255, 255, 255) | Color::Hsv(0, 0, 100) => println!("белый"), // отработает эта ветка.
Color::Rgb(red, green, blue) => {
println!("красный: {}, зелёный: {}, синий: {}", red, green, blue)
} // отработает при любых значениях Rgb, которые не соответствуют условиям выше.
Color::Hsv(hue, saturation, brightness) => println!(
"тон: {}, насыщенность: {}, яркость: {}",
hue, saturation, brightness
), // то же самое, только с Hsv.
}
let (a, b) = (1, 2);
println!("{}", a); // 1
println!("{}", b); // 2
let x = Some(10);
if let Some(value) = x {
// здесь мы деструктурируем x, переменная value хранит значение 10.
// выполнится эта ветка, так как "x" хранит внутри значение.
println!("значение = {}", value);
} else {
// оператор "else" здесь выступает заменой "_" в выражениях match.
println!("x - пуст");
}
let s = "zz1yyy"
&s.find("1") // Some(2)
&s[2..] // 1yyy
&s[..2] // zz
Оператор ? специально введен для удобства таких проверок. Вместо того чтобы
явно писать
let output = match File::create(filename) {
Ok(f) => { f }
Err(e) => { return Err(e); }
};
можно ограничиться гораздо более лаконичным эквивалентным кодом:
let output = File::create(filename)?;
// Тип std::io::Error type.
struct Error { ... };
// Тип std::io::Result type, эквивалентен обычному типу `Result`, но специализирован таким
// образом, что типом ошибки является std::io::Error.
type Result<T> = std::result::Result<T, Error>
Если ввести это определение в область видимости с помощью объявления
use std::io::Result, то можно будет записать тип, возвращаемый функцией write_image,
более кратко – Result<()>. С такой формой вы часто будете встречаться, читая документацию по функциям
из модулей std::io, std::fs и других.
let args: Vec<String> = std::env::args().collect();
fn main() {
let mut zeroVec = vec![0; 10]; // вектор с нулями размером 10
x(&mut zeroVec);
println!("{:?}",zeroVec);
}
fn x( z: &mut[i32]) {
z[0]=1;
}
crossbeam::scope(|spawner| { ... });
Аргумент |spawner| { ... } – выражение замыкания в Rust. Замыкание – это нечто,
что можно вызывать как функцию. В данном случае |spawner| – список аргументов,
а { ... } – тело функции. Заметим, что, в отличие от функций, объявленных с помощью
ключевого слова fn, нет нужды объявлять типы аргументов замыкания,
Rust сам выведет и их, и тип возвращаемого значения.
for (i, e) in vector.into_iter().enumerate() {
println!("{} {}", i , e);
}
crossbeam = "0.2.8"
extern crate crossbeam;
crossbeam::scope(|spawner| {
let z = 1;
spawner.spawn(move || {
println!("{}",z);
});
});
Аргумент |spawner| { ... } – выражение замыкания в Rust. Замыкание – это нечто,
что можно вызывать как функцию. В данном случае |spawner| – список аргументов,
а { ... } – тело функции. Заметим, что, в отличие от функций, объявленных с помощью
ключевого слова fn, нет нужды объявлять типы аргументов замыкания,
Rust сам выведет и их, и тип возвращаемого значения.
Здесь crossbeam::scope вызывает замыкание, передавая в качестве аргумента
spawner значение, которое замыкание сможет использовать для создания новых
потоков. Функция crossbeam::scope ждет завершения всех потоков, а затем возвращает управление
spawner.spawn создаем поток, исполняющий замыкание move || { ... }. Синтаксис
выглядит немного странно: так обозначается замыкание без аргументов с телом
{ ... }. Ключевое слово move в начале говорит, что это замыкание принимает владение используемыми в нем переменными; в ч
В крейте num_cpus имеется функция, которая возвращает число доступных процессоров
в системе.
str field in struct:
struct Human {
name: &'static str,
age: u16,
}
fn string_to_static_str(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
Как это ни удивительно, еще одно распространенное применение кортежа –
нуль-кортеж (). Его принято называть «единичным типом», потому что
существует всего одно значение этого типа, которое тоже записывается ().
В Rust единичный тип используется тогда, когда никакого осмысленного значения нет, но
контекст требует наличия какого-то типа.
Например, функция, не возвращающая значения, имеет тип ().
Ссылки
Выражение &x порождает ссылку на x; в Rust говорят, что оно «заимствует
ссылку на x». Если имеется ссылка r, то выражение *r дает значение, на которое
указывает r. Все это очень похоже на операторы & и * в C и C++.
- &T – неизменяемая ссылка, как const T* в C;
- &mut T – изменяемая ссылка, как T* в C.
Боксы
Самый простой способ выделить память для значения из кучи – воспользоваться
функцией Box::new.
let t = (12, "eggs");
let b = Box::new(t); // выделить память для кортежа из кучи
Переменная t имеет тип (i32, &str), поэтому тип b – Box<(i32, &str)>. Функция
Box::new() выделяет память, достаточную для хранения кортежа в куче. Когда b
покидает область видимости, память сразу же освобождается, если только b не
передана – например, посредством возврата из функции. Передача (move) – это
основа механизма управления памятью, выделенной из кучи, в Rust;
let mut v = vec![2, 3, 5, 7];
let mut v = vec![0 ; 10];
let nums = vec!["One","Two"];
let new_nums: Vec<&str> =nums.into_iter().skip(1).collect();
println!("{:?}",new_nums);
Срезки
Срезка, записываемая в виде [T] без указания длины, – это участок массива или
вектора. Поскольку длина срезки может быть любой, они не хранятся в переменных
и не передаются в виде аргументов функции. Срезки всегда передаются по ссылке.
Пример:
let nums = [1,3,4]; // срезка
let numbers = vec![1,2,3];
let c = &numbers; //указатель на срезку
let p = &numbers[0..1]; //указатль на срезку
println!("In the room the women come and go,
Singing of Mount Abora");
println!("In the room the women come and go, \nSinging of Mount Abora");
println!("In the room the women come and go, \
Singing of Mount Abora.");
Иногда необходимость удваивать все знаки \ в строке вызывает неудобства
(классический пример – регулярные выражения и пути в Windows). Для таких случаев
в Rust предусмотрены простые строки (raw string). Простая строка начинается
буквой r. Все знаки обратной косой черты и пробельные символы включаются
в простую строку без какой-либо обработки. Никакие управляющие последовательности не распознаются.
let default_win_install_path = r"C:\Program Files\Gorillas";
let pattern = Regex::new(r"\d+(\.\d+)*");
println!(r###"
Эта простая строка начинается последовательностью 'r###"'.
Поэтому она заканчивается, только когда встретится кавычка ('"'),
за которой следуют три знака решетки ('###'):
"###);
let noodles = "noodles".to_string();
let oodles = &noodles[1..];
let poodles = "ಠ_ಠ";
С типом String связан буфер переменного размера, в котором хранится текст
в кодировке UTF-8. Буфер выделяется из кучи, поэтому его размер может увеличиваться или
уменьшаться по мере необходимости. В примере выше переменная
noodles имеет тип String и владеет буфером длиной 8 байтов, из которых используются
семь. Можно считать, что тип String – это вектор Vec<u8>, который гарантированно содержит
корректно сформированную строку UTF-8; именно так в действительности и реализован тип String.
Тип &str (произносится «стир» или «срезка строки») – это ссылка на отрезок
текста в кодировке UTF-8, которым владеет кто-то еще: этот текст «заимствуется».
В примере выше переменная oodles – это ссылка &str на последние 6 байтов буфера, принадлежащего
переменной noodles, т. е. она представляет текст «oodles». Как
и любая другая ссылка на срезку, &str является толстым указателем, содержащим
адрес данных и их длину. Можно считать, что &str – это просто тип &[u8], который
гарантированно содержит корректно сформированный текст в кодировке UTF-8.
let a = Box:new(1); // создает значение в куче и вернем ссылку
fn main() {
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s;
let u = s;
}
fn main() {
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s.clone();
let u = s.clone();
}
// Построить вектор строк "101", "102", ... "105"
let mut v = Vec::new();
for i in 101 .. 106 {
v.push(i.to_string());
}
// 1. Взять последнее значение:
let fifth = v.pop().unwrap();
assert_eq!(fifth, "105");
// 2. Взять значение из середины вектора и переместить на его место
// последний элемент:
let second = v.swap_remove(1);
assert_eq!(second, "102");
Передача владения  91
// 3. Подставить другое значение вместо изъятого:
let third = std::mem::replace(&mut v[2], "substitute".to_string());
assert_eq!(third, "103");
// Посмотрим, что осталось от вектора.
assert_eq!(v, vec!["101", "104", "substitute"]);
fn main() {
let man = Human {
name: Some("John".to_string())
};
let mut users = vec![man];
let name1 = std::mem::replace( &mut users[0].name, None);
let name2 = users[0].name.take(); // эквивалентно срочки выше
}
#[derive(Copy, Clone)]
struct Label { number: u32 }
#[derive(Debug)]
struct ...
// to_string
Типы Rc и Arc очень похожи, единственное различие между ними в том, что Arc
можно безопасно разделять между потоками без каких-либо церемоний (Arc означает
«Atomic Reference Count» – «атомарный счетчик ссылок»), тогда как в типе Rc
для обновления счетчика ссылок применяется более быстрый, но не потокобезопасный код.
Если разделять указатели между потоками не нужно, то и не платите
за менее производительный тип Arc, а пользуйтесь Rc; Rust не даст случайно передать
значение такого типа через границы потоков. Во всех остальных отношениях
оба типа эквивалентны, поэтому мы будем рассматривать только Rc.
use std::rc::Rc;
// Rust может вывести вс эти типы, но для ясности они указаны явно
let s: Rc<String> = Rc::new("shirataki".to_string());
let t: Rc<String> = s.clone();
let u: Rc<String> = s.clone();
Бывают ссылки двух видов:
1. разделяемая ссылка позволяет читать значение, но не позволяет изменять
его. Зато в каждый момент времени можно иметь сколь угодно много разделяемых
ссылок на одно значение. Выражение &e дает разделяемую ссылку
на значение e; если e имеет тип T, то &e имеет тип &T (читается «ссылка на T»).
Разделяемые ссылки – копируемые типы;
2. изменяемая ссылка позволяет читать и модифицировать значение. Но вместе
с ней не должно существовать никаких других ссылок на то же значение.
Выражение &mut e дает изменяемую ссылку на значение e, ее тип записывается в виде &mut T.
Изменяемые ссылки – некопируемые типы.
Различие между разделяемыми и изменяемыми ссылками можно трактовать
как проверку соблюдения правила «много читателей или один писатель» на этапе компиляции.
let x = 10;
let r = &x; // &x – разделяемая ссылка на x
assert!(*r == 10); // явное разыменование r
let mut y = 32;
let m = &mut y; // &mut y – изменяемая ссылка на y
*m += 32; // явно разыменовываем m, чтобы изменить значение y
assert!(*m == 64); // и проверяем новое значение y
Поскольку ссылки широко распространены в Rust, оператор . неявно
разыменовывает свой левый операнд, если это необходимо.
struct Anime { name: &'static str, bechdel_pass: bool };
let aria = Anime { name: "Aria: The Animation", bechdel_pass: true };
let anime_ref = &aria;
assert_eq!(anime_ref.name, "Aria: The Animation");
// Эквивалентно, но разыменование явное:
assert_eq!((*anime_ref).name, "Aria: The Animation");
Оператор . может также неявно заимствовать ссылку на свой левый операнд,
если это необходимо для вызова метода. Например, метод sort типа Vec принимает
изменяемую ссылку на метод, поэтому следующие два вызова эквивалентны:
let mut v = vec![1973, 1968];
v.sort(); // неявно заимствует изменяемую ссылку на v
(&mut v).sort(); // эквивалентно, но уж очень некрасиво
Ссылки на ссылки
В Rust разрешены ссылки на ссылки:
struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;
(Для ясности мы явно выписали ссылочные типы, но их можно и опустить, в этом
коде нет ничего, что Rust не смог бы вывести самостоятельно.) Оператор . проследует
по ссылке столько раз, сколько необходимо для доступа к значению:
assert_eq!(rrr.y, 729)
let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = &rx;
let rry = &ry;
assert!(rrx <= rry);
assert!(rrx == rry);
assert!(rx == ry); // объекты ссылки равны
assert!(!std::ptr::eq(rx, ry)); // но расположены по разным адресам
Эквивалент глобальной переменной в Rust – статическая переменная: это
значение, которое создается в момент запуска программы и существует до момента ее завершения
fn f<'a>(p: &'a i32) { ... }
Здесь время жизни 'a (произносится «апостроф A») – это параметрическое время
жизни f. Конструкцию <'a> можно прочитать как «для любого времени жизни
'a», так что, записывая fn f<'a>(p: &'a i32), мы определяем функцию, которая
принимает ссылку на значение типа i32 с любым заданным временем жизни 'a.
Поскольку мы обязаны обеспечить работу при любом времени жизни 'a, то код
должен работать и тогда, когда это наименьшее возможное время жизни: в точности охватывающее вызов f.
'static время жизни на все время работы программы
static WORTH_POINTING_AT: i32 = 1000;
глобальная переменная на все время жизни программы
Опускание параметрического времени жизни
struct S<'a, 'b> {
x: &'a i32,
y: &'b i32
}
fn sum_r_xy(r: &i32, s: S) -> i32 {
r + s.x + s.y
}
Сигнатура этой функции – сокращенная запись следующей:
fn sum_r_xy<'a, 'b, 'c>(r: &'a i32, s: S<'b, 'c>) -> i32
Правила разделяемости и изменяемости (& и &mut):
- разделяемый доступ предназначен только для чтения. Значения,
заимствуемые разделяемыми ссылками, можно только читать. На всем протяжении
времени жизни разделяемой ссылки ни значение самого объекта ссылки,
ни значение чего-либо, достижимого из этого объекта, нельзя изменять
никаким способом. Не существует ни одной активной изменяемой ссылки
на что-либо внутри структуры, владелец структуры может только читать ее
и т. д. Структура действительно заморожена;
- напротив, изменяемый доступ носит монопольный характер. Значение,
заимствованное изменяемой ссылкой, доступно только по этой ссылке. На
протяжении всего времени ее жизни не существует никакого возможного
пути к объекту ссылки или к значениям, достижимым из этого объекта.
Единственные ссылки, время жизни которых может пересекаться со
временем жизни изменяемой ссылки, – те, которые заимствованы у нее самой.
if let Some(cookie) = request.session_cookie {
return restore_session(cookie);
}
if let Err(err) = present_cheesy_anti_robot_task() {
log_robot_attempt(err);
politely_accuse_user_of_being_a_robot();
} else {
session.mark_as_human();
}
Циклы
Существуют четыре выражения цикла:
while condition {
block
}
while let pattern = expr {
block
}
loop {
block
}
for pattern in collection {
block
}
В Rust циклы являются выражениями, но не порождают полезных значений.
Значением цикла всегда является ().
Выражениям, которые не завершаются нормально, назначается
специальный тип !, и к ним не применяются правила об обязательном
совпадении типов. Тип ! встречается в сигнатуре функции std::process::exit():
fn exit(code: i32) -> !
Здесь ! означает, что exit() никогда не возвращает управление. Это уходящая
функция (divergent function).
return Vec<i32>::with_capacity(1000); // ошибка: что-то насчет сцепленных сравений
let ramp = (0 .. n).collect<Vec<i32>>(); // та же ошибка
Проблема в том, что в выражениях < – это оператор «меньше». Компилятор Rust
услужливо предлагает в таких случаях писать ::<T> вместо <T>, и это решает проблему:
return Vec::<i32>::with_capacity(1000); // правильно, используется ::<
let ramp = (0 .. n).collect::<Vec<i32>>(); // правильно, используется ::<
Символ ::<...> в сообществе Rust любовно называют «турборыба».
Вместо этого зачастую можно просто опустить параметрические типы и позволить Rust вывести их.
return Vec::with_capacity(10); // правильно, если fn возвращает значение типа Vec<i32>
let ramp: Vec<i32> = (0 .. n).collect(); // правильно, указан тип переменной
Считается хорошим стилем опускать типы, если их можно вывести.
Оператор .. позволяет опускать любой операнд. Он порождает объекты четырех
разных типов в зависимости от того, какие операнды присутствуют:
.. // RangeFull
a .. // RangeFrom { start: a }
.. b // RangeTo { end: b }
a .. b // Range { start: a, end: b }
Но есть и несколько других важных автоматических преобразований:
- значения типа &String автоматически преобразуются в тип &str без явного
приведения;
- значения типа &Vec<i32> автоматически преобразуются в тип &[i32];
- значения типа &Box<Chessboard> автоматически преобразуются в тип &Chessboard.
Все эти преобразования называются Deref-преобразованиями, поскольку применяются к
типам, реализующим встроенную характеристику Deref. Цель Deref преобразований
– сделать так, чтобы типы интеллектуальных указателей, например Box, вели
себя настолько похоже на указываемые значения, насколько это
возможно. Благодаря Deref использование типа Box<Chessboard> мало чем
отличается от использования простого типа Chessboard.
Обычные ошибки обрабатываются с помощью типа Result. Как правило, они
вызваны причинами, которые программа не контролирует: ошибочные входные
данные, пропадание сети или проблемы с правами. То, что такие ситуации возникают, –
не наша вина, даже безошибочная программа время от времени сталкивается с ними.
Паника – это ошибка другого рода – такая, которая никогда не должна происходить.
Паника
Программа паникует, встретив нечто настолько ужасное, что можно заподозрить
дефект в самой программе, например:
- доступ к элементу за границами массива;
- деление целого числа на нуль;
- вызов метода .unwrap() для значения типа Option, оказавшегося равным None;
- ошибочное утверждение.
Существует также макрос panic!() на случай, если программа обнаружит, что
делает что-то неправильное, и потому должна сама поднять панику.
На такой случай Rust предоставляет нам выбор: раскрутить стек в случае
возникновения паники или снять процесс. По умолчанию подразумевается раскрутка стека.
Раскрутка стека – поведение паники по умолчанию, но есть два случая, когда Rust
не пытается раскрутить стек.
Если метод .drop() повторно паникует, когда Rust пытается выполнить очистку
после первой паники, то ошибка считается фатальной. Rust прекращает раскрутку
и снимает процесс целиком.
Кроме того, поведение паники можно настраивать. Если откомпилировать программу
с флагом -C panic=abort, то к немедленному завершению процесса приводит
уже первая паника. (В этом режиме Rust может не знать, как раскручивать
стек, поэтому размер откомпилированного кода уменьшается.
Result:
- result.is_ok() и result.is_err() возвращают значение типа bool, показывающее,
содержит result успешный или ошибочный результат.
- result.ok() возвращает успешный результат, если таковой присутствует,
в виде значения типа Option<T>. Если result содержит успешный результат,
то это будет Some(success_value), в противном случае None (а код ошибки отбрасывается).
- result.err() возвращает ошибочный результат, если таковой присутствует,
в виде значения типа Option<E>.
- result.unwrap_or(fallback) возвращает успешный результат, если таковой содержится в
result. В противном случае возвращается fallback, а код ошибки
отбрасывается.
Это удобная альтернатива методу .ok(), поскольку возвращается тип T, а не
Option<T>. Разумеется, это работает, только если подходящее подменное значение существует.
- result.unwrap_or_else(fallback_fn) – то же самое, но вместо возврата подменного
значения непосредственно вызывает переданную функцию или замы-
кание. Предназначен для случаев, когда было бы расточительно вычислять
подменное значение, если мы не собираемся его использовать. fallback_fn
вызывается только тогда, когда получен ошибочный результат.
- result.unwrap() также возвращает успешный результат, если он содержится
в result. Если же result содержит ошибочный результат, то этот метод паникует. У этого
метода есть полезные применения, о которых мы поговорим ниже.
- result.expect(message) – то же, что .unwrap(), но позволяет задать сообщение,
которое печатается в случае паники.
Наконец, последние два метода заимствуют ссылки на значение, хранящееся в Result.
- result.as_ref() преобразует Result<T, E> в Result<&T, &E>, заимствуя ссылку на
успешный или ошибочный результат, содержащийся в имеющемся значении result.
- result.as_mut() – то же самое, но заимствуется изменяемая ссылка.
Тип возвращаемого значения – Result<&mut T, &mut E>.
В стандартной библиотеке определено несколько типов ошибок со скучными
именами: std::io::Error, std::fmt::Error, std::str::Utf8Error и т. д.
Все они реализуют общий интерфейс, характеристику std::error::Error, т. е. обладают перечисленными ниже свойствами.
- Допускают печать макросом println!(). Если ошибка печатается по формату
{}, то выводится лишь краткое описание. Если же используется спецификатор формата {:?},
то выводится отладочное представление ошибки. Оно не
столь дружелюбно к пользователю, зато содержит дополнительную техническую информацию.
// результат работы `println!("error: {}", err);`
error: failed to lookup address information: No address associated with
hostname
// результат работы `println!("error: {:?}", err);`
error: Error { repr: Custom(Custom { kind: Other, error: StringError(
"failed to lookup address information: No address associated with
hostname") }) }
- err.description() возвращает сообщение об ошибке в виде &str.
- err.cause() возвращает значение типа Option<&Error>: истинную ошибку,
приведшую к err, если таковая имеется.
Распространение ошибок
Как правило, пытаясь сделать нечто такое, что может завершиться ошибкой, мы
не хотим обнаруживать и обрабатывать эту ошибку немедленно. Писать предложение
match на десять строк в любом месте, где может случиться неприятность,
попросту нерационально.
Поэтому мы обычно предпочитаем, чтобы ошибку обработала вызывающая
сторона, т. е. хотим, чтобы ошибки распространялись по стеку вызовов.
Для этой цели в Rust имеется оператор ?. Его можно добавить в любое выражение,
порождающее Result, например результат вызова функции:
let weather = get_weather(hometown)?;
Поведение ? зависит от того, возвращает ли функция успешный или ошибочный результат.
- В случае успеха оператор разворачивает Result методом unwrap,
чтобы получить находящееся внутри него значение. В данном случае типом переменной
weather будет не Result<WeatherReport, io::Error>, а просто WeatherReport.
- В случае ошибки оператор сразу же возвращает управление из объемлющей
функции, передавая ошибочный результат вверх по цепочке вызовов.
Но чтобы этот механизм работал, оператор ? следует использовать только
в функциях, возвращающих значение типа Result.
Ничего магического в операторе ? нет. То же самое можно сделать с помощью
выражения match, только гораздо более многословно:
let weather = match get_weather(hometown) {
Ok(success_value) => success_value,
Err(err) => return Err(err)
};
Псевдонимы типа Result
Иногда в документации по Rust встречаются места, где кажется, что тип ошибки
в Result отсутствует:
fn remove_file(path: &Path) -> Result<()>
Это означает, что используется псевдоним типа Result.
Псевдоним типа – это своего рода сокращение имени типа. В модулях часто
определяется псевдоним типа Result, чтобы не повторять раз за разом тип ошибки,
который единообразно используется почти во всех функциях модуля. Например, в модуле std::io из
стандартной библиотеки есть такая строчка:
pub type Result<T> = result::Result<T, Error>;
Работа с ошибками нескольких типов
Все типы ошибок в стандартной библиотеке можно преобразовать в тип Box<std::error::Error>,
представляющий «любую ошибку». Поэтому легкий способ обработать ошибки нескольких
типов состоит в том, чтобы определить такие псевдонимы типов:
type GenError = Box<std::error::Error>;
type GenResult<T> = Result<T, GenError>;
Затем нужно изменить тип возвращаемого значения read_numbers() на GenResult<Vec<i64>>.
После этого функция компилируется. Оператор ? автоматически
преобразует любой тип ошибки в GenError по мере необходимости.
Кстати говоря, оператор ? производит это автоматическое преобразование
с помощью стандартного метода, которым можете воспользоваться и вы. Чтобы
преобразовать любую ошибку в тип GenError, вызовите метод GenError::from().
let io_error = io::Error::new( // создать экземпляр типа io::Error
io::ErrorKind::Other, "timed out");
return Err(GenError::from(io_error)); // вручную преобразовать его в GenError
Если вы вызываете функцию, которая возвращает GenResult, и хотите обработать
только ошибки одного вида, а остальные передать вызывающей стороне, то
воспользуйтесь методом error.downcast_ref::<ErrorType>(). Он заимствует ссылку
на ошибку, только если она принадлежит интересующему вас типу.
loop {
match compile_project() {
Ok(()) => return Ok(()),
Err(err) => {
if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() {
insert_semicolon_in_source_code(mse.file(), mse.line())?;
continue; // попробовать еще раз!
}
return Err(err);
}
}
Игнорирование ошибок
компилятор Rust предупредит о неиспользованном значении Result:
writeln!(stderr(), "error: {}", err); // предупреждение: результат не используется
Для подавления этого предупреждения применяют идиому let _ = ...:
let _ = writeln!(stderr(), "error: {}", err); // ok, результат игнорируется
Объявление пользовательского типа ошибки
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
use std;
use std::fmt;
// Ошибки должны допускать распечатку.
impl fmt::Display for JsonError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
Тип Result  157
write!(f, "{} ({}:{})", self.message, self.line, self.column)
}
}
// Ошибки должны реализовывать характеристику std::error::Error.
impl std::error::Error for JsonError {
fn description(&self) -> &str {
&self.message
}
}
struct S {
...
}
impl S {
fn sort(&mut self) {
...
}
}
В первом аргументе Rust передает методу значение, для которого этот метод
вызван. Этот аргумент должен иметь специальное имя self. Поскольку типом self,
очевидно, является тип, указанный в начале блока impl, или ссылка на него, то
Rust позволяет опускать тип и писать self, &self или &mut self вместо self: Queue,
self: &Queue или self: &mut Queue соответственно. При желании можете использовать
более длинную форму, но почти во всех Rust-программах употребляется сокращенная.
Струкутры с внутренней изменяемостью: std::cell::Cell<T> и std::cell::RefCell<T>.
Cell<T> – структура, содержащая единственное закрытое значение типа T. Необычного
в ней только то, что получить и установить значение этого поля можно
даже тогда, когда сама структура Cell не помечена ключевым словом mut.
- Cell::new(value) создает новое значение Cell, передавая ему владение данным значением value.
- cell.get() возвращает копию значения, хранящегося внутри cell.
- cell.set(value) сохраняет значение value в cell, уничтожая ранее хранившееся значение.
В данном случае следует воспользоваться типом RefCell. Как и Cell<T>,
RefCell<T> – универсальный тип, содержащий единственное поле типа T. Но,
в отличие от Cell, RefCell поддерживает заимствование ссылок на свое значение.
- RefCell::new(value) создает новое значение RefCell, передавая ему владение
данным значением value.
- ref_cell.borrow() возвращает Ref<T> – разделяемую ссылку на значение, хранящееся в ref_cell.
Этот метод паникует, если на значение уже заимствована изменяемая ссылка.
- ref_cell.borrow_mut() возвращает RefMut<T>, изменяемую ссылку на значение,
хранящееся в ref_cell.
Этот метод паникует, если ссылка на значение уже заимствована.
fn min<T: Ord>(value1: T, value2: T) -> T {
if value1 <= value2 {
value1
} else {
value2
}
}
В этой функции <T: Ord> означает, что аргументы min могут иметь любой тип T,
реализующий характеристику Ord, т. е. любой упорядоченный тип. Компилятор
генерирует машинный код, специализированный для конкретного использованного типа T.
Трейт это свойство, которое может поддерживаться или не поддерживаться заданным типом.
Чаще всего трейт описывает некоторую способность: то, что тип может делать.
- Значение, реализующее std::io::Write, может записывать байты.
- Значение, реализующее std::iter::Iterator, может порождать последовательность значений.
- Значение, реализующее std::clone::Clone, может создавать собственные
клоны в памяти.
- Значение, реализующее std::fmt::Debug, можно распечатать макросом
println!() со спецификатором формата {:?}.
Все эти терйты входят в стандартную библиотеку Rust и реализуются
различными стандартными типами.
- Тип std::fs::File реализует трейт Write; он записывает байты
в локальный файл. Тип std::net::TcpStream записывает байты в сетевое соединение.
- Тип Vec<u8> также реализует Write. Каждый вызов .write() для вектора байтов добавляет данные в конец вектора.
- Тип Range<i32> (таков, например, тип диапазона 0..10) реализует трейт Iterator,
как и некоторые итераторные типы, ассоциированные со
срезками, хеш-таблицами и т. д.
- Большинство типов из стандартной библиотеки реализует трейт
Clone. Исключения составляют в основном типы наподобие TcpStream, которые представляют не только данные в памяти.
- Аналогично большинство библиотечных типов реализует характеристику Debug.
let b:&[u8] = b"hello world\n";
К методам, объявленным в трейте, применимо одно необычное правило:
сама характеристика должна находиться в области видимости. Иначе все ее
методы будут скрыты.
let mut buf: Vec<u8> = vec![];
buf.write_all(b"hello")?; // ошибка: метод `write_all` не найден
В данном случае компилятор выводит сообщение об ошибке, любезно предлагая
добавить предложение use std::io::Write;. Это действительно решает проблему.
use std::io::Write;
let mut buf: Vec<u8> = vec![];
buf.write_all(b"hello")?; // ok
fn top_ten<T: Debug + Hash + Eq>(values: &Vec<T>) { ... }
fn nearest<'t, 'c, P>(target: &'t P, candidates: &'c [P]) -> &'c P
where P: MeasureDistance
{
...
}
Псевдонимы типов также могут быть универсальными:
type PancakeResult<T> = Result<T, PancakeError>;
Полностью квалифицированные вызовы методов
Метод – это просто частный случай функции. Следующие два вызова эквивалентны:
"hello".to_string()
str::to_string("hello")
Вторая форма выглядит в точности как вызов статического метода. Это работает,
несмотря на то что метод to_string принимает аргумент self. Просто передайте
self в качестве первого аргумента функции.
Поскольку to_string – метод стандартной характеристики ToString, его можно
вызвать еще двумя способами:
ToString::to_string("hello")
<str as ToString>::to_string("hello")
- Характеристика Drop используется для очистки значений, покидающих область видимости, как деструкторы в C++.
- Интеллектуальные указатели, например Box<T> и Rc<T>, могут реализовать
характеристику Deref, чтобы указатель повторял методы обернутого значения.
- Посредством реализации характеристик From<T> и Into<T> мы можем подсказать
Rust, как преобразовывать значение из одного типа в другой.
Клонирование с преобразованием типа:
let xxx: &[i32] = &[1,2,3,4];
let ccc:Vec<i32> = xxx.to_owned();
Тип std::borrow::Cow (акроним «Clone on write» – копирование при записи)
Вот его определение:
enum Cow<'a, B: ?Sized + 'a>
where B: ToOwned
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
Тип Cow<B> либо заимствует разделяемую ссылку на B, либо владеет значением,
от которого можно заимствовать такую ссылку
use std::path::PathBuf;
use std::borrow::Cow;
fn describe(error: &Error) -> Cow<'static, str> {
match *error {
Error::OutOfMemory => "out of memory".into(),
Error::StackOverflow => "stack overflow".into(),
Error::MachineOnFire => "machine on fire".into(),
Error::Unfathomable => "machine bewildered".into(),
Error::FileNotFound(ref path) => {
format!("file not found: {}", path.display()).into()
}
}
}
Здесь для конструирования значений используется реализация характеристики Into в типе Cow.
В большинстве ветвей предложения match возвращается значение Cow::Borrowed, ссылающееся на
статически выделенную строку. Но в ветви FileNotFound мы используем макрос format!, чтобы построить сообщение, включающее
имя файла. Эта ветвь порождает значение Cow::Owned.
Если сторона, вызывающая describe, не собирается изменять значение, то может рассматривать Cow просто как &str:
println!("Случилась беда: {}", describe(&error));
Если же вызывающая сторона хочет получить значение во владение, то может
легко это сделать:
let mut log: Vec<String> = Vec::new();
...
log.push(describe(&error).into_owned());
КОЛЛЕКЦИИ
- Vec<T> – растущий массив значений типа T, выделяемый в куче. Примерно
половина главы посвящена типу Vec и его многочисленным методам.
- Тип VecDeque<T> похож на Vec<T>, но более пригоден в качестве очереди,
обслуживаемой по принципу «первым пришел, первым ушел». Он поддерживает
эффективное добавление и удаление значений с обеих сторон. Но ценой
за это является небольшое замедление всех остальных операций.
- Тип LinkedList<T> поддерживает быстрый доступ к началу и концу списка,
как VecDeque<T>, а также быструю конкатенацию списков. Но в общем случае
LinkedList<T> медленнее, чем Vec<T> и VecDeque<T>.
- BinaryHeap<T> – очередь с приоритетами. Значения в BinaryHeap организованы
так, чтобы операции поиска и удаления максимального значения производились эффективно.
- HashMap<K, V> – таблица пар ключей и значений. Поиск по ключу производится
быстро. Порядок хранения записей не определен.
- Тип BTreeMap<K, V> похож на HashMap<K, V>, но записи отсортированы по ключу.
В коллекции BTreeMap<String, i32> записи хранятся в порядке сравнения
строк. Если порядок записей несуществен, то лучше использовать HashMap,
поскольку она работает быстрее.
- HashSet<T> – множество значений типа T. Добавление и удаление производятся
быстро, как и ответ на вопрос, принадлежит ли заданное значение
множеству.
- Тип BTreeSet<T> похож на HashSet<T>, но элементы хранятся в отсортированном
виде. И снова HashSet работает быстрее.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment