Пример программы с использованием структур
Чтобы понять, когда стоит использовать структуры, напишем программу, которая вычисляет площадь прямоугольника. Начнём с отдельных переменных, а затем постепенно переделаем программу, заменив их структурами.
Создадим новый бинарный проект 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! может делать многие виды форматирования, и по умолчанию фигурные скобки говорят 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.
#[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!выводит в поток консоли стандартной ошибки (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,
}
Мы видим, что первая часть вывода пришла из файла src/main.rs строки 10, где мы отлаживаем выражение 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.