Пример программы, использующей структуры
Чтобы понять, когда нам могут понадобиться структуры, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнём с использования одиночных переменных, а затем будем улучшать программу до использования структур.
Давайте создадим новый проект программы при помощи Cargo и назовём его rectangles. Наша программа будет получать на вход длину и ширину прямоугольника в пикселях и затем рассчитывать площадь прямоугольника. Листинг 5-8 показывает один из коротких вариантов кода, который позволит нам сделать именно то, что надо.
fn main() { let width1 = 30; let height1 = 50; println!( "Площадь прямоугольника равна {} квадратных пикселей.", 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`
Площадь прямоугольника равна 1500 квадратных пикселей.
Этот код успешно вычисляет площадь прямоугольника, вызывая функцию area
с каждым измерением, но мы можем улучшить его ясность и читабельность.
Проблема данного метода очевидна, если взглянуть на сигнатуру area
:
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"Площадь прямоугольника равна {} квадратных пикселей.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
Функция area
должна вычислять площадь одного прямоугольника, но функция, которую мы написали, имеет два параметра, и нигде в нашей программе не ясно, что эти параметры взаимосвязаны. Было бы более читабельным и организованным сгруппировать ширину и высоту вместе. В разделе "Кортежи" Главы 3 мы уже обсуждали один из способов сделать это — использовать кортежи.
Рефакторинг внедрением кортежей
Листинг 5-9 — это другая версия программы, использующая кортежи.
fn main() { let rect1 = (30, 50); println!( "Площадь прямоугольника равна {} квадратных пикселей.", 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!( "Площадь прямоугольника равна {} квадратных пикселей.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
Здесь мы определили структуру и дали ей имя 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: {}", rect1);
}
При компиляции этого кода мы получаем ошибку. Главное в ней вот что:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
Макрос println!
умеет выполнять множество видов форматирования, и по умолчанию фигурные скобки в println!
означают использование форматирования, определяемого трейтом Display
. Форматирование по нему создаёт репрезентацию значения, предназначеную для непосредственно конечного пользователя. Базовые типы, изученные ранее, по умолчанию реализуют трейт Display
, потому что есть только один способ отобразить число 1
или любой другой примитивный тип. Но для структур форматирование println!
менее очевидно, потому что есть гораздо больше способов отобразить её в консоли: с запятыми или без; с фигурными скобками или без; все ли поля или только некоторые? Из-за этой неоднозначности Rust не пытается угадать, что нам нужно, а так как структуры не имеют реализации Display
по умолчанию, код не компилируется: программа не знает, что подставить на место {}
в println!
.
Продолжив чтение текста ошибки, мы найдём полезный совет:
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Давайте попробуем его использовать! Вызов макроса println!
теперь будет выглядеть так println!("rect1: {rect1:?}");
. Ввод спецификатора :?
внутри фигурных скобок говорит макросу println!
, что мы хотим использовать другой формат вывода, известный как Debug
. Трейт Debug
позволяет печатать структуру способом, удобным именно для разработчиков, чтобы видеть значение во время отладки кода.
Скомпилируем код с этими изменениями. Упс! Мы всё ещё получаем ошибку:
error[E0277]: `Rectangle` doesn't implement `Debug`
Но компилятор снова даёт нам полезное замечание:
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Rust имеет функциональность для печати отладочной информации, но мы должны явно включить эту функциональность для нашей структуры, чтобы сделать её доступной. Для этого добавим внешний атрибут #[derive(Debug)]
сразу перед определением структуры, как показано в Листинге 5-12.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1: {rect1:?}"); }
Теперь при запуске программы мы не получим ошибок и увидим следующий вывод:
$ 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: 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: Rectangle {
width: 30,
height: 50,
}
Другой способ распечатать значение в формате Debug
— использовать макрос dbg!
, который печатает название файла и номер строки, где происходит вызов макроса dbg!
, плюс переданное в него выражение со значением, в которое то вычисляется. Макрос dbg!
принимает владение над переданным в него выражением (в отличие от макроса println!
, использующего ссылки), но возвращает результат переданного выражения.
Примечание: При вызове макроса
dbg!
выполняется печать в стандартный поток ошибок (stderr
), в отличие отprintln!
, который использует стандартный поток вывода в консоль (stdout
). Подробнее оstderr
иstdout
мы поговорим в разделе "Вывод сообщений об ошибках в стандартный поток ошибок" Главы 12.
Вот пример, когда нас интересует 1) значение, которое будет приписано полю width
, и 2) значение всей структуры в 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); }
Мы можем обернуть выражение 30 * scale
макросом dbg!
, потому что dbg!
возвращает владение значением выражения. Поле width
получит то же значение, как если бы у нас не было вызова dbg!
. Далее, мы не хотим, чтобы макрос dbg!
становился владельцем rect1
, поэтому используем ссылку на 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
. Для получения дополнительной информации, ознакомьтесь с разделом "Атрибуты" Справочника Rust.
Функция area
является довольно специфичной: она считает только площадь прямоугольников. Было бы полезно привязать данное поведение как можно ближе к структуре Rectangle
, потому что наш специфичный код не будет работать с любым другим типом. Давайте рассмотрим, как можно улучшить наш код, превратив функцию area
в метод area
, определённый для типа Rectangle
.