Урок 6: Структуры и трейты в Rust — Собираем данные и учим их работать вместе
Привет, юный программист! 👋 Сегодня мы познакомимся с двумя очень важными инструментами в Rust: структурами и трейтами. Представь, что ты строишь робота. Чтобы робот работал, тебе нужны детали (данные), из которых он состоит, и инструкции (поведение), как этими деталями управлять. Структуры — это как чертёж деталей робота, а трейты — это инструкции, которые говорят, что робот может делать.
В этом уроке мы научимся:
- Создавать свои собственные типы данных с помощью структур
- Добавлять методам к структурам, чтобы они умели делать разные вещи
- Использовать трейты, чтобы заставить разные структуры работать одинаково
- Писать код, который легко расширять и не бояться ошибок
💡 Аналогия из жизни: Представь, что ты собираешь конструктор LEGO. Структура — это схема, как собрать конкретную модель (например, космический корабль). Трейт — это инструкция, как заставить модель двигаться, стрелять или светиться. Разные модели могут иметь общие трейты!
1. Знакомство со структурами (Structs)
Структура — это способ объединить несколько переменных разных типов в одну логическую единицу. Например, если мы хотим описать книгу, нам нужно знать её название, автора, год издания и количество страниц. Без структуры нам пришлось бы хранить всё это в отдельных переменных, что было бы неудобно. С помощью структуры мы создаём новый тип данных — Book.
1.1. Объявление структуры
Давай создадим простую структуру для описания книги:
// Объявляем структуру с именем Book
struct Book {
// Поля структуры — это данные, которые она хранит
title: String, // Название книги
author: String, // Автор
year: u16, // Год издания (u16 — целое число до 65535)
pages: u32, // Количество страниц (u32 — целое число до 4 миллиардов)
}
Обрати внимание:
- Имя структуры пишется с большой буквы (Book, Car, Student). Это правило Rust.
- Поля — это переменные внутри структуры. У каждого поля есть имя и тип.
- Структура — это как форма для печенья. Сама по себе она не хранит данные, а только описывает, какие данные будут.
1.2. Создание экземпляра структуры
Чтобы использовать структуру, нужно создать её экземпляр — объект с конкретными значениями полей. Это как испечь печенье по форме.
fn main() {
// Создаём экземпляр структуры Book
let my_book = Book {
title: String::from("Приключения Rust"),
author: String::from("Иван Программист"),
year: 2024,
pages: 350,
};
// Выводим информацию о книге
println!("Книга: {}", my_book.title);
println!("Автор: {}", my_book.author);
println!("Год: {}", my_book.year);
println!("Страниц: {}", my_book.pages);
}
Обрати внимание на синтаксис: мы пишем Book { поле1: значение1, поле2: значение2, ... }. Порядок полей не важен, но имена должны точно совпадать с теми, что в объявлении структуры.
1.3. Изменение полей структуры
Если мы хотим изменять поля после создания, нужно объявить экземпляр как изменяемый (mut):
fn main() {
let mut my_book = Book {
title: String::from("Rust для начинающих"),
author: String::from("Мария Кодер"),
year: 2023,
pages: 200,
};
println!("Страниц: {}", my_book.pages); // 200
// Меняем количество страниц
my_book.pages = 250;
println!("Теперь страниц: {}", my_book.pages); // 250
}
1.4. Функции для создания структур
Часто бывает удобно написать функцию, которая создаёт структуру с заданными параметрами. Это упрощает код.
// Функция, которая создаёт новую книгу
fn create_book(title: String, author: String, year: u16, pages: u32) -> Book {
Book {
title, // Сокращённая запись: title: title
author, // Если имя поля совпадает с именем переменной, можно писать просто название
year,
pages,
}
}
fn main() {
let book1 = create_book(
String::from("Учим Rust играючи"),
String::from("Алексей Умников"),
2025,
180,
);
println!("Название: {}", book1.title);
}
Обрати внимание на сокращённую запись: если имя поля и имя переменной совпадают, можно писать просто title вместо title: title.
2. Методы структур
Структуры могут иметь методы — функции, которые привязаны к конкретному экземпляру. Методы позволяют выполнять действия над данными структуры. Например, книга может "рассказать" о себе.
2.1. Добавление методов с помощью impl
Методы объявляются внутри блока impl (сокращение от implementation — реализация).
struct Book {
title: String,
author: String,
year: u16,
pages: u32,
}
// Блок реализации для структуры Book
impl Book {
// Метод, который выводит информацию о книге
// &self — ссылка на экземпляр структуры (self — это сам объект)
fn describe(&self) {
println!("Книга: {}", self.title);
println!("Автор: {}", self.author);
println!("Год издания: {}", self.year);
println!("Страниц: {}", self.pages);
}
// Метод, который проверяет, толстая ли книга (больше 300 страниц)
fn is_thick(&self) -> bool {
self.pages > 300
}
// Метод, который возвращает возраст книги (текущий год - год издания)
fn age(&self, current_year: u16) -> u16 {
current_year - self.year
}
}
fn main() {
let book = Book {
title: String::from("Rust в примерах"),
author: String::from("Дмитрий Книжный"),
year: 2020,
pages: 450,
};
// Вызываем метод describe
book.describe();
// Проверяем, толстая ли книга
if book.is_thick() {
println!("Это толстая книга!");
} else {
println!("Книга тонкая.");
}
// Узнаём возраст книги
println!("Книге {} лет", book.age(2025));
}
Обрати внимание:
&self— это ссылка на сам объект. Это как сказать "я". Метод может читать данные объекта, но не изменять их.- Если нужно изменять объект, используется
&mut self. - Методы вызываются через точку:
book.describe().
2.2. Методы, которые изменяют объект
Иногда нужно изменить данные внутри структуры. Для этого используется &mut self.
impl Book {
// Метод, который добавляет страницы
fn add_pages(&mut self, extra_pages: u32) {
self.pages += extra_pages;
println!("Добавлено {} страниц. Теперь всего: {}", extra_pages, self.pages);
}
// Метод, который меняет автора
fn change_author(&mut self, new_author: String) {
self.author = new_author;
println!("Автор изменён.");
}
}
fn main() {
let mut book = Book {
title: String::from("Моя книга"),
author: String::from("Я"),
year: 2024,
pages: 100,
};
book.add_pages(50); // Теперь страниц 150
book.change_author(String::from("Мы")); // Автор изменён
}
3. Трейты (Traits) — общее поведение для разных структур
Представь, что у тебя есть разные типы объектов: книга, фильм, игра. Все они могут иметь название и год выпуска. Но как заставить их работать одинаково? В Rust для этого есть трейты (traits).
Трейт — это набор методов, которые должны быть реализованы у структуры. Если структура реализует трейт, она обещает, что у неё есть эти методы.
💡 Аналогия: Трейт — это контракт. Если ты говоришь "я реализую трейт Printable", то ты обещаешь, что у тебя есть метод print(). Другие функции могут вызывать этот метод, не зная, какой именно тип данных используется.
3.1. Объявление трейта
Давай создадим трейт Describable, который требует метод describe:
// Объявляем трейт Describable
trait Describable {
// Метод, который должен быть реализован
fn describe(&self);
}
Трейт может содержать несколько методов. Некоторые могут иметь реализацию по умолчанию.
3.2. Реализация трейта для структуры
Теперь заставим нашу структуру Book реализовать трейт Describable:
struct Book {
title: String,
author: String,
year: u16,
}
// Реализуем трейт Describable для Book
impl Describable for Book {
fn describe(&self) {
println!("Книга: {} от автора {}, {} год", self.title, self.author, self.year);
}
}
fn main() {
let book = Book {
title: String::from("Rust для детей"),
author: String::from("Петя"),
year: 2024,
};
// Вызываем метод из трейта
book.describe();
}
3.3. Реализация трейта для нескольких типов
Самое крутое — один трейт можно реализовать для разных структур. Например, у нас есть Movie (фильм) и Game (игра):
struct Movie {
title: String,
director: String,
year: u16,
duration: u16, // в минутах
}
struct Game {
title: String,
genre: String,
year: u16,
platform: String,
}
// Реализуем Describable для Movie
impl Describable for Movie {
fn describe(&self) {
println!("Фильм: {}, режиссёр: {}, {} год, длительность: {} мин",
self.title, self.director, self.year, self.duration);
}
}
// Реализуем Describable для Game
impl Describable for Game {
fn describe(&self) {
println!("Игра: {}, жанр: {}, {} год, платформа: {}",
self.title, self.genre, self.year, self.platform);
}
}
fn main() {
let movie = Movie {
title: String::from("Матрица"),
director: String::from("Вачовски"),
year: 1999,
duration: 136,
};
let game = Game {
title: String::from("Minecraft"),
genre: String::from("Песочница"),
year: 2011,
platform: String::from("Все"),
};
// Оба типа имеют метод describe(), хотя реализация разная!
movie.describe();
game.describe();
}
3.4. Трейты с методами по умолчанию
Можно задать метод по умолчанию в самом трейте. Если структура не переопределит его, будет использоваться версия по умолчанию.
trait Greeter {
// Метод по умолчанию
fn greet(&self) {
println!("Привет! Я что-то, но не знаю, что именно.");
}
// Метод, который нужно обязательно реализовать
fn name(&self) -> &str;
}
struct Robot {
model: String,
}
struct Human {
first_name: String,
last_name: String,
}
impl Greeter for Robot {
// Переопределяем greet
fn greet(&self) {
println!("Бип-буп! Я робот модели {}", self.model);
}
fn name(&self) -> &str {
&self.model
}
}
impl Greeter for Human {
// Используем greet по умолчанию!
fn name(&self) -> &str {
&self.first_name
}
}
fn main() {
let robot = Robot { model: String::from("R2-D2") };
let human = Human {
first_name: String::from("Анна"),
last_name: String::from("Иванова"),
};
robot.greet(); // Выведет кастомное приветствие
human.greet(); // Выведет приветствие по умолчанию
println!("Меня зовут {}", human.name());
}
4. Трейты как параметры функций
Одна из самых мощных возможностей трейтов — использовать их в качестве параметров функций. Функция может принимать любой тип, который реализует определённый трейт.
4.1. Синтаксис impl Trait
// Функция, которая принимает что угодно, реализующее Describable
fn print_description(item: &impl Describable) {
item.describe();
}
fn main() {
let book = Book { /* ... */ };
let movie = Movie { /* ... */ };
// Оба вызова работают!
print_description(&book);
print_description(&movie);
}
4.2. Синтаксис с дженериками (T: Trait)
Более продвинутый способ — использовать дженерики (обобщённые типы):
// T — это обобщённый тип, который должен реализовывать Describable
fn print_description_generic(item: &T) {
item.describe();
}
4.3. Несколько трейтов
Функция может требовать, чтобы тип реализовывал сразу несколько трейтов:
trait Printable {
fn print(&self);
}
trait Serializable {
fn to_json(&self) -> String;
}
// Функция требует оба трейта
fn save_and_print(item: &(impl Printable + Serializable)) {
item.print();
let json = item.to_json();
println!("JSON: {}", json);
}
5. Практические примеры
5.1. Создаём структуру "Студент"
struct Student {
name: String,
age: u8,
grade: f32, // Средний балл
subjects: Vec, // Список предметов
}
impl Student {
fn new(name: String, age: u8) -> Student {
Student {
name,
age,
grade: 0.0,
subjects: Vec::new(),
}
}
fn add_subject(&mut self, subject: String) {
self.subjects.push(subject);
}
fn set_grade(&mut self, grade: f32) {
if grade >= 0.0 && grade <= 5.0 {
self.grade = grade;
} else {
println!("Оценка должна быть от 0 до 5!");
}
}
fn info(&self) {
println!("Студент: {}, возраст: {}, средний балл: {}", self.name, self.age, self.grade);
println!("Предметы: {:?}", self.subjects);
}
}
fn main() {
let mut student = Student::new(String::from("Маша"), 14);
student.add_subject(String::from("Математика"));
student.add_subject(String::from("Русский язык"));
student.set_grade(4.5);
student.info();
}
5.2. Трейт "Comparable" для сравнения
trait Comparable {
fn is_better_than(&self, other: &Self) -> bool;
}
struct Score {
points: u32,
player_name: String,
}
impl Comparable for Score {
fn is_better_than(&self, other: &Self) -> bool {
self.points > other.points
}
}
fn compare_scores(a: &Score, b: &Score) {
if a.is_better_than(b) {
println!("{} лучше, чем {}", a.player_name, b.player_name);
} else {
println!("{} лучше, чем {}", b.player_name, a.player_name);
}
}
fn main() {
let score1 = Score { points: 1500, player_name: String::from("Алиса") };
let score2 = Score { points: 1200, player_name: String::from("Боб") };
compare_scores(&score1, &score2);
}
6. Частые ошибки и советы
⚠️ Ошибка: Забыть указать&selfв методе. Если метод не использует данные структуры, он всё равно должен принимать&self(или не быть методом вовсе, а быть ассоциированной функцией).
impl Book {
// Правильно: метод использует self
fn title_length(&self) -> usize {
self.title.len()
}
// Ассоциированная функция (не метод) — не использует self
fn default() -> Book {
Book {
title: String::from("Без названия"),
author: String::from("Неизвестен"),
year: 2000,
pages: 0,
}
}
}
// Вызов ассоциированной функции
let default_book = Book::default();
⚠️ Ошибка: Пытаться изменить поле структуры, когда она не объявлена как mut. Rust очень строг в этом отношении.
7. Практические задания
Теперь твоя очередь! Попробуй выполнить эти задания. Не бойся ошибаться — ошибки помогают учиться.
Задание 1: Структура "Автомобиль"
Создай структуру Car с полями:
brand(String) — маркаmodel(String) — модельyear(u16) — год выпускаmileage(u32) — пробег в км
Реализуй методы:
new(brand, model, year)— создаёт машину с нулевым пробегомdrive(&mut self, km: u32)— увеличивает пробегinfo(&self)— выводит информацию о машине
Задание 2: Трейт "Area" для фигур
Создай трейт Area с методом area(&self) -> f64. Реализуй его для структур:
Circleс полемradius: f64(площадь = π * r²)Rectangleс полямиwidth: f64иheight: f64(площадь = w * h)
Напиши функцию, которая принимает &impl Area и выводит площадь.
Задание 3: Система оценок
Создай структуру GradeBook для хранения оценок студентов:
- Поле
grades: Vec— список оценок (от 1 до 5) - Метод
add_grade(&mut self, grade: u8)— добавляет оценку (проверь, что она в диапазоне 1-5) - Метод
average(&self) -> f32— возвращает средний балл - Метод
best(&self) -> u8— возвращает максимальную оценку
Создай трейт Summary с методом summary(&self) -> String, который возвращает строку с итогами. Реализуй его для GradeBook.
💡 Совет: Начни с первого задания. Если что-то не получается, перечитай раздел про структуры. Помни, что все поля должны быть явно инициализированы при создании экземпляра.
Заключение
Поздравляю! 🎉 Ты изучил одни из самых важных концепций в Rust: структуры и трейты. Теперь ты можешь:
- Создавать свои типы данных, объединяя разные переменные
- Добавлять поведение к типам через методы
- Определять общее поведение для разных типов с помощью трейтов
- Писать функции, которые работают с любым типом, реализующим нужный трейт
В следующем уроке мы узнаем о перечислениях (enums) и сопоставлении с образцом (pattern matching). Это поможет нам обрабатывать разные варианты данных ещё более элегантно.
А пока — практикуйся! Чем больше ты пишешь кода, тем лучше понимаешь, как работают эти концепции. Удачи! 🚀