Практическое руководство по Rust. 2/4

от автора

Hello world!

Представляю вашему вниманию вторую часть практического руководства по Rust.

Другой формат, который может показаться вам более удобным.

Руководство основано на Comprehensive Rust — руководстве по Rust от команды Android в Google и рассчитано на людей, которые уверенно владеют любым современным языком программирования. Еще раз: это руководство не рассчитано на тех, кто только начинает кодить ?

В этой части мы рассмотрим следующие темы:

  • сопоставление с образцом (pattern matching) — извлечение данных из структур
  • методы — функции, ассоциированные с типами
  • трейты (traits) — поведение, общее для нескольких типов
  • дженерики (generics) — общие типы
  • типы и трейты, предоставляемые стандартной библиотекой Rust

Материалы для более глубокого изучения названных тем:

Также см. Большую шпаргалку по 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
  • сравните эти выводы:

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)     } }

Это конец второй части руководства.

Материалы для более глубокого изучения рассмотренных тем:

Happy coding!



ссылка на оригинал статьи https://habr.com/ru/articles/794941/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *