Пример программы, использующей структуры
To understand when we might want to use structs, let’s write a program that calculates the area of a rectangle. We’ll start by using single variables and then refactor the program until we’re using structs instead.
Давайте создадим новый проект программы при помощи 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
}
In one way, this program is better. Tuples let us add a bit of structure, and we’re now passing just one argument. But in another way, this version is less clear: Tuples don’t name their elements, so we have to index into the parts of the tuple, making our calculation less obvious.
Если мы перепутаем местами ширину с высотой при расчёте площади, то ничего страшного не произойдёт. Но если мы захотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина width находится в кортеже по индексу 0, а высота height — по индексу 1. Если кто-то другой работал бы с нашим кодом, ему бы пришлось разбираться в этом и также помнить про очерёдность. Поскольку наше решение всё ещё не передаёт смысла используемых значений, очень легко совершить ошибку.
Refactoring with Structs
Структуры используются, чтобы добавлять смысл данным при помощи назначения им осмысленных имён. Мы можем переделать используемый кортеж в структуру с единым именем для сущности и частными названиями её частей, как показано в Листинге 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 structHere, we’ve defined a struct and named it Rectangle. Inside the curly brackets, we defined the fields as width and height, both of which have type u32. Then, in main, we created a particular instance of Rectangle that has a width of 30 and a height of 50.
Наша функция area теперь определена с одним параметром rectangle, имеющим тип неизменяемой ссылки на структуру Rectangle. Как упоминалось в Главе 4, следует заимствовать структуру, а не передавать владение на неё. Таким образом, функция main продолжает владеть rect1 и может использовать её дальше, для чего мы и используем & в сигнатуре и в вызове функции.
The area function accesses the width and height fields of the Rectangle instance (note that accessing fields of a borrowed struct instance does not move the field values, which is why you often see borrows of structs). Our function signature for area now says exactly what we mean: Calculate the area of Rectangle, using its width and height fields. This conveys that the width and height are related to each other, and it gives descriptive names to the values rather than using the tuple index values of 0 and 1. This is a win for clarity.
Adding Functionality with Derived Traits
Было бы полезно иметь возможность печатать экземпляр Rectangle во время отладки программы и видеть значения всех полей. Листинг 5-11 использует макрос println!, который мы уже использовали в предыдущих главах. Тем не менее, код ниже не сработает.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
Rectangle instanceПри компиляции этого кода мы получаем ошибку. Главное в ней вот что:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
Макрос println! умеет выполнять множество видов форматирования, и по умолчанию фигурные скобки в println! означают использование форматирования, определяемого трейтом Display. Форматирование по нему создаёт репрезентацию значения, предназначеную для непосредственно конечного пользователя. Базовые типы, изученные ранее, по умолчанию реализуют трейт Display, потому что есть только один способ отобразить число 1 или любой другой примитивный тип. Но для структур форматирование println! менее очевидно, потому что есть гораздо больше способов отобразить её в консоли: с запятыми или без; с фигурными скобками или без; все ли поля или только некоторые? Из-за этой неоднозначности Rust не пытается угадать, что нам нужно, а так как структуры не имеют реализации Display по умолчанию, код не компилируется: программа не знает, что подставить на место {} в println!.
Продолжив чтение текста ошибки, мы найдём полезный совет:
| |`Rectangle` cannot be formatted with the default formatter
| required by this formatting parameter
Let’s try it! The println! macro call will now look like println!("rect1 is {rect1:?}");. Putting the specifier :? inside the curly brackets tells println! we want to use an output format called Debug. The Debug trait enables us to print our struct in a way that is useful for developers so that we can see its value while we’re debugging our code.
Скомпилируем код с этими изменениями. Упс! Мы всё ещё получаем ошибку:
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: {rect1:?}");
}
Debug trait and printing the Rectangle instance using debug formattingТеперь при запуске программы мы не получим ошибок и увидим следующий вывод:
$ 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!, использующего ссылки), но возвращает результат переданного выражения.
Note: Calling the dbg! macro prints to the standard error console stream (stderr), as opposed to println!, which prints to the standard output console stream (stdout). We’ll talk more about stderr and stdout in the “Redirecting Errors to Standard Error” section in Chapter 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.
Our area function is very specific: It only computes the area of rectangles. It would be helpful to tie this behavior more closely to our Rectangle struct because it won’t work with any other type. Let’s look at how we can continue to refactor this code by turning the area function into an area method defined on our Rectangle type.