Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Пример программы со структурами

Чтобы понять, когда пригодятся структуры, напишем программу, которая вычисляет площадь прямоугольника. Начнём с отдельных переменных, а потом будем переделывать программу, пока не придём к структурам.

Создадим через Cargo новый бинарный проект rectangles. Программа будет брать ширину и высоту прямоугольника в пикселях и вычислять его площадь. В листинге 5-8 короткая программа в src/main.rs, которая делает ровно это — одним из возможных способов.

Файл: src/main.rs
fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}
Листинг 5-8: Вычисляем площадь прямоугольника, заданного отдельными переменными ширины и высоты

Запустим программу командой cargo run:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

Код справляется с задачей: вызывает функцию area с обоими размерами и получает площадь. Но его можно сделать понятнее и читабельнее.

Проблема хорошо видна по сигнатуре area:

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Функция area должна вычислять площадь одного прямоугольника, но параметров у неё два, и нигде в программе не сказано, что эти параметры связаны между собой. Было бы понятнее и удобнее держать ширину и высоту вместе. Один способ сделать это мы уже обсуждали в разделе «Кортежи» главы 3 — использовать кортеж.

Рефакторинг с кортежами

В листинге 5-9 — версия программы с кортежем.

Файл: src/main.rs
fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}
Листинг 5-9: Задаём ширину и высоту прямоугольника кортежем

С одной стороны, программа стала лучше. Кортеж добавляет немного структуры, и теперь мы передаём всего один аргумент. Но с другой стороны, эта версия менее понятна: у элементов кортежа нет имён, приходится обращаться к ним по индексам, и вычисление становится не таким очевидным.

Для площади неважно, где ширина, а где высота, — но если мы захотим нарисовать прямоугольник на экране, путаница станет ошибкой! Придётся держать в голове, что width — это элемент кортежа с индексом 0, а height — с индексом 1. Тому, кто будет пользоваться нашим кодом, разобраться в этом и запомнить будет ещё сложнее. Мы не выразили в коде смысл наших данных, и теперь в нём легче ошибиться.

Рефакторинг со структурами

Структуры позволяют придать данным смысл, снабдив их подписями. Кортеж можно превратить в структуру, у которой есть имя и у целого, и у частей, как в листинге 5-10.

Файл: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}
Листинг 5-10: Определяем структуру Rectangle

Здесь мы определили структуру Rectangle (прямоугольник). В фигурных скобках объявлены поля width и height, оба типа u32. Затем в main мы создали конкретный экземпляр Rectangle шириной 30 и высотой 50.

Теперь у функции area один параметр, rectangle, и его тип — неизменяемое заимствование экземпляра структуры Rectangle. Как мы говорили в главе 4, лучше позаимствовать структуру, чем забирать владение ею. Тогда main сохраняет владение и может дальше пользоваться rect1 — для этого мы и пишем & и в сигнатуре функции, и в месте её вызова.

Функция area обращается к полям width и height экземпляра Rectangle. (Заметьте: обращение к полям заимствованного экземпляра структуры не перемещает значения полей — поэтому структуры так часто и передают по ссылке.) Сигнатура area теперь говорит ровно то, что мы имеем в виду: вычислить площадь Rectangle по его полям width и height. Видно, что ширина и высота связаны друг с другом, а у значений понятные имена вместо индексов кортежа 0 и 1. Ясность только выиграла.

Добавляем возможности с помощью выводимых трейтов

Во время отладки было бы удобно вывести экземпляр Rectangle и увидеть значения всех его полей. В листинге 5-11 мы пробуем сделать это макросом println!, как в предыдущих главах. Но так не получится.

Файл: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1}");
}
Листинг 5-11: Пытаемся вывести экземпляр Rectangle

При компиляции получаем ошибку, главное в которой — вот это сообщение:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

Макрос println! умеет форматировать вывод по-разному. По умолчанию фигурные скобки говорят ему использовать форматирование Display — вывод для конечного пользователя. Примитивные типы, которые мы видели до сих пор, реализуют Display по умолчанию: показать пользователю 1 или любое другое примитивное значение можно, по сути, только одним способом. А вот со структурами неясно, как println! должен форматировать вывод: вариантов слишком много. Нужны ли запятые? Выводить ли фигурные скобки? Показывать ли все поля? Из-за этой неоднозначности Rust не пытается угадать, чего мы хотим, и у структур нет готовой реализации Display для println! и заполнителя {}.

Если читать ошибки дальше, найдётся полезная подсказка:

help: the trait `std::fmt::Display` is not implemented for `Rectangle`
  --> src/main.rs:1:1

Попробуем! Теперь вызов println! будет выглядеть так: println!("rect1 is {rect1:?}");. Спецификатор :? внутри фигурных скобок говорит println!, что нужен формат вывода Debug. Трейт Debug позволяет выводить структуру в виде, удобном для разработчика, — чтобы видеть её значение во время отладки.

Скомпилируйте код с этим изменением. Вот незадача! Ошибка всё равно есть:

error[E0277]: `Rectangle` doesn't implement `Debug`

Но компилятор снова даёт полезную подсказку:

   |                        required by this formatting parameter
   |

Возможность выводить отладочную информацию в Rust есть, но для своей структуры её нужно явно включить. Для этого прямо перед определением структуры добавляем внешний атрибут #[derive(Debug)], как в листинге 5-12.

Файл: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
Листинг 5-12: Добавляем атрибут, выводящий трейт Debug, и печатаем экземпляр Rectangle в отладочном формате

Теперь программа запускается без ошибок, и мы видим:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Отлично! Не самый красивый вывод, но значения всех полей видны, а при отладке это точно поможет. Для больших структур удобнее вывод, который легче читать, — тогда в строке println! вместо {:?} можно написать {:#?}. В нашем примере стиль {:#?} даст такой вывод:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Другой способ вывести значение в формате Debug — макрос dbg!. В отличие от println!, который принимает ссылку, dbg! забирает владение выражением. Он выводит имя файла и номер строки, где в коде стоит вызов dbg!, вместе со значением выражения, а потом возвращает владение этим значением обратно.

Примечание: макрос dbg! пишет в стандартный поток ошибок (stderr), а не в стандартный поток вывода (stdout), как println!. Подробнее о stderr и stdout — в разделе «Вывод ошибок в stderr» главы 12.

Вот пример, где нас интересует значение, которое присваивается полю width, и значение всей структуры в rect1:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Мы оборачиваем в dbg! выражение 30 * scale. Поскольку dbg! возвращает владение значением выражения, поле width получит то же значение, что и без dbg!. А вот владение rect1 отдавать dbg! мы не хотим, поэтому в следующем вызове передаём ссылку на rect1. Вывод примера выглядит так:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Первая часть вывода пришла из строки 10 файла src/main.rs, где мы отлаживаем выражение 30 * scale; его значение — 60 (форматирование Debug для целых чисел выводит просто их значение). Вызов dbg! в строке 14 файла src/main.rs выводит значение &rect1, то есть структуру Rectangle, — в «красивом» формате Debug для типа Rectangle. Макрос dbg! очень выручает, когда пытаешься понять, что на самом деле делает твой код!

Кроме Debug, Rust предоставляет ещё ряд трейтов, которые можно использовать с атрибутом derive, чтобы добавить своим типам полезное поведение. Эти трейты и их поведение перечислены в приложении C. Как реализовывать такие трейты со своим поведением и как создавать собственные трейты, мы разберём в главе 10. Есть и много других атрибутов, помимо derive; подробнее — в разделе «Attributes» справочника Rust.

Наша функция area очень узкоспециализированная: она умеет считать только площадь прямоугольника. Раз ни с каким другим типом она работать не будет, было бы полезно теснее связать это поведение со структурой Rectangle. Посмотрим, как продолжить рефакторинг и превратить функцию area в метод area, определённый для типа Rectangle.