Как стать автором
Обновить
1984.61

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

Уровень сложности Средний
Время на прочтение 30 мин
Количество просмотров 6.5K



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!




Теги:
Хабы:
+29
Комментарии 2
Комментарии Комментарии 2

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud