Пример программы со структурами
Чтобы понять, когда пригодятся структуры, напишем программу, которая вычисляет площадь прямоугольника. Начнём с отдельных переменных, а потом будем переделывать программу, пока не придём к структурам.
Создадим через Cargo новый бинарный проект rectangles. Программа будет брать ширину и высоту прямоугольника в пикселях и вычислять его площадь. В листинге 5-8 короткая программа в 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
}
Запустим программу командой 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 — версия программы с кортежем.
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
}
С одной стороны, программа стала лучше. Кортеж добавляет немного структуры, и теперь мы передаём всего один аргумент. Но с другой стороны, эта версия менее понятна: у элементов кортежа нет имён, приходится обращаться к ним по индексам, и вычисление становится не таким очевидным.
Для площади неважно, где ширина, а где высота, — но если мы захотим нарисовать
прямоугольник на экране, путаница станет ошибкой! Придётся держать
в голове, что width — это элемент кортежа с индексом 0, а height — с
индексом 1. Тому, кто будет пользоваться нашим кодом, разобраться в этом и
запомнить будет ещё сложнее. Мы не выразили в коде смысл наших данных, и
теперь в нём легче ошибиться.
Рефакторинг со структурами
Структуры позволяют придать данным смысл, снабдив их подписями. Кортеж можно превратить в структуру, у которой есть имя и у целого, и у частей, как в листинге 5-10.
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
}
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!, как в предыдущих главах. Но так не
получится.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
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.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
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.