Приклад програми, що використовує структури
Щоб зрозуміти, де можна використовувати структури, напишімо програму, що обчислює площу прямокутника. Почнемо з окремих змінних, а потім рефакторизуємо її так, щоб вона використовувала структури.
За допомогою 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 [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
This code succeeds in figuring out the area of the rectangle by calling the area
function with each dimension, but we can do more to make this code clear and readable.
Проблема в цьому коді очевидна, якщо поглянути на сигнатуру функції 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 }
З одного боку, ця програма краща. Кортежі додають трохи структурованості, і тепер ми передаємо лише один аргумент. Але з іншого боку ця версія менш зрозуміла: кортежі не мають назв для своїх елементів, тому тепер доводиться індексувати частини кортежу, що робить наші обчислення менш очевидними.
Не має значення, якщо ми переплутаємо ширину і висоту при обчисленні площі, але якщо ми захочемо намалювати прямокутник на екрані, це матиме значення! Нам доведеться пам'ятати, що ширина
має індекс 0
у кортежі, а висота
має індекс 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 = {:?}", 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); }
Now when we run the program, we won’t get any errors, and we’ll see the following output:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [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 [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.
Here’s an example where we’re interested in the value that gets assigned to the width
field, as well as the value of the whole struct in 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!
повертає володіння виразу, поле with
отримає це саме значення, як ніби й не було виклику dbg!
. Ми не хочемо, щоб dbg!
перебирав володіння rect1
, так що ми використовуємо посилання на rect1
при наступному виклику. Ось як виглядає те, що виводить цей приклад:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
Ми можемо побачити, що перший фрагмент виведений з рядка 10 src/main. s, де ми відстежуємо вираз 30 * scale
, і його обчислене значення 60 (реалізація форматування Debug
для цілих чисел виводить самі їхні значення). Виклик dbg!
у рядку 14 src/main. s виводить значення &rect1
, яке дорівнює структурі Rectangle
. Виведення використовує покращене форматування Debug
для типу Rectangle
. Макрос dbg!
може бути дійсно корисним, коли ви намагаєтеся розібратися, що робить ваш код!
На додачу до трейту Debug
, Rust надає нам ряд трейтів, що можна використовувати з атрибутом derive
, які можуть додати корисну поведінку до наших власних типів. Ці трейти та їхня поведінка перераховані в Додатку C. Ми розглянемо, як реалізувати ці трейти з кастомізованою поведінкою і як створювати свої власні трейти в Розділі 10. Також існує багато атрибутів, відмінних від
derive
; для отримання додаткової інформації дивіться розділ "Атрибути" Довідника Rust.
Функція area
дуже конкретна: вона розраховує лише площу прямокутників. Було б корисно прив'язати цю поведінку до нашої структури Rectangle
, оскільки вона не буде працювати з жодним іншим типом. Подивімося, як ми можемо продовжувати рефакторизовувати цей код, перетворивши функцію area
на метод area
, визначений на нашому типі Rectangle
.