Приклад програми, що використовує структури

Щоб зрозуміти, де можна використовувати структури, напишімо програму, що обчислює площу прямокутника. Почнемо з окремих змінних, а потім рефакторизуємо її так, щоб вона використовувала структури.

За допомогою 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
}

Listing 5-8: Calculating the area of a rectangle specified by separate width and height variables

Тепер запустимо програму командою 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
}

Listing 5-9: Specifying the width and height of the rectangle with a tuple

З одного боку, ця програма краща. Кортежі додають трохи структурованості, і тепер ми передаємо лише один аргумент. Але з іншого боку ця версія менш зрозуміла: кортежі не мають назв для своїх елементів, тому тепер доводиться індексувати частини кортежу, що робить наші обчислення менш очевидними.

Не має значення, якщо ми переплутаємо ширину і висоту при обчисленні площі, але якщо ми захочемо намалювати прямокутник на екрані, це матиме значення! Нам доведеться пам'ятати, що ширина має індекс 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
}

Блок коду 5-10: визначення структури 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! так само як це було в попередніх розділах. Але цей код не працює.

Файл: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}

Listing 5-11: Attempting to print a Rectangle instance

Якщо скомпілювати цей код, ми дістанемо помилку із головним повідомленням:

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);
}

Listing 5-12: Adding the attribute to derive the Debug trait and printing the Rectangle instance using debug formatting

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.rs, де ми зневаджуємо вираз 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.