Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Пример программы с использованием структур

Чтобы понять, когда нам может понадобиться использовать структуры, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнем с использования отдельных переменных, а затем будем перерабатывать программу, пока вместо них не начнем использовать структуры.

Создадим новый бинарный проект с помощью Cargo под названием rectangles, который будет принимать ширину и высоту прямоугольника, заданные в пикселях, и вычислять его площадь. Листинг 5-8 показывает короткую программу с одним из способов сделать именно это в файле src/main.rs нашего проекта.

Filename: 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: Вычисление площади прямоугольника, заданного отдельными переменными ширины и высоты

Теперь запустите эту программу с помощью 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 показывает другую версию нашей программы, в которой используются кортежи.

Filename: 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: Указание ширины и высоты прямоугольника с помощью кортежа

В каком-то смысле эта программа лучше. Кортежи позволяют добавить немного структуры, и теперь мы передаем только один аргумент. Но с другой стороны эта версия менее понятна: кортежи не дают имен своим элементам, поэтому нам приходится обращаться к частям кортежа по индексам, из-за чего вычисление становится менее очевидным.

Если перепутать ширину и высоту, для вычисления площади это не имело бы значения, но если мы захотим нарисовать прямоугольник на экране, значение уже будет! Нам пришлось бы помнить, что width находится в кортеже по индексу 0, а height – по индексу 1. Другому человеку, который будет использовать наш код, было бы еще сложнее разобраться в этом и удерживать это в памяти. Поскольку мы не выразили смысл наших данных в коде, теперь ошибки внести проще.

Рефакторинг со структурами

Мы используем структуры, чтобы добавить смысл, присваивая данным метки. Мы можем превратить используемый кортеж в структуру с именем для целого значения и именами для его частей, как показано в листинге 5-10.

Filename: 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
}
Listing 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! так же, как использовали его в предыдущих главах. Однако это не сработает.

Filename: 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: Попытка вывести экземпляр Rectangle

Когда мы компилируем этот код, получаем ошибку с таким основным сообщением:

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. Трейт Debug позволяет выводить нашу структуру способом, полезным для разработчиков, чтобы мы могли видеть ее значение во время отладки кода.

Скомпилируйте код с этим изменением. Досадно! Мы все еще получаем ошибку:

error[E0277]: `Rectangle` doesn't implement `Debug`

Но снова компилятор дает нам полезную заметку:

   |                        required by this formatting parameter
   |

Rust действительно включает функциональность для вывода отладочной информации, но нам нужно явно подключить ее, чтобы сделать эту функциональность доступной для нашей структуры. Для этого мы добавляем внешний атрибут #[derive(Debug)] прямо перед определением структуры, как показано в листинге 5-12.

Filename: 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: Добавление атрибута для получения трейта 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,
}

Мы видим, что первая часть вывода пришла из строки 10 файла src/main.rs, где мы отлаживаем выражение 30 * scale, а его результирующее значение равно 60 (форматирование Debug, реализованное для целых чисел, выводит только их значение). Вызов dbg! в строке 14 файла src/main.rs выводит значение &rect1, то есть структуру Rectangle. Этот вывод использует красивое форматирование Debug типа Rectangle. Макрос dbg! может быть действительно полезен, когда вы пытаетесь понять, что делает ваш код!

Помимо трейта Debug, Rust предоставляет нам ряд трейтов для использования с атрибутом derive, которые могут добавлять полезное поведение нашим пользовательским типам. Эти трейты и их поведение перечислены в Приложении C. В главе 10 мы рассмотрим, как реализовать эти трейты с пользовательским поведением, а также как создавать собственные трейты. Также существует много атрибутов помимо derive; дополнительную информацию смотрите в разделе «Attributes» справочника Rust.

Наша функция area очень специфична: она вычисляет только площадь прямоугольников. Было бы полезно связать это поведение теснее с нашей структурой Rectangle, потому что оно не будет работать ни с каким другим типом. Давайте посмотрим, как можно продолжить рефакторинг этого кода, превратив функцию area в метод area, определенный для нашего типа Rectangle.