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!