Hello world!
Представляю вашему вниманию вторую часть практического руководства по Rust.
Другой формат, который может показаться вам более удобным.
Руководство основано на Comprehensive Rust — руководстве по Rust
от команды Android
в Google
и рассчитано на людей, которые уверенно владеют любым современным языком программирования. Еще раз: это руководство не рассчитано на тех, кто только начинает кодить 😉
В этой части мы рассмотрим следующие темы:
- сопоставление с образцом (pattern matching) — извлечение данных из структур
- методы — функции, ассоциированные с типами
- трейты (traits) — поведение, общее для нескольких типов
- дженерики (generics) — общие типы
- типы и трейты, предоставляемые стандартной библиотекой
Rust
Материалы для более глубокого изучения названных тем:
- Книга/учебник по Rust (на русском языке) — главы 6, 8, 10, 13, 17 и 18
- rustlings — упражнения 04-06, 09, 11, 12, 14 и 15
- Rust на примерах (на русском языке) — 5, 6, 14, 16, 19 и 20
- Rust by practice — упражнения 8-12 и 18
Также см. Большую шпаргалку по Rust.
Сопоставление с образцом
Деструктуризация
Как и кортежи (tuples), структуры (structs) и перечисления (enums) также могут деструктурироваться (destructure) сопоставлением:
Структуры
struct Foo {
x: (u32, u32),
y: u32,
}
// Запрещаем форматирование
#[rustfmt::skip]
fn main() {
let foo = Foo { x: (1, 2), y: 3 };
match foo {
Foo { x: (1, b), y } => println!("x.0 = 1, b = {b}, y = {y}"),
Foo { y: 2, x: i } => println!("y = 2, x = {i:?}"),
Foo { y, .. } => println!("y = {y}, другие поля игнорируются"),
}
}
Перечисления
Шаблоны (patterns) могут использоваться для привязки переменных к частям значений. Это, помимо прочего, позволяет исследовать структуру типов. Начнем с определения простого enum
:
enum Result {
Ok(i32),
Err(String),
}
fn divide_in_two(n: i32) -> Result {
if n % 2 == 0 {
Result::Ok(n / 2)
} else {
Result::Err(format!("нельзя разделить {n} на 2 равные части"))
}
}
fn main() {
let n = 100;
match divide_in_two(n) {
Result::Ok(half) => println!("{n}, деленное на 2: {half}"),
Result::Err(msg) => println!("возникла ошибка: {msg}"),
}
}
Здесь для деструктуризации Result
используется 2 блока (руки/рукава — arms). В первом блоке half
привязывается к значению внутри варианта Ok
. Во втором блоке msg
привязывается к сообщению об ошибке (внутри варианта Err
).
Структуры:
- измените литеральные значения в
foo
для совпадения с другими шаблонами - добавьте новое поле в
Foo
и модифицируйте шаблон соответствующим образом
Перечисления:
- выражение
if-else
возвращает перечисление, которое распаковывается с помощьюmatch
- добавьте третий вариант в перечисление и изучите сообщение об ошибке
- доступ к значениям в вариантах перечисления возможен только после сопоставления с шаблоном
- изучите ошибки, связанные с тем, что сопоставление не является исчерпывающим
Поток управления let
Rust
предоставляет несколько конструкций управления потоком выполнения программы, которых нет в других языках программирования и которые используются для сопоставления с шаблоном:
if let
while let
match
if let
Выражение if-let позволяет выполнять код в зависимости от совпадения значения с шаблоном:
fn sleep_for(secs: f32) {
let dur = if let Ok(dur) = std::time::Duration::try_from_secs_f32(secs) {
dur
} else {
std::time::Duration::from_millis(500)
};
std::thread::sleep(dur);
println!("спал в течение {:?}", dur);
}
fn main() {
// Выполнится код блока `else`
sleep_for(-10.0);
// Выполнится код блока `if`
sleep_for(0.8);
}
let-else
Для обычного случая сопоставления с шаблоном и возврата из функции следует использовать let-else. Код блока else
должен прерывать поток выполнения программы (return
, break
, panic!
и т.п.).
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
let s = if let Some(s) = maybe_string {
s
} else {
return Err(String::from("получено `None`"));
};
let first_byte_char = if let Some(first_byte_char) = s.chars().next() {
first_byte_char
} else {
return Err(String::from("получена пустая строка"));
};
if let Some(digit) = first_byte_char.to_digit(16) {
Ok(digit)
} else {
Err(String::from("не шестнадцатеричное число"))
}
}
fn main() {
println!("результат: {:?}", hex_or_die_trying(Some(String::from("foo")))); // 15 - байтовое представление символа `f`
}
Выражение while-let повторно проверяет соответствие значения шаблону:
fn main() {
let mut name = String::from("Comprehensive Rust 🦀");
while let Some(c) = name.pop() {
println!("символ: {c}");
}
// Существуют более эффективные способы 😉
}
Здесь String::pop() возвращает Some(c)
до тех пор, пока строка не окажется пустой, после чего возвращается None
. while-let
позволяет перебирать все элементы.
if-let:
- в отличие от
match
,if-let
не должно охватывать все случаи. Поэтому его использование может быть менее многословным, чем использованиеmatch
- обычным способом использования
if-let
является обработкаSome
при работе сOption
- в отличие от
match
,if-let
не поддерживает защитников сопоставления (match guards)
let-else:
let-else
поддерживает распаковку (flattening) вложенного кода. Перепишем пример следующим образом:
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
let Some(s) = maybe_string else {
return Err(String::from("получено `None`"));
};
let Some(first_byte_char) = s.chars().next() else {
return Err(String::from("получена пустая строка"));
};
let Some(digit) = first_byte_char.to_digit(16) else {
return Err(String::from("не шестнадцатеричное число"));
};
return Ok(digit);
}
while-let:
- цикл
while-let
повторяется, пока значение совпадает с шаблоном - цикл
while-let
в примере можно сделать бесконечным с инструкциейif
внутри, которая прерывает цикл, когдаname.pop()
ничего не возвращает
Упражнение: оценка выражения
Напишем простой рекурсивный вычислитель арифметических выражений.
Тип Box
представляет собой умный указатель (smart pointer), который мы подробно рассмотрим позже. Выражение можно "упаковать" с помощью Box::new()
, как показано в тестах. Для вычисления упакованного выражения, следует использовать оператор разыменования (*
): eval(*boxed_expr)
.
Некоторые выражения не могут быть вычислены и возвращают ошибку. Стандартный тип Result<Value, String>
— это перечисление, которое представляет успешное значение (Ok(Value)
) или ошибку (Err(String)
). Мы подробно рассмотрим этот тип позже.
// Операция, выполняемая над двумя подвыражениями
#[derive(Debug)]
enum Operation {
Add,
Sub,
Mul,
Div,
}
// Выражение в форме дерева
#[derive(Debug)]
enum Expression {
// Операция над двумя подвыражениями
Op {
op: Operation,
left: Box<Expression>,
right: Box<Expression>,
},
// Литеральное значение
Value(i64),
}
// Рекурсивный вычислитель арифметических выражений
fn eval(e: Expression) -> Result<i64, String> {
todo!("реализуй меня")
}
fn main() {
let expr = Expression::Op {
op: Operation::Sub,
left: Box::new(Expression::Value(20)),
right: Box::new(Expression::Value(10)),
};
println!("выражение: {:?}", expr);
println!("результат: {:?}", eval(expr));
}
// Модуль с тестами - код компилируется только при запуске тестов с помощью команды `cargo test`
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value() {
assert_eq!(eval(Expression::Value(19)), Ok(19));
}
#[test]
fn test_sum() {
assert_eq!(
eval(Expression::Op {
op: Operation::Add,
left: Box::new(Expression::Value(10)),
right: Box::new(Expression::Value(20)),
}),
Ok(30)
);
}
#[test]
fn test_recursion() {
let term1 = Expression::Op {
op: Operation::Mul,
left: Box::new(Expression::Value(10)),
right: Box::new(Expression::Value(9)),
};
let term2 = Expression::Op {
op: Operation::Mul,
left: Box::new(Expression::Op {
op: Operation::Sub,
left: Box::new(Expression::Value(3)),
right: Box::new(Expression::Value(4)),
}),
right: Box::new(Expression::Value(5)),
};
assert_eq!(
eval(Expression::Op {
op: Operation::Add,
left: Box::new(term1),
right: Box::new(term2),
}),
Ok(85)
);
}
#[test]
fn test_error() {
assert_eq!(
eval(Expression::Op {
op: Operation::Div,
left: Box::new(Expression::Value(99)),
right: Box::new(Expression::Value(0)),
}),
Err(String::from("деление на ноль"))
);
}
}
fn eval(e: Expression) -> Result<i64, String> {
// Определяем вариант
match e {
// Операция.
// Деструктуризация
Expression::Op { op, left, right } => {
// Рекурсивно вычисляем левое подвыражение
let left = match eval(*left) {
Ok(v) => v,
Err(e) => return e,
};
// Рекурсивно вычисляем правое подвыражение
let right = match eval(*right) {
Ok(v) => v,
Err(e) => return e,
};
// Возвращаем результат, упакованный в `Ok`
Ok(
// Определяем тип операции
match op {
Operation::Add => left + right,
Operation::Sub => left - right,
Operation::Mul => left * right,
Operation::Div => {
// Если правый операнд равняется 0
if right == 0 {
// Возвращаем вызывающему (caller) сообщение об ошибке, обернутое в `Err`.
// Мы распространяем (propagate) ошибку, поэтому она не оборачивается в `Ok`
return Err(String::from("деление на ноль"));
} else {
left / right
}
}
}
)
}
// Значение.
// Просто возвращаем значение, упакованное в `Ok`
Expression::Value(v) => Ok(v),
}
}
Методы и трейты
Методы
Rust
позволяет привязывать функции к типам (такие функции называются ассоциированными — методы экземпляров в других языках). Это делается с помощью блока impl
:
#[derive(Debug)]
struct Race {
name: String,
laps: Vec<i32>,
}
impl Race {
// Нет получателя, статичный метод
fn new(name: &str) -> Self {
Self { name: String::from(name), laps: Vec::new() }
}
// Эксклюзивное заимствование (exclusive borrowing), допускающее чтение и запись в `self`
fn add_lap(&mut self, lap: i32) {
self.laps.push(lap);
}
// Общее, доступное только для чтение заимствование (shared borrowing) `self`
fn print_laps(&self) {
println!("Записано время {} кругов для {}:", self.laps.len(), self.name);
for (idx, lap) in self.laps.iter().enumerate() {
println!("Круг {idx}: {lap} секунд");
}
}
// Эксклюзивное владение (exclusive ownership) `self`
fn finish(self) {
let total: i32 = self.laps.iter().sum();
println!("Гонка {} закончена, общее время: {}", self.name, total);
}
}
fn main() {
let mut race = Race::new("Monaco Grand Prix");
race.add_lap(70);
race.add_lap(68);
race.print_laps();
race.add_lap(71);
race.print_laps();
race.finish();
// race.add_lap(42);
}
Аргументы self
определяют "получателя" (receiver) — объект, на котором реализуется метод. Получатели могут быть следующими:
&self
— заимствует объект у вызывающего с помощью общей иммутабельной ссылки. После этого объект может быть повторно использован&mut self
— заимствует объект у вызывающего с помощью уникальной мутабельной ссылки. После этого объект может быть повторно использованself
— принимает владение объектом и перемещает его от вызывающего. Метод становится владельцем объекта. Объект удаляется (освобождается) после того, как метод вернул значение. Полное владение не означает автоматической мутабельностиmut self
— аналогичноself
, но метод может модифицировать объект- нет получателя — такой метод становится статичным. Обычно используется для создания конструкторов, которые по соглашению вызываются с помощью
new()
Ремарки:
- методы отличаются от функций следующим:
- методы вызываются на экземпляре типа (такого как структура или перечисление), их первый параметр — сам экземпляр (
self
) - методы позволяют держать код реализации функционала в одном месте, что способствует лучшей организации кода
- методы вызываются на экземпляре типа (такого как структура или перечисление), их первый параметр — сам экземпляр (
- особенности использования ключевого слова
self
:
self
является сокращением дляself: Self
, вместоSelf
может использоваться название структуры, например,Race
- таким образом,
Self
— это синоним реализуемого (impl
) типа и может быть использован в любом месте внутри блока self
используется как другие структуры, для доступа к его отдельным полям может использоваться точечная нотация- для демонстрации разницы между
&self
иself
попробуйте запуститьfinish()
дважды - существуют также специальные обертки типов, которые могут использоваться в качестве типов получателя, например,
Box<Self>
Трейты
Rust
позволяет создавать абстрактные типы с помощью трейтов (traits). Они похожи на интерфейсы в других языках программирования:
struct Dog {
name: String,
age: i8,
}
struct Cat {
lives: i8,
}
trait Pet {
fn talk(&self) -> String;
fn greet(&self) {
println!("Какая милаха! Как тебя зовут? {}", self.talk());
}
}
impl Pet for Dog {
fn talk(&self) -> String {
format!("Гав, меня зовут {}!", self.name)
}
}
impl Pet for Cat {
fn talk(&self) -> String {
String::from("Мау!")
}
}
fn main() {
let captain_floof = Cat { lives: 9 };
let fido = Dog { name: String::from("Фидо"), age: 5 };
captain_floof.greet();
fido.greet();
}
Ремарки:
- трейт определяет методы, которые должен предоставлять тип для реализации этого трейта
- трейты реализуются в блоке
impl <trait> for <type> { .. }
- трейты могут определять как дефолтные методы, так и методы, которые пользователь должен реализовать самостоятельно. Дефолтные методы могут полагаться на пользовательские:
greet()
имеет реализацию по умолчанию и зависит отtalk()
Автоматическая реализация трейтов
Встроенные/стандартные трейты могут быть реализованы на кастомных типах автоматически:
#[derive(Debug, Clone, Default)]
struct Player {
name: String,
strength: u8,
hit_points: u8,
}
fn main() {
let p1 = Player::default(); // трейт `Default` добавляет конструктор `default()`.
let mut p2 = p1.clone(); // трейт `Clone` добавляет метод `clone()`
p2.name = String::from("EldurScrollz");
// Трейт `Debug` добавляет поддержку вывода в терминал с помощью `{:?}`.
println!("{:?} vs. {:?}", p1, p2);
}
Автоматическая реализация выполняется с помощью макросов, многие крейты предоставляют макросы для добавления полезного функционала. Например, крейт serde предоставляет автоматическую реализацию сериализации с помощью #[derive(Serialize)]
.
Трейт-объекты
Трейт-объекты (trait objects) позволяют хранить значения разных типов, например, в коллекции:
struct Dog {
name: String,
age: i8,
}
struct Cat {
lives: i8,
}
trait Pet {
fn talk(&self) -> String;
}
impl Pet for Dog {
fn talk(&self) -> String {
format!("Гав, меня зовут {}!", self.name)
}
}
impl Pet for Cat {
fn talk(&self) -> String {
String::from("Мау!")
}
}
fn main() {
// Трейт-объект, который может содержать значение любого типа, реализующего трейт `Pet`
let pets: Vec<Box<dyn Pet>> = vec![
Box::new(Cat { lives: 9 }),
Box::new(Dog { name: String::from("Фидо"), age: 5 }),
];
for pet in pets {
println!("Привет, кто ты? {}", pet.talk());
}
}
Память после выделения pets
:
Ремарки:
- типы, реализующие определенный трейт, могут иметь разный размер. Это делает возможным такие вещи, как
Vec<dyn Pet>
в примере dyn Pet
— это способ сообщить компилятору о типе динамического размера, который реализуетPet
- в примере
pets
выделяются в стеке (stack), а вектор — в куче (heap). 2 элемента вектора являются жирными указателями (fat pointers):
- жирный указатель — это указатель двойной ширины. Он состоит из двух компонентов: указателя на реальный объект и указателя на таблицу виртуальных методов (vtable) для реализации
Pet
этого конкретного объекта - данными для
Dog
являютсяname
иage
.Cat
имеет полеlives
- жирный указатель — это указатель двойной ширины. Он состоит из двух компонентов: указателя на реальный объект и указателя на таблицу виртуальных методов (vtable) для реализации
- сравните эти выводы:
println!("{} {}", std::mem::size_of::<Dog>(), std::mem::size_of::<Cat>());
println!("{} {}", std::mem::size_of::<&Dog>(), std::mem::size_of::<&Cat>());
println!("{}", std::mem::size_of::<&dyn Pet>());
println!("{}", std::mem::size_of::<Box<dyn Pet>>());
Упражнение: библиотека GUI
Спроектируем классическую библиотеку GUI (graphical user interface — графический пользовательский интерфейс). Для простоты реализуем только его рисование — вывод в терминал в виде текста.
В нашей библиотеке будет несколько виджетов:
Window
— имеетtitle
и содержит другие виджетыButton
— имеетlabel
. В реальной библиотеке кнопка также будет принимать обработчик ее нажатияLabel
— имеетlabel
Виджеты реализуют трейт Widget
.
Напишите методы draw_into()
для реализации трейта Widget
.
pub trait Widget {
// Натуральная ширина `self`.
fn width(&self) -> usize;
// Рисуем/записываем виджет в буфер
fn draw_into(&self, buffer: &mut dyn std::fmt::Write);
// Рисуем виджет в стандартный вывод
fn draw(&self) {
let mut buffer = String::new();
self.draw_into(&mut buffer);
println!("{buffer}");
}
}
// Подпись может состоять из нескольких строк
pub struct Label {
label: String,
}
impl Label {
// Конструктор подписи
fn new(label: &str) -> Label {
Label { label: label.to_owned() }
}
}
pub struct Button {
label: Label,
}
impl Button {
// Конструктор кнопки
fn new(label: &str) -> Button {
Button { label: Label::new(label) }
}
}
pub struct Window {
title: String,
widgets: Vec<Box<dyn Widget>>,
}
impl Window {
// Конструктор окна
fn new(title: &str) -> Window {
Window { title: title.to_owned(), widgets: Vec::new() }
}
// Метод добавления виджета
fn add_widget(&mut self, widget: Box<dyn Widget>) {
self.widgets.push(widget);
}
// Метод получения максимальной ширины
fn inner_width(&self) -> usize {
std::cmp::max(
self.title.chars().count(),
self.widgets.iter().map(|w| w.width()).max().unwrap_or(0),
)
}
}
impl Widget for Window {
todo!("реализуй меня")
}
impl Widget for Button {
todo!("реализуй меня")
}
impl Widget for Label {
todo!("реализуй меня")
}
fn main() {
let mut window = Window::new("Rust GUI Demo 1.23");
window.add_widget(Box::new(Label::new("This is a small text GUI demo.")));
window.add_widget(Box::new(Button::new("Click me!")));
window.draw();
}
Вывод программы может быть очень простым:
========
Rust GUI Demo 1.23
========
This is a small text GUI demo.
| Click me! |
Или же можно воспользоваться операторами форматирования заполнения/выравнивания для выравнивания текста. Вот как можно управлять выравниванием текста с помощью разных символов (например, /
):
fn main() {
let width = 10;
println!("слева: |{:/<width$}|", "foo");
println!("по центру: |{:/^width$}|", "foo");
println!("справа: |{:/>width$}|", "foo");
}
Эти приемы позволяют сделать вывод программы таким:
+--------------------------------+
| Rust GUI Demo 1.23 |
+================================+
| This is a small text GUI demo. |
| +-------------+ |
| | Click me! | |
| +-------------+ |
+--------------------------------+
impl Widget for Window {
fn width(&self) -> usize {
// Добавляем к максимальной ширине 4 для отступов и границ
// (по одному отступу и границе с каждой стороны)
self.inner_width() + 4
}
fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
let mut inner = String::new();
for widget in &self.widgets {
widget.draw_into(&mut inner);
}
let inner_width = self.inner_width();
// TODO: после изучения обработки ошибок, можно сделать так,
// чтобы метод `draw_into()` возвращал `Result<(), std::fmt::Error>`
// и использовать здесь оператор ? вместо `unwrap()`
writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
writeln!(buffer, "| {:^inner_width$} |", &self.title).unwrap();
writeln!(buffer, "+={:=<inner_width$}=+", "").unwrap();
for line in inner.lines() {
writeln!(buffer, "| {:inner_width$} |", line).unwrap();
}
writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
}
}
impl Widget for Button {
fn width(&self) -> usize {
self.label.width() + 4 // добавляем немного отступов (по 2 с каждой стороны)
}
fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
let width = self.width();
let mut label = String::new();
self.label.draw_into(&mut label);
writeln!(buffer, "+{:-<width$}+", "").unwrap();
for line in label.lines() {
writeln!(buffer, "|{:^width$}|", &line).unwrap();
}
writeln!(buffer, "+{:-<width$}+", "").unwrap();
}
}
impl Widget for Label {
fn width(&self) -> usize {
self.label.lines().map(|line| line.chars().count()).max().unwrap_or(0)
}
fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
writeln!(buffer, "{}", &self.label).unwrap();
}
}
Дженерики
Общие функции
Rust
поддерживает дженерики (generics), которые позволяют абстрагировать алгоритмы или структуры данных (например, сортировку или двоичное дерево) по используемым или хранимым типам:
// Функция возвращает `even` или `odd` в зависимости от значения `n`.
// Здесь `T` - это параметр типа (type parameter), индикатор дженерика
fn pick<T>(n: i32, even: T, odd: T) -> T {
if n % 2 == 0 {
even
} else {
odd
}
}
fn main() {
println!("возвращенное число: {:?}", pick(97, 222, 333));
println!("возвращенный кортеж: {:?}", pick(28, ("dog", 1), ("cat", 2)));
}
Ремарки:
Rust
выводит типы дляT
на основе типов аргументов и типа возвращаемого значения- это похоже на шаблоны (templates)
C++
, ноRust
частично компилирует универсальную функцию сразу, поэтому эта функция должна быть допустимой для всех типов, соответствующих ограничениям. Например, попробуйте изменить функциюpick()
так, чтобы она возвращалаeven + odd
, еслиn == 0
. Даже если используется только реализацияpick()
с целыми числами,Rust
все равно считает ее недействительной.C++
позволит вам это сделать - общий код преобразуется в обычный (необобщенный) код на основе того, как код вызывается. Это абстракция с нулевой стоимостью: мы получаем точно такой же результат, как если бы вручную закодировали структуры данных без абстракции
Общие структуры
Дженерики могут использоваться для абстрагирования типов полей структур:
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn coords(&self) -> (&T, &T) {
(&self.x, &self.y)
}
// fn set_x(&mut self, x: T)
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
println!("{integer:?} и {float:?}");
println!("координаты: {:?}", integer.coords());
}
- Почему
T
определен дважды вimpl<T> Point<T>
? Потому что:
- это общая реализация общего типа — разные дженерики
- эти методы определяются для любого
T
- можно написать
impl Point<u32>
, тогда: Point
по-прежнему будет дженериком, и мы сможем использоватьPoint<f64>
, но методы в этом блоке будут доступны только дляPoint<u32>
- определите новую переменную
let p = Point { x: 5, y: 10.0 };
и обновите код, чтобы он работал с разными типами — для этого потребуется 2 переменные типа, например,T
иU
Ограничение трейтом
При работе с дженериками часто требуется, чтобы типы реализовывали какой-то трейт, чтобы можно было вызывать его методы.
Это делается с помощью T: Trait
или impl Trait
:
fn duplicate<T: Clone>(a: T) -> (T, T) {
(a.clone(), a.clone())
}
// struct NotClonable;
fn main() {
let foo = String::from("foo");
let pair = duplicate(foo);
println!("{pair:?}");
}
- Попробуйте создать
NotClonable
и передать ее вduplicate()
- для реализации нескольких трейтов можно использовать
+
для их объединения - третьим вариантом реализации трейта является использование ключевого слова
where
:
fn duplicate<T>(a: T) -> (T, T)
where
T: Clone,
{
(a.clone(), a.clone())
}
where
"очищает" сигнатуру функции, если у нее много параметров.where
предоставляет дополнительные функции, что делает его более мощным:
- тип слева от
:
может быть опциональным (Option<T>
) - обратите внимание, что
Rust
(пока) не поддерживает специализацию (перегрузку функции). Например, учитывая исходнуюduplicate()
, невозможно добавить специализированнуюduplicate(a: u32)
- тип слева от
impl Trait
По аналогии с ограничением типа трейтом, синтаксис impl Trait
можно использовать в параметрах и возвращаемом значении функции:
// Синтаксический сахар для:
// fn add_42_millions<T: Into<i32>>(x: T) -> i32 {
fn add_42_millions(x: impl Into<i32>) -> i32 {
x.into() + 42_000_000
}
fn pair_of(x: u32) -> impl std::fmt::Debug {
(x + 1, x - 1)
}
fn main() {
let many = add_42_millions(42_i8);
println!("{many}");
let many_more = add_42_millions(10_000_000);
println!("{many_more}");
let debuggable = pair_of(27);
println!("отлаживаемый: {debuggable:?}");
}
impl Trait
позволяет работать с безымянными типами. Значениеimpl Trait
зависит от места его использования:
- для параметра
impl Trait
похож на анонимный общий параметр с ограничением трейтом - для возвращаемого типа это означает, что он — это некий конкретный тип, реализующий признак, без указания типа. Это может быть полезным, если мы не хотим раскрывать конкретный тип в общедоступном API
- для параметра
- каков тип
debuggable
? Напишитеlet debuggable: () = ..
и изучите сообщение об ошибке
Упражнение: определение минимального значение с помощью дженерика
В этом небольшом упражнении мы с помощью трейта LessThan
реализуем общую функцию min()
, которая определяет наименьшее из двух значений.
trait LessThan {
// Возвращаем `true`, если `self` меньше чем `other`
fn less_than(&self, other: &Self) -> bool;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
struct Citation {
author: &'static str,
year: u32,
}
impl LessThan for Citation {
fn less_than(&self, other: &Self) -> bool {
if self.author < other.author {
true
} else if self.author > other.author {
false
} else {
self.year < other.year
}
}
}
fn min() {
todo!("реализуй меня")
}
fn main() {
let cit1 = Citation { author: "Shapiro", year: 2011 };
let cit2 = Citation { author: "Baumann", year: 2010 };
let cit3 = Citation { author: "Baumann", year: 2019 };
// Отладочная версия `assert_eq!`, которая удаляется из производственных сборок
debug_assert_eq!(min(cit1, cit2), cit2);
debug_assert_eq!(min(cit2, cit3), cit2);
debug_assert_eq!(min(cit1, cit3), cit3);
}
fn min<T: LessThan>(l: T, r: T) -> T {
if l.less_than(&r) {
l
} else {
r
}
}
Типы, предоставляемые стандартной библиотекой Rust
Rust
поставляется со стандартной библиотекой, которая помогает определить набор общих типов, используемых библиотеками и программами Rust
. Таким образом, две библиотеки могут беспрепятственно работать вместе, поскольку обе они используют один и тот же тип String
, например.
На самом деле Rust
содержит несколько слоев стандартной библиотеки: core
, alloc
и std
:
core
содержит самые основные типы и функции, которые не зависят отlibc
, распределителя (allocator) или даже наличия операционной системыalloc
включает типы, для которых требуется глобальный распределитель кучи, напримерVec
,Box
иArc
- встраиваемые приложения, написанные на
Rust
, часто используют толькоcore
и иногдаalloc
Документация
Rust
предоставляет замечательную документацию, например:
- описание всех подробностей циклов
- описание примитивных типов, вроде u8
- описание типов стандартной библиотеки, таких как Option или BinaryHeap
Мы можем документировать собственный код:
/// Функция определяет, можно ли первый аргумент делить на второй
///
/// Если вторым аргументом является 0, результатом является `false`
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
if rhs == 0 {
return false;
}
lhs % rhs == 0
}
Содержимое рассматривается как Markdown
. Все опубликованные библиотечные крейты (crates) Rust
автоматически документируются на docs.rs с помощью инструмента rusdoc.
Чтобы документировать элемент внутри другого элемента (например, внутри модуля), используйте //!
или /*! .. */
, называемые "внутренними комментариями документа":
//! Этот модель содержит функционал, связанный с делением целых чисел
- Взгляните на документацию крейта rand
Option
Мы уже несколько раз встречались с Option
. Он хранит либо некоторое значение Some(T)
, либо индикатор отсутствия значения None
. Например, String::find() возвращает Option<usize>
:
fn main() {
let name = "Löwe 老虎 Léopard Gepardi";
let mut position: Option<usize> = name.find('é');
println!("find вернул {position:?}");
assert_eq!(position.unwrap(), 14);
position = name.find('Z');
println!("find вернул {position:?}");
assert_eq!(position.expect("символ не найден"), 0);
}
Ремарки:
Option
широко используется, не только в стандартной библиотекеunwrap()
либо возвращает значениеSome
, либо паникует.expect()
похож наunwrap()
, но принимает сообщение об ошибке
- мы можем паниковать на
None
, но мы также можем "случайно" забыть проверитьNone
unwrap()
/expect()
обычно используются для распаковкиSome
в местах, где мы относительно уверены в корректной работе кода. Как правило, в реальных программахNone
обрабатывается лучшим способом
- мы можем паниковать на
- оптимизация ниши (niche optimization) означает, что
Option<T>
часто занимает столько же памяти, сколькоT
Result
Result
похож на Option
, но является индикатором успеха или провала операции, каждый со своим типом. В дженерике Result<T, E>
T
используется в варианте Ok
, а E
— в варианте Err
.
use std::fs::File;
use std::io::Read;
fn main() {
let file: Result<File, std::io::Error> = File::open("diary.txt");
match file {
Ok(mut file) => {
let mut contents = String::new();
if let Ok(bytes) = file.read_to_string(&mut contents) {
println!("{contents}\n({bytes} байт)");
} else {
println!("Невозможно прочитать файл");
}
}
Err(err) => {
println!("Невозможно открыть дневник: {err}");
}
}
}
Ремарки:
- как и в случае с
Option
, значениеResult
может быть извлечено с помощьюunwrap()
/expect()
Result
содержит большое количество полезных методов, поэтому рекомендуется ознакомиться с его документациейResult
— это стандартный способ обработки ошибок, о чем мы поговорим в третьей части руководства- при работе с вводом/выводом тип
Result<T, std::io::Error>
является настолько распространенным, чтоstd::io
предоставляет специальныйResult
, позволяющий указывать только тип значенияOk
:
use std::fs::File;
use std::io::{Read, Result};
// `main()` тоже может возвращать `Result`
fn main() -> Result<()> {
// Оператор `?` либо распаковывает значение `Ok`, либо распространяет ошибку (возвращает ее вызывающему)
let mut file = File::open("diary.txt")?;
let mut contents = String::new();
let bytes = file.read_to_string(&mut contents)?;
println!("{contents}\n({bytes} байт)");
Ok(())
}
String
String — это стандартный выделяемый в куче (heap-allocated) расширяемый (growable) UTF-8 строковый буфер:
fn main() {
let mut s1 = String::new();
s1.push_str("привет");
println!("s1: длина = {}, емкость = {}", s1.len(), s1.capacity());
let mut s2 = String::with_capacity(s1.len() + 1);
s2.push_str(&s1);
s2.push('!');
println!("s2: длина = {}, емкость = {}", s2.len(), s2.capacity());
let s3 = String::from("🇨🇭");
println!("s3: длина = {}, количество символов = {}", s3.len(), s3.chars().count());
}
String
реализует Deref<Target = str>: мы можем вызывать все методы str
на String
.
Ремарки:
String::new()
возвращает новую пустую строку. Когда заранее известен размер строки, можно использоватьString::with_capacity()
String::len()
возвращает размерString
в байтах (который может отличаться от количества символов)String::chars()
возвращает итератор по настоящим символам. Обратите внимание, чтоchar
может отличаться от того, что мы привыкли считать "символом", согласно кластерам графем (grapheme clusters)- когда мы говорим о строках, мы говорим о
&str
илиString
- когда тип реализует
Deref<Target = T>
, компилятор позволяет прозрачно вызывать методыT
String
реализуетDeref<Target = str>
, что предоставляет ей доступ к методамstr
- напишите и сравните
let s3 = s1.deref();
иlet s3 = &*s1;
String
реализован как обертка над вектором байт, многие методы вектора поддерживаютсяString
, но с некоторыми ограничениями (гарантиями)- сравните разные способы индексирования
String
:
- извлечение символа с помощью
s3.chars().nth(i).unwrap()
, гдеi
находится в границах строки и за их пределами - извлечение подстроки (среза — slice) с помощью
s3[0..4]
, где диапазон находится в границах символов (character boundaries) и за их пределами
- извлечение символа с помощью
Vec
Vec — это стандартный расширяемый (resizable) буфер, выделяемый в куче:
fn main() {
let mut v1 = Vec::new();
v1.push(42);
println!("v1: длина = {}, емкость = {}", v1.len(), v1.capacity());
let mut v2 = Vec::with_capacity(v1.len() + 1);
v2.extend(v1.iter());
v2.push(9999);
println!("v2: длина = {}, емкость = {}", v2.len(), v2.capacity());
// Канонический макрос для инициализации вектора с элементами
let mut v3 = vec![0, 0, 1, 2, 3, 4];
// Сохраняем только четные элементы
v3.retain(|x| x % 2 == 0);
println!("{v3:?}");
// Удаляем последовательные дубликаты
v3.dedup();
println!("{v3:?}");
}
Vec
реализует Deref<Target = [T]>: мы можем вызывать методы срезов на Vec
.
Ремарки:
Vec
— это тип коллекции, наряду сString
иHashMap
. Данные, которые он содержит, хранятся в куче. Это означает, что размер данных может быть неизвестен во время компиляции. Он может увеличиваться и уменьшаться во время выполнения- обратите внимание, что
Vec<T>
— это дженерик, но нам не нужно явно определятьT
.Rust
самостоятельно выводит тип вектора после первого вызоваpush()
vec![..]
— это канонический макрос, позволяющий создавать векторы по аналогии сVec::new()
, но с начальными элементами- для индексации вектора можно использовать
[]
, но при выходе за пределы вектора, программа запаникует. Более безопасным доступом к элементам вектора являетсяget()
, возвращающийOption
. Методpop()
удаляет последний элемент вектора Vec
имеет доступ ко всем методов срезов, о которых мы поговорим в третьей части руководства
HashMap
Стандартная хеш-карта с защитой от HashDoS-атак:
use std::collections::HashMap;
fn main() {
let mut page_counts = HashMap::new();
page_counts.insert("Adventures of Huckleberry Finn".to_string(), 207);
page_counts.insert("Grimms' Fairy Tales".to_string(), 751);
page_counts.insert("Pride and Prejudice".to_string(), 303);
if !page_counts.contains_key("Les Misérables") {
println!(
"We know about {} books, but not Les Misérables.",
page_counts.len()
);
}
for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
match page_counts.get(book) {
Some(count) => println!("{book}: {count} pages"),
None => println!("{book} is unknown."),
}
}
// Метод `entry()` позволяет вставлять значения отсутствующих ключей
for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
let page_count: &mut i32 = page_counts.entry(book.to_string()).or_insert(0);
*page_count += 1;
}
println!("{page_counts:#?}");
}
Ремарки:
HashMap
не содержится в прелюдии (prelude) и должна импортироваться явно- попробуйте следующий код. Первая строка проверяет, содержится ли книга в карте и возвращает альтернативное значение при ее отсутствии. Вторая строка вставляет альтернативное значение, если книга не найдена в карте:
let pc1 = page_counts
.get("Harry Potter and the Sorcerer's Stone")
.unwrap_or(&336);
let pc2 = page_counts
.entry("The Hunger Games".to_string())
.or_insert(374);
- в отличие от
vec!
,Rust
, к сожалению, не предоставляет макросhashmap!
- однако, начиная с
Rust 1.56
,HashMap
реализует From<[(K, V); N]>, позволяющий инициализировать хэш-карту с помощью литерального массива:
- однако, начиная с
let page_counts = HashMap::from([
("Harry Potter and the Sorcerer's Stone".to_string(), 336),
("The Hunger Games".to_string(), 374),
]);
HashMap
может создаваться из любогоIterator
, возвращающего кортежи(ключ, значение)
- в примерах мы избегаем использования
&str
в качестве ключей хэш-карт для простоты. Это возможно, но может привести к проблемам с заимствованием - рекомендуется внимательно ознакомиться с документацией
HashMap
Упражнение: счетчик
В этом упражнении мы возьмем очень простую структуру данных и сделаем ее универсальной. Она использует HashMap
для отслеживания того, какие значения были просмотрены и сколько раз появлялось каждое из них.
Первоначальная версия Counter
жестко запрограммирована для работы только со значениями u32
. Сделайте структуру и ее методы универсальными для типа отслеживаемого значения, чтобы Counter
мог работать с любым типом.
Если задание покажется вам слишком легким и вы быстро с ним справитесь, попробуйте использовать метод entry()
, чтобы вдвое сократить количество поисков хеша, необходимых для реализации метода подсчета.
use std::collections::HashMap;
// `Counter` считает, сколько раз встретилось каждое значение типа `T`
struct Counter {
values: HashMap<u32, u64>,
}
impl Counter {
// Статичный метод создания нового `Counter`
fn new() -> Self {
Counter {
values: HashMap::new(),
}
}
// Метод подсчета появлений определенного значения
fn count(&mut self, value: u32) {
if self.values.contains_key(&value) {
*self.values.get_mut(&value).unwrap() += 1;
} else {
self.values.insert(value, 1);
}
}
// Метод возврата количества появлений определенного значения
fn times_seen(&self, value: u32) -> u64 {
self.values.get(&value).copied().unwrap_or_default()
}
}
fn main() {
let mut ctr = Counter::new();
ctr.count(13);
ctr.count(14);
ctr.count(16);
ctr.count(14);
ctr.count(14);
ctr.count(11);
for i in 10..20 {
println!("saw {} values equal to {}", ctr.times_seen(i), i);
}
let mut strctr = Counter::new();
strctr.count("apple");
strctr.count("orange");
strctr.count("apple");
println!("got {} apples", strctr.times_seen("apple"));
}
Подсказки:
- общим должен быть только тип ключа
- приступите к реализации
struct Counter<T>
и внимательно изучите подсказку компилятора - общий тип должен реализовывать 2 встроенных типа: один из прелюдии, другой из
std::hash
// ...
use std::hash::Hash;
struct Counter<T: Eq + Hash> {
values: HashMap<T, u64>,
}
impl<T: Eq + Hash> Counter<T> {
// ...
fn count(&mut self, value: T) {
// Дополнительное задание.
// Здесь также можно использовать `or_insert(0)`
*self.values.entry(value).or_default() += 1;
}
fn times_seen(&self, value: T) -> u64 {
self.values.get(&value).copied().unwrap_or_default()
}
}
Трейты, предоставляемые стандартной библиотекой Rust
Рекомендуется внимательно ознакомиться с документацией каждого трейта.
Сравнения
Эти трейты поддерживают сравнение между значениями. Они могут реализовываться на типах, содержащих поля, которые реализуют эти трейты.
PartialEq и Eq
PartialEq
— это отношение частичной эквивалентности (partial equivalence relation), с требуемым методом eq()
и предоставляемым методом ne()
. Эти методы вызываются операторами ==
и !=
.
struct Key {
id: u32,
metadata: Option<String>,
}
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
Eq
— это отношение полной эквивалентности (рефлексивное, симметричное и транзитивное), реализующее PartialEq
. Функции, требующие полную эквивалентность, используют Eq
как ограничивающий трейт (trait bound).
PartialEq
может быть реализован для разных типов, а Eq
нет, поскольку он является рефлексивным:
struct Key {
id: u32,
metadata: Option<String>,
}
impl PartialEq<u32> for Key {
fn eq(&self, other: &u32) -> bool {
self.id == *other
}
}
PartialOrd и Ord
PartialOrd
определяет частичный порядок (partial ordering), с методом partial_cmp()
. Этот метод используется для реализации операторов <
, <=
, >=
и >
.
use std::cmp::Ordering;
#[derive(Eq, PartialEq)]
struct Citation {
author: String,
year: u32,
}
impl PartialOrd for Citation {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.author.partial_cmp(&other.author) {
Some(Ordering::Equal) => self.year.partial_cmp(&other.year),
author_ord => author_ord,
}
}
}
Ord
— это тотальный (total) порядок, с методом cmp()
, возвращающим Ordering
.
На практике эти трейты чаще реализуются автоматически (derive
), чем вручную.
Операторы
Перегрузка операторов реализуется с помощью трейта std::ops:
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
impl std::ops::Add for Point {
type Output = Self;
fn add(self, other: Self) -> Self {
Self { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = Point { x: 100, y: 200 };
println!("{:?} + {:?} = {:?}", p1, p2, p1 + p2);
}
- Мы можем реализовать
Add
для&Point
. В каких случаях это может быть полезным?
- Ответ:
Add::add()
потребляетself
. Если типT
, для которого перегружается оператор, не являетсяCopy
(копируемым), мы должны реализовать перегрузку оператора для&T
. Это позволяет избежать необходимости явного клонирования при вызове
- Ответ:
- Почему
Output
является ассоциированным типом? Можем ли мы сделать его параметром типа или метода?
- Короткий ответ: параметры типа функции контролируются вызывающим, а ассоциированные типы (
Output
) — тем, кто реализует трейт
- Короткий ответ: параметры типа функции контролируются вызывающим, а ассоциированные типы (
- Мы можем реализовать
Add
для двух разных типов, например,impl Add<(i32, i32)> for Point
добавит кортеж вPoint
From и Into
Типы, реализующие трейты From и Into, могут преобразовываться в другие типы:
fn main() {
let s = String::from("hello");
let addr = std::net::Ipv4Addr::from([127, 0, 0, 1]);
let one = i16::from(true);
let bigger = i32::from(123_i16);
println!("{s}, {addr}, {one}, {bigger}");
}
Into
автоматически реализуется при реализации From
:
fn main() {
let s: String = "hello".into();
let addr: std::net::Ipv4Addr = [127, 0, 0, 1].into();
let one: i16 = true.into();
let bigger: i32 = 123_i16.into();
println!("{s}, {addr}, {one}, {bigger}");
}
Приведение типов
Rust
поддерживает как неявное приведение (преобразование) типов (casting), так и явное с помощью as
:
fn main() {
let value: i64 = 1000;
println!("as u16: {}", value as u16);
println!("as i16: {}", value as i16);
println!("as u8: {}", value as u8);
}
Результаты as
всегда определяются в Rust
, поэтому являются согласованными на разных платформах. Это может не соответствовать нашему интуитивному мнению об изменении знака или приведении к меньшему типу.
Приведение типов с помощью as
— это относительно сложный инструмент, который легко использовать неправильно и который может стать источником мелких ошибок, поскольку используемые типы или диапазоны значений в них могут легко измениться. Приведение лучше всего использовать тогда, когда целью является указать безусловное усечение (unconditional truncation) (например, выбор нижних 32 битов u64
с помощью as u32
, независимо от того, что было в старших битах).
Для приведения, которое всегда можно выполнить успешно (например, из u32
в u64
), предпочтительнее использовать From
или Into
. Для приведения, которое в некоторых случаях выполнить невозможно, доступны TryFrom
и TryInto
, которые позволяют по-разному обрабатывать случаи возможности и невозможности приведения одного типа к другому.
Read и Write
Read и BufRead позволяют абстрагироваться от источников (sources) u8
:
use std::io::{BufRead, BufReader, Read, Result};
fn count_lines<R: Read>(reader: R) -> usize {
let buf_reader = BufReader::new(reader);
buf_reader.lines().count()
}
// Здесь `Result<T>` из `std::io` == `Result<T, std::io::Error>`
fn main() -> Result<()> {
let slice: &[u8] = b"foo\nbar\nbaz\n";
println!("строк в срезе: {}", count_lines(slice));
let file = std::fs::File::open(std::env::current_exe()?)?;
println!("строк в файле: {}", count_lines(file));
Ok(())
}
Write, в свою очередь, позволяет абстрагироваться от приемников (sinks) u8
:
use std::io::{Result, Write};
fn log<W: Write>(writer: &mut W, msg: &str) -> Result<()> {
writer.write_all(msg.as_bytes())?;
writer.write_all("\n".as_bytes())
}
fn main() -> Result<()> {
let mut buffer = Vec::new();
log(&mut buffer, "Hello")?;
log(&mut buffer, "World")?;
println!("{:?}", buffer);
Ok(())
}
Трейт Default
Трейт Default генерирует дефолтное значение типа:
#[derive(Debug, Default)]
struct Derived {
x: u32,
y: String,
z: Implemented,
}
#[derive(Debug)]
struct Implemented(String);
impl Default for Implemented {
fn default() -> Self {
Self("Иван Петров".into())
}
}
fn main() {
let default_struct = Derived::default();
println!("{default_struct:#?}");
let almost_default_struct =
Derived { y: "Y установлена!".into(), ..Derived::default() };
println!("{almost_default_struct:#?}");
let nothing: Option<Derived> = None;
println!("{:#?}", nothing.unwrap_or_default());
}
Ремарки:
Default
может быть реализован как вручную, так и с помощьюderive
- автоматическая реализация создает значение, в котором для всех полей установлены значения по умолчанию
- это означает, что все поля структуры также должны реализовывать
Default
- это означает, что все поля структуры также должны реализовывать
- стандартные типы
Rust
часто реализуютDefault
с разумными значениями (0
,""
и т.д.) - частичная инициализация структуры хорошо работает с
Default
- стандартная библиотека
Rust
знает, что типы могут реализовыватьDefault
, и предоставляет удобные методы, которые его используют - синтаксис
..
называется синтаксисом обновления структуры
Замыкания
Замыкания (closures) или лямбда-выражения имеют типы, которым нельзя дать имя. Однако они реализуют специальные трейты Fn, FnMut и FnOnce:
fn apply_with_log(func: impl FnOnce(i32) -> i32, input: i32) -> i32 {
println!("вызов функции на {input}");
func(input)
}
fn main() {
let add_3 = |x| x + 3;
println!("add_3: {}", apply_with_log(add_3, 10));
println!("add_3: {}", apply_with_log(add_3, 20));
let mut v = Vec::new();
let mut accumulate = |x: i32| {
v.push(x);
v.iter().sum::<i32>()
};
println!("accumulate: {}", apply_with_log(&mut accumulate, 4));
println!("accumulate: {}", apply_with_log(&mut accumulate, 5));
let multiply_sum = |x| x * v.into_iter().sum::<i32>();
println!("multiply_sum: {}", apply_with_log(multiply_sum, 3));
}
Ремарки:
Fn
(например,add_3
) не потребляет и не изменяет захваченные значения или, возможно, вообще ничего не захватывает. Ее можно вызывать несколько раз одновременноFnMut
(например,accumulate
) может менять захваченные значения. Ее можно вызывать несколько раз, но не одновременноFnOnce
(например,multiply_sum
) можно вызвать только один раз. Она может потреблять захваченные значенияFnMut
— это подтип (подтрейт — subtrait)FnOnce
.Fn
— это подтипFnMut
иFnOnce
. Это означает, что мы можем использоватьFnMut
там, где ожидаетсяFnOnce
, иFn
там, где ожидаетсяFnMut
илиFnOnce
- при определении функции, принимающей замыкание, мы должны сначала брать
FnOnce
, затемFnMut
и в концеFn
как наиболее гибкий тип - напротив, при определении замыкания мы начинаем с
Fn
- по умолчанию замыкание захватывают значение по ссылке. Ключевое слово
move
позволяет замыканию захватывать само значение
fn make_greeter(prefix: String) -> impl Fn(&str) {
return move |name| println!("{} {}", prefix, name);
}
fn main() {
let hi = make_greeter("привет".to_string());
hi("всем");
}
Упражнение: ROT13
В этом упражнении мы реализуем классический шифр "ROT13".
Меняйте только алфавитные символы ASCII
, чтобы результат оставался валидным UTF-8
.
use std::io::Read;
struct RotDecoder<R: Read> {
input: R,
rot: u8,
}
impl<R: Read> Read for RotDecoder<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
todo!("реализуй меня")
}
}
fn main() {
let mut rot =
RotDecoder { input: "Gb trg gb gur bgure fvqr!".as_bytes(), rot: 13 };
let mut result = String::new();
// `read_to_string()` вызывает `read()` под капотом и преобразует его результат в строку
rot.read_to_string(&mut result).unwrap();
println!("{}", result);
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn joke() {
let mut rot =
RotDecoder { input: "Gb trg gb gur bgure fvqr!".as_bytes(), rot: 13 };
let mut result = String::new();
rot.read_to_string(&mut result).unwrap();
assert_eq!(&result, "To get to the other side!");
}
#[test]
fn binary() {
let input: Vec<u8> = (0..=255u8).collect();
let mut rot = RotDecoder::<&[u8]> { input: input.as_ref(), rot: 13 };
let mut buf = [0u8; 256];
assert_eq!(rot.read(&mut buf).unwrap(), 256);
for i in 0..=255 {
if input[i] != buf[i] {
assert!(input[i].is_ascii_alphabetic());
assert!(buf[i].is_ascii_alphabetic());
}
}
}
}
impl<R: Read> Read for RotDecoder<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
// Читаем данные в буфер
let size = self.input.read(buf)?;
// Перебираем байты
for b in &mut buf[..size] {
// Только буквы алфавита
if b.is_ascii_alphabetic() {
// База
let base = if b.is_ascii_uppercase() { 'A' } else { 'a' } as u8;
// Сдвигаем на `rot` в пределах 26 (количество букв в английском алфавите)
*b = (*b - base + self.rot) % 26 + base;
}
}
// Возвращаем "сдвинутые" байты
Ok(size)
}
}
Это конец второй части руководства.
Материалы для более глубокого изучения рассмотренных тем:
- Книга/учебник по Rust (на русском языке) — главы 6, 8, 10, 13, 17 и 18
- rustlings — упражнения 04-06, 09, 11, 12, 14 и 15
- Rust на примерах (на русском языке) — 5, 6, 14, 16, 19 и 20
- Rust by practice — упражнения 8-12 и 18
Happy coding!