Rust Курс

Структуры, блоки impl и трейты в Rust

Самоучитель Python 3, собранный из материалов данного сайта.Предназначен в основном для тех, кто хочет изучить язык программирования Python с нуля.

Урок 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). Это поможет нам обрабатывать разные варианты данных ещё более элегантно.

А пока — практикуйся! Чем больше ты пишешь кода, тем лучше понимаешь, как работают эти концепции. Удачи! 🚀