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

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Пример программы с использованием структур. Эта лабораторная работа является частью Книги по Rust. Вы можете практиковать свои навыки Rust в LabEx.

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100396{{"Пример программы с использованием структур"}} rust/integer_types -.-> lab-100396{{"Пример программы с использованием структур"}} rust/function_syntax -.-> lab-100396{{"Пример программы с использованием структур"}} rust/expressions_statements -.-> lab-100396{{"Пример программы с использованием структур"}} end

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

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

Создадим новый бинарный проект с помощью Cargo под названием rectangles, который будет принимать ширину и высоту прямоугольника, заданную в пикселях, и вычислять площадь прямоугольника. Listing 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: Вычисление площади прямоугольника, заданного отдельными переменными ширины и высоты

Теперь запустите эту программу с помощью cargo run:

The area of the rectangle is 1500 square pixels.

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

Проблема в этом коде очевидна в сигнатуре area:

fn area(width: u32, height: u32) -> u32 {

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

Рефакторинг с использованием кортежей

Listing 5-9 показывает другую версию нашей программы, которая использует кортежи.

Имя файла: src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
      1 area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
  2 dimensions.0 * dimensions.1
}

Listing 5-9: Указание ширины и высоты прямоугольника с использованием кортежа

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

Перемешивание ширины и высоты не имеет значения для вычисления площади, но если мы хотим нарисовать прямоугольник на экране, это будет иметь значение! Мы должны牢记, что width - это индекс кортежа 0, а height - индекс кортежа 1. Для других разработчиков будет еще труднее понять и запомнить это, если они будут использовать наш код. Поскольку мы не передали смысл наших данных в коде, теперь更容易引入错误. (这里原文“easier”有误,根据语境应该是“harder”,翻译为“更容易引入错误”不太符合逻辑,应改为“更难避免错误”)

更难避免错误,因为我们没有在代码中传达数据的含义。

Рефакторинг с использованием структур: добавление дополнительной смысловой нагрузки

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

Имя файла: src/main.rs

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

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

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

4 fn area(rectangle: &Rectangle) -> u32 {
  5 rectangle.width * rectangle.height
}

Listing 5-10: Определение структуры Rectangle

Здесь мы определили структуру и назвали ее Rectangle [1]. Внутри фигурных скобок мы определили поля width и height, оба из которых имеют тип u32 [2]. Затем, в main, мы создали конкретный экземпляр Rectangle, у которого ширина равна 30, а высота равна 50 [3].

Наша функция area теперь определена с одним параметром, который мы назвали rectangle, тип которого представляет собой неизменяемую ссылку на экземпляр структуры Rectangle [4]. Как упоминалось в главе 4, мы хотим взять ссылку на структуру, а не владеть ею. Таким образом, main сохраняет свою собственность и может продолжать использовать rect1, что объясняет использование & в сигнатуре функции и при вызове функции.

Функция area обращается к полям width и height экземпляра Rectangle [5] (заметьте, что доступ к полям экземпляра структуры, взятой по ссылке, не перемещает значения полей, что объясняет, почему часто используются ссылки на структуры). Наша сигнатура функции area теперь точно отражает то, что мы имеем в виду: вычислить площадь Rectangle, используя ее поля width и height. Это показывает, что ширина и высота взаимосвязаны, и дает описательные имена значениям вместо использования индексов кортежа 0 и 1. Это является выигрышем в вопросе ясности.

Добавление полезной функциональности с помощью производных трейтов

Было бы полезно иметь возможность выводить экземпляр Rectangle при отладке программы и видеть значения всех его полей. Listing 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: Попытка вывести экземпляр Rectangle

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

error[E0277]: `Rectangle` не реализует `std::fmt::Display`

Макрос println! может выполнять много видов форматирования, и по умолчанию фигурные скобки сообщают println! использовать форматирование, называемое Display: вывод, предназначенный для непосредственного потребления конечным пользователем. Примитивные типы, которые мы видели до сих пор, по умолчанию реализуют Display, потому что существует только один способ, которым вы хотите показать 1 или любой другой примитивный тип пользователю. Но при работе со структурами менее ясно, как println! должен форматировать вывод, потому что есть больше возможностей отображения: нужны ли запятые? Хотите ли вы печатать фигурные скобки? Следует ли показывать все поля? В связи с этой неоднозначностью Rust не пытается угадать, что мы хотим, и структурам не предоставляется реализация Display, которую можно использовать с println! и占位符ом {}.

Если мы продолжим читать ошибки, мы увидим эту полезную заметку:

= help: трейт `std::fmt::Display` не реализован для `Rectangle`
= note: в строках форматирования вы можете использовать `{:?}` (или {:#?} для
красивого вывода) вместо этого

Попробуем! Теперь вызов макроса println! будет выглядеть так: println!("rect1 is {:?}", rect1);. Размещение спецификатора :? внутри фигурных скобок сообщает println!, что мы хотим использовать формат вывода, называемый Debug. Трейт Debug позволяет нам выводить нашу структуру таким образом, который полезен для разработчиков, чтобы мы могли увидеть ее значение при отладке кода.

Компилируем код с этой правкой. Упс! Мы по-прежнему получаем ошибку:

error[E0277]: `Rectangle` не реализует `Debug`

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

= help: трейт `Debug` не реализован для `Rectangle`
= note: добавьте `#[derive(Debug)]` или реализуйте `Debug` вручную

Rust есть функциональность для вывода отладочной информации, но мы должны явно включить ее, чтобы она стала доступной для нашей структуры. Для этого мы добавляем внешний атрибут #[derive(Debug)] сразу перед определением структуры, как показано в Listing 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: Добавление атрибута для вывода трейта Debug и вывод экземпляра Rectangle с использованием отладочного форматирования

Теперь, когда мы запускаем программу, мы не получим никаких ошибок, и мы увидим следующий вывод:

rect1 is Rectangle { width: 30, height: 50 }

Отлично! Это не самый красивый вывод, но он показывает значения всех полей для этого экземпляра, что определенно поможет при отладке. Когда у нас есть большие структуры, полезно иметь вывод, который легче читать; в таких случаях мы можем использовать {:#?} вместо {:?} в строке println!. В этом примере использование стиля {:#?} выведет следующее:

rect1 is Rectangle {
    width: 30,
    height: 50,
}

Другой способ вывести значение с использованием формата Debug - это использовать макрос dbg!, который получает владение выражением (в отличие от println!, который получает ссылку), выводит имя файла и номер строки, где происходит вызов макроса dbg! в вашем коде, вместе с результирующим значением этого выражения и возвращает владение значением.

Примечание: вызов макроса dbg! выводит в стандартный поток ошибок консоли (stderr), в отличие от println!, который выводит в стандартный поток вывода консоли (stdout). Мы поговорим больше о stderr и stdout в разделе "Запись сообщений об ошибках в стандартный поток ошибок вместо стандартного потока вывода".

Вот пример, в котором мы интересуемся значением, которое присваивается полю width, а также значением всей структуры в rect1:

Имя файла: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
      1 width: dbg!(30 * scale),
        height: 50,
    };

  2 dbg!(&rect1);
}

Мы можем поместить dbg! вокруг выражения 30 * scale [1], и, так как dbg! возвращает владение значением выражения, поле width получит то же значение, что и если бы мы не использовали вызов dbg! там. Мы не хотим, чтобы dbg! получил владение rect1, поэтому в следующем вызове мы используем ссылку на rect1 [2]. Вот, как выглядит вывод этого примера:

[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Мы видим, что первый кусок вывода появился из [1], где мы отлаживаем выражение 30 * scale, и его результирующее значение равно 60 (реализация форматирования Debug для целых чисел - это вывод только их значения). Вызов dbg! в [2] выводит значение &rect1, которое представляет собой структуру Rectangle. Этот вывод использует красивый формат Debug для типа Rectangle. Макрос dbg! может быть действительно полезен, когда вы пытаетесь понять, что делает ваш код!

Кроме трейта Debug, Rust предоставил ряд трейтов, которые мы можем использовать с атрибутом derive, которые могут добавить полезное поведение к нашим пользовательским типам. Эти трейты и их поведение перечислены в Приложении C. Мы рассмотрим, как реализовать эти трейты с пользовательским поведением, а также как создавать свои собственные трейты в главе 10. Есть также многие атрибуты, кроме derive; для получения дополнительной информации см. раздел "Атрибуты" в Справочнике по Rust по адресу https://doc.rust-lang.org/reference/attributes.html.

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

Резюме

Поздравляем! Вы завершили лабораторную работу An Example Program Using Structs. Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.