Пример использования структур
Чтобы понять, когда нам может понадобиться использование структур, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнём с использования одиночных переменных, а затем будем улучшать программу до использования структур.
Давайте создадим новый проект программы при помощи 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 }
Теперь запустим программу, используя 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 }
С одной стороны, эта программа лучше. Кортежи позволяют добавить немного структуры, и теперь мы передаём только один аргумент. Но с другой стороны, эта версия менее понятна: кортежи не называют свои элементы, поэтому нам приходится индексировать части кортежа, что делает наше вычисление менее очевидным.
Если мы перепутаем местами ширину с высотой при расчёте площади, то это не имеет значения. Но если мы хотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина 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 }
Здесь мы определили структуру и дали ей имя 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);
}
При компиляции этого кода мы получаем ошибку с сообщением:
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 is {:?}", 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.
Файл: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {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 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!
выполняется печать в стандартный поток ошибок (stderr
), в отличие отprintln!
, который использует стандартный поток вывода в консоль (stdout
). Подробнее оstderr
иstdout
мы поговорим в разделе «Запись сообщений об ошибках в стандартный вывод ошибок вместо стандартного вывода» главы 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!
. Мы не хотим, чтобы макрос 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
.