Приклад програми з використанням структур (An Example Program Using Structs)
Щоб зрозуміти, коли ми можемо захотіти використати структури, давайте напишемо програму, яка обчислює площу прямокутника. Ми почнемо з використання окремих змінних, а потім переробимо програму так, щоб замість цього використовувати структури.
Давайте створимо новий двійковий проєкт за допомогою Cargo під назвою rectangles, який братиме ширину та висоту прямокутника, задані в пікселях, і обчислюватиме площу прямокутника. У Лістингу (Listing) 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: за допомогою кортежів.
Переробка з кортежами (Refactoring with Tuples)
У Лістингу (Listing) 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. Комусь іншому було б ще важче це зрозуміти й тримати в пам’яті, якби він або вона захотіли використати наш код. Оскільки ми не передали зміст наших даних у нашому коді, тепер легше ввести помилки.
Переробка зі структурами (Refactoring with Structs: Adding More Meaning)
Ми використовуємо структури, щоб додати зміст шляхом маркування даних. Ми можемо перетворити кортеж, який використовуємо, на структуру з назвою для цілого, а також назвами для частин, як показано в Лістингу (Listing) 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. Усередині фігурних дужок ми визначили поля як width і height, обидва з типом u32. Потім у main ми створили конкретний екземпляр Rectangle, який має ширину 30 і висоту 50.
Наша функція area тепер визначена з одним параметром, який ми назвали rectangle, чий тип є незмінним запозиченням екземпляра структури Rectangle. Як ми обговорювали в розділі 4, ми хочемо запозичити структуру, а не брати її у володіння. Таким чином main зберігає володіння і може продовжувати використовувати rect1, через що ми використовуємо & у сигнатурі функції та там, де викликаємо функцію.
Функція area отримує доступ до полів width і height екземпляра Rectangle (зверніть увагу, що доступ до полів запозиченого екземпляра структури не переміщує значення полів, саме тому ви часто бачите запозичення структур). Наша сигнатура функції для area тепер точно каже те, що ми маємо на увазі: обчислити площу Rectangle, використовуючи його поля width і height. Це передає, що width і height пов’язані між собою, і дає описові назви значенням замість використання значень індексів кортежа 0 і 1. Це виграш для ясності.
Додавання корисної функціональності за допомогою виведених трейтів (Adding Useful Functionality with Derived Traits)
Було б корисно мати змогу надрукувати екземпляр Rectangle, коли ми налагоджуємо нашу програму, і побачити значення всіх його полів. У Лістингу (Listing) 5-11 спробовано використати макрос (macro) println!, як ми вже робили в попередніх розділах. Однак це не спрацює.
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! і заповнювачем {}.
Якщо ми продовжимо читати помилки, то знайдемо таку корисну примітку:
| |`Rectangle` cannot be formatted with the default formatter
| required by this formatting parameter
Спробуймо це! Виклик макроса println! тепер виглядатиме так: println!("rect1 is {rect1:?}");. Розміщення специфікатора :? всередині фігурних дужок повідомляє
println!, що ми хочемо використати формат виводу, який називається Debug. Трейт (trait) Debug дає нам змогу друкувати нашу структуру так, щоб це було корисно для розробників, аби ми могли бачити її значення під час налагодження нашого коду.
Скомпілюйте код із цією зміною. От халепа! Ми все ще отримуємо помилку:
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 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, — застосувати макрос (macro) dbg!, який бере у володіння вираз (на відміну від println!, який бере посилання), друкує ім’я файлу та номер рядка, де виклик цього макроса dbg! відбувається у вашому коді, разом із результативним значенням цього виразу, і повертає володіння значення.
Примітка: виклик макроса
dbg!друкує в консольний потік стандартної помилки (stderr), на відміну відprintln!, який друкує в консольний потік стандартного виводу (stdout). Ми докладніше говоритимемо проstderrіstdoutу розділі “Redirecting Errors to Standard Error” розділу 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; для отримання додаткової інформації дивіться розділ “Attributes” у Rust Reference.
Наша функція area дуже специфічна: вона лише обчислює площу прямокутників. Було б корисно тісніше пов’язати цю поведінку з нашою структурою Rectangle, тому що вона не працюватиме з жодним іншим типом. Давайте подивимося, як ми можемо продовжити переробку цього коду, перетворивши функцію area на метод area, визначений для нашого типу Rectangle.