🦀 Rust

POO : Encapsulation et polymorphisme en Rust : guide détaillé

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — шеф-повар в ресторане. У тебя есть холодильник, плита, ножи и продукты. Ты знаешь, как приготовить борщ, пиццу и десерт. Но представь, что к тебе на кухню заходит посетитель и начинает сам лезть в холодильник, резать овощи твоим ножом и включать плиту. Что будет? Хаос! Продукты испортятся, нож затупится, а посетитель может обжечься.

В программировании происходит то же самое. Когда программа маленькая, один программист может держать всё в голове. Но когда над проектом работают 10, 100 или 1000 человек, нужно чётко договориться: «Вот это — моя кухня, сюда не лезь. Вот это — твоя зона, я туда не суюсь». Для этого и нужны инкапсуляция и полиморфизм.

Инкапсуляция — это как закрытая дверь на кухню. Ты (программист) прячешь внутренние детали работы объекта (например, как именно хранятся данные) и показываешь только то, что нужно: кнопки управления. Посетитель (другой программист) не знает, как устроен холодильник внутри, но знает, что если нажать кнопку «Заказать борщ», ему принесут борщ.

Полиморфизм — это как универсальная розетка. В неё можно воткнуть и чайник, и тостер, и фен. Они все разные, но работают от одного и того же разъёма. В программировании это значит: разные объекты могут выполнять одно и то же действие (например, «нарисовать себя»), но каждый делает это по-своему. Круг рисуется как круг, квадрат — как квадрат, а треугольник — как треугольник. Но команда одна: «Нарисуй!».

💡 Запомни главное: Инкапсуляция — это про безопасность и простоту использования. Полиморфизм — это про гибкость и расширяемость. Вместе они делают код понятным, надёжным и удобным для изменений.

Теория: как это работает изнутри

Инкапсуляция: принцип «чёрного ящика»

Представь, что каждый объект в программе — это чёрный ящик. Снаружи у него есть несколько кнопок и экран. Внутри — сложная электроника. Ты нажимаешь кнопку «Вкл» — ящик работает. Тебе не нужно знать, какие там микросхемы и провода. Это и есть инкапсуляция: скрытие внутренней реализации и предоставление публичного интерфейса.

В Rust инкапсуляция достигается через:

  • Модификаторы доступа (ключевое слово pub) — делает поле или метод доступным снаружи модуля.
  • Структуры (struct) — группируют данные.
  • Методы (impl) — функции, привязанные к структуре.

Без pub всё внутри модуля считается приватным. Ты можешь менять внутреннее устройство структуры, и код, который её использует, не сломается — если ты не меняешь публичные методы.

Полиморфизм: принцип «один интерфейс — много реализаций»

Полиморфизм бывает разным. В Rust есть два основных вида:

  1. Полиморфизм через трейты (traits) — это как контракт. Трейт описывает, какие методы должен иметь тип. Любой тип, который реализует трейт, может использоваться там, где этот трейт требуется.
  2. Полиморфизм через перегрузку (generics) — это когда одна и та же функция работает с разными типами данных, не зная заранее, с какими именно.

В отличие от классического ООП (как в Java или C++), в Rust нет наследования классов. Вместо этого используется композиция и трейты. Это делает код более гибким и безопасным.

⚠️ Важно: В Rust нет ключевого слова class. Вместо классов — структуры (struct) и перечисления (enum). Вместо наследования — трейты. Это не хуже и не лучше — это просто другой подход, который даёт больше контроля над памятью и производительностью.

Синтаксис в Rust: все формы и варианты

1. Базовая структура с инкапсуляцией

// Определяем структуру "Счёт в банке"
struct BankAccount {
    // Приватное поле — никто снаружи не может его прочитать или изменить
    balance: f64,
    // Приватное поле — владелец счёта
    owner: String,
}

// Реализация методов для структуры
impl BankAccount {
    // Конструктор — создаёт новый счёт
    // pub значит, что этот метод доступен снаружи
    pub fn new(owner: &str, initial_balance: f64) -> BankAccount {
        BankAccount {
            balance: initial_balance,
            owner: String::from(owner),
        }
    }

    // Публичный метод — показывает баланс (но не даёт его изменить!)
    pub fn get_balance(&self) -> f64 {
        self.balance
    }

    // Публичный метод — положить деньги на счёт
    pub fn deposit(&mut self, amount: f64) {
        if amount > 0.0 {
            self.balance += amount;
            println!("✅ Внесено {} на счёт {}. Баланс: {}", amount, self.owner, self.balance);
        } else {
            println!("❌ Сумма должна быть положительной!");
        }
    }

    // Публичный метод — снять деньги
    pub fn withdraw(&mut self, amount: f64) -> bool {
        if amount > 0.0 && amount <= self.balance {
            self.balance -= amount;
            println!("✅ Снято {} со счёта {}. Баланс: {}", amount, self.owner, self.balance);
            true
        } else {
            println!("❌ Недостаточно средств или неверная сумма!");
            false
        }
    }

    // Приватный метод — доступен только внутри структуры
    fn check_balance(&self) {
        println!("🔒 Внутренняя проверка: баланс = {}", self.balance);
    }
}

fn main() {
    // Создаём счёт
    let mut account = BankAccount::new("Анна", 1000.0);
    
    // Работаем через публичные методы
    account.deposit(500.0);
    account.withdraw(200.0);
    
    // Узнаём баланс
    println!("Текущий баланс: {}", account.get_balance());
    
    // А вот так сделать НЕЛЬЗЯ — поле balance приватное:
    // println!("{}", account.balance); // ОШИБКА!
}

2. Трейты — основа полиморфизма

// Определяем трейт (контракт) "Фигура"
trait Shape {
    // Метод, который должна реализовать каждая фигура
    fn area(&self) -> f64;
    
    // Метод с реализацией по умолчанию
    fn description(&self) -> String {
        String::from("Это геометрическая фигура")
    }
}

// Структура "Круг"
struct Circle {
    radius: f64,
}

// Реализуем трейт Shape для круга
impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
    
    // Переопределяем метод по умолчанию
    fn description(&self) -> String {
        format!("Круг с радиусом {}", self.radius)
    }
}

// Структура "Прямоугольник"
struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
    
    fn description(&self) -> String {
        format!("Прямоугольник {}x{}", self.width, self.height)
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let rect = Rectangle { width: 4.0, height: 6.0 };
    
    // Один и тот же метод area() работает по-разному!
    println!("{}", circle.description());
    println!("Площадь круга: {:.2}", circle.area());
    
    println!("{}", rect.description());
    println!("Площадь прямоугольника: {:.2}", rect.area());
}

3. Полиморфизм через трейт-объекты

trait Animal {
    fn make_sound(&self);
}

struct Dog {
    name: String,
}

impl Animal for Dog {
    fn make_sound(&self) {
        println!("{} говорит: Гав-гав!", self.name);
    }
}

struct Cat {
    name: String,
}

impl Animal for Cat {
    fn make_sound(&self) {
        println!("{} говорит: Мяу!", self.name);
    }
}

fn main() {
    // Создаём вектор из разных животных
    // Box — это трейт-объект, который может хранить любой тип, реализующий Animal
    let animals: Vec> = vec![
        Box::new(Dog { name: String::from("Бобик") }),
        Box::new(Cat { name: String::from("Мурка") }),
        Box::new(Dog { name: String::from("Шарик") }),
    ];
    
    // Проходим по всем животным и вызываем один и тот же метод
    for animal in animals {
        animal.make_sound(); // Полиморфизм в действии!
    }
}

Примеры кода — от простого к сложному

Пример 1: Простейшая инкапсуляция — термостат

struct Thermostat {
    // Приватное поле — текущая температура
    current_temp: f64,
    // Приватное поле — целевая температура
    target_temp: f64,
}

impl Thermostat {
    // Конструктор
    pub fn new(target: f64) -> Thermostat {
        Thermostat {
            current_temp: 20.0, // Начальная температура
            target_temp: target,
        }
    }
    
    // Публичный метод — получить текущую температуру
    pub fn get_current_temp(&self) -> f64 {
        self.current_temp
    }
    
    // Публичный метод — установить целевую температуру
    pub fn set_target(&mut self, target: f64) {
        if target > 0.0 && target < 50.0 {
            self.target_temp = target;
            println!("🎯 Целевая температура установлена на {}", target);
        } else {
            println!("❌ Недопустимая температура!");
        }
    }
    
    // Публичный метод — включить обогрев на 1 градус
    pub fn heat_up(&mut self) {
        if self.current_temp < self.target_temp {
            self.current_temp += 1.0;
            println!("🔥 Температура повышена до {}", self.current_temp);
        }
    }
}

fn main() {
    let mut thermostat = Thermostat::new(25.0);
    println!("Текущая: {}°C", thermostat.get_current_temp());
    thermostat.heat_up();
    thermostat.heat_up();
    thermostat.set_target(30.0);
}

Пример 2: Инкапсуляция с проверкой данных

struct User {
    username: String,
    // Приватное поле — пароль хранится в зашифрованном виде
    password_hash: String,
    // Приватное поле — возраст
    age: u8,
}

impl User {
    pub fn new(username: &str, password: &str, age: u8) -> Result {
        // Проверяем данные перед созданием
        if username.len() < 3 {
            return Err("Имя пользователя должно быть не менее 3 символов".to_string());
        }
        if password.len() < 6 {
            return Err("Пароль должен быть не менее 6 символов".to_string());
        }
        if age < 13 {
            return Err("Возраст должен быть не менее 13 лет".to_string());
        }
        
        Ok(User {
            username: String::from(username),
            password_hash: simple_hash(password), // Хешируем пароль
            age: age,
        })
    }
    
    pub fn check_password(&self, password: &str) -> bool {
        self.password_hash == simple_hash(password)
    }
    
    pub fn get_username(&self) -> &str {
        &self.username
    }
}

// Простая функция хеширования (в реальности используют bcrypt и т.д.)
fn simple_hash(s: &str) -> String {
    format!("hash_{}", s.len())
}

fn main() {
    match User::new("Анна", "секретный_пароль", 25) {
        Ok(user) => {
            println!("✅ Пользователь {} создан!", user.get_username());
            println!("Пароль верный: {}", user.check_password("секретный_пароль"));
            println!("Пароль неверный: {}", user.check_password("неправильный"));
        }
        Err(error) => {
            println!("❌ Ошибка: {}", error);
        }
    }
}

Пример 3: Полиморфизм с трейтами — платёжная система

trait PaymentMethod {
    fn pay(&self, amount: f64) -> bool;
    fn get_type(&self) -> &str;
}

struct CreditCard {
    number: String,
    limit: f64,
}

impl PaymentMethod for CreditCard {
    fn pay(&self, amount: f64) -> bool {
        if amount <= self.limit {
            println!("💳 Оплата {} через карту {} выполнена", amount, self.number);
            true
        } else {
            println!("❌ Превышен лимит карты!");
            false
        }
    }
    
    fn get_type(&self) -> &str {
        "Кредитная карта"
    }
}

struct PayPal {
    email: String,
    balance: f64,
}

impl PaymentMethod for PayPal {
    fn pay(&self, amount: f64) -> bool {
        if amount <= self.balance {
            println!("🅿️ Оплата {} через PayPal ({}) выполнена", amount, self.email);
            true
        } else {
            println!("❌ Недостаточно средств на PayPal!");
            false
        }
    }
    
    fn get_type(&self) -> &str {
        "PayPal"
    }
}

struct Cash;

impl PaymentMethod for Cash {
    fn pay(&self, amount: f64) -> bool {
        println!("💵 Оплата {} наличными принята", amount);
        true
    }
    
    fn get_type(&self) -> &str {
        "Наличные"
    }
}

fn process_payment(method: &dyn PaymentMethod, amount: f64) {
    println!("Обработка платежа через {}", method.get_type());
    method.pay(amount);
}

fn main() {
    let card = CreditCard {
        number: String::from("1234-5678-9012-3456"),
        limit: 5000.0,
    };
    
    let paypal = PayPal {
        email: String::from("user@example.com"),
        balance: 1000.0,
    };
    
    let cash = Cash;
    
    // Одна функция process_payment работает с любым способом оплаты!
    process_payment(&card, 3000.0);
    process_payment(&paypal, 500.0);
    process_payment(&cash, 150.0);
}

Пример 4: Композиция вместо наследования

// В Rust нет наследования, но есть композиция — включение одной структуры в другую

struct Engine {
    horsepower: u32,
    running: bool,
}

impl Engine {
    fn new(horsepower: u32) -> Engine {
        Engine { horsepower, running: false }
    }
    
    fn start(&mut self) {
        self.running = true;
        println!("🔧 Двигатель {} л.с. запущен", self.horsepower);
    }
    
    fn stop(&mut self) {
        self.running = false;
        println!("🔧 Двигатель остановлен");
    }
}

struct Car {
    // Композиция: машина содержит двигатель
    engine: Engine,
    brand: String,
    model: String,
}

impl Car {
    fn new(brand: &str, model: &str, horsepower: u32) -> Car {
        Car {
            engine: Engine::new(horsepower),
            brand: String::from(brand),
            model: String::from(model),
        }
    }
    
    fn start(&mut self) {
        println!("🚗 {} {} заводится...", self.brand, self.model);
        self.engine.start(); // Делегируем работу двигателю
    }
    
    fn stop(&mut self) {
        self.engine.stop();
        println!("🚗 {} {} заглушен", self.brand, self.model);
    }
}

fn main() {
    let mut my_car = Car::new("Toyota", "Camry", 200);
    my_car.start();
    my_car.stop();
}

Пример 5: Дженерики — полиморфизм на этапе компиляции

// Функция, которая работает с любым типом, реализующим трейт PartialOrd (сравнение)
fn find_max(list: &[T]) -> &T {
    let mut max_index = 0;
    
    for i in 1..list.len() {
        if list[i] > list[max_index] {
            max_index = i;
        }
    }
    
    &list[max_index]
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("Максимальное число: {}", find_max(&numbers));
    
    let chars = vec!['y', 'm', 'a', 'q'];
    println!("Максимальный символ: {}", find_max(&chars));
    
    let floats = vec![3.14, 2.71, 1.41, 1.73];
    println!("Максимальное число с плавающей точкой: {:.2}", find_max(&floats));
}

Пример 6: Реализация итератора — продвинутый полиморфизм

struct Fibonacci {
    current: u64,
    next: u64,
    max: u64,
}

impl Fibonacci {
    fn new(max: u64) -> Fibonacci {
        Fibonacci { current: 0, next: 1, max }
    }
}

// Реализуем трейт Iterator для нашей структуры
impl Iterator for Fibonacci {
    type Item = u64;
    
    fn next(&mut self) -> Option {
        if self.current > self.max {
            return None;
        }
        
        let result = self.current;
        self.current = self.next;
        self.next = result + self.next;
        
        Some(result)
    }
}

fn main() {
    println!("Числа Фибоначчи до 100:");
    
    // Используем наш итератор в цикле for
    for num in Fibonacci::new(100) {
        print!("{} ", num);
    }
    println!();
    
    // Можно использовать методы итераторов
    let fib_sum: u64 = Fibonacci::new(50).sum();
    println!("Сумма чисел Фибоначчи до 50: {}", fib_sum);
}

Пример 7: Паттерн "Строитель" (Builder) с инкапсуляцией

struct Pizza {
    size: String,
    cheese: bool,
    pepperoni: bool,
    mushrooms: bool,
    olives: bool,
}

// Строитель — отдельная структура для пошагового создания Pizza
struct PizzaBuilder {
    size: String,
    cheese: bool,
    pepperoni: bool,
    mushrooms: bool,
    olives: bool,
}

impl PizzaBuilder {
    fn new(size: &str) -> PizzaBuilder {
        PizzaBuilder {
            size: String::from(size),
            cheese: false,
            pepperoni: false,
            mushrooms: false,
            olives: false,
        }
    }
    
    // Каждый метод возвращает self, чтобы можно было цепочку вызовов
    fn add_cheese(mut self) -> PizzaBuilder {
        self.cheese = true;
        self
    }
    
    fn add_pepperoni(mut self) -> PizzaBuilder {
        self.pepperoni = true;
        self
    }
    
    fn add_mushrooms(mut self) -> PizzaBuilder {
        self.mushrooms = true;
        self
    }
    
    fn add_olives(mut self) -> PizzaBuilder {
        self.olives = true;
        self
    }
    
    // Финальный метод — создаёт Pizza
    fn build(self) -> Pizza {
        Pizza {
            size: self.size,
            cheese: self.cheese,
            pepperoni: self.pepperoni,
            mushrooms: self.mushrooms,
            olives: self.olives,
        }
    }
}

fn main() {
    // Создаём пиццу пошагово
    let pizza = PizzaBuilder::new("Большая")
        .add_cheese()
        .add_pepperoni()
        .add_mushrooms()
        .build();
    
    println!("🍕 Пицца готова!");
    println!("Размер: {}", pizza.size);
    println!("Сыр: {}", if pizza.cheese { "да" } else { "нет" });
    println!("Пепперони: {}", if pizza.pepperoni { "да" } else { "нет" });
    println!("Грибы: {}", if pizza.mushrooms { "да" } else { "нет" });
    println!("Оливки: {}", if pizza.olives { "да" } else { "нет" });
}

Сравнение с Python: чем похоже и чем отличается

Характеристика Python Rust
Инкапсуляция Соглашение: _private (подчёркивание). На самом деле всё доступно. Строгая: pub — публичное, без pub — приватное. Наружу не вылезти.
Полиморфизм Утиная типизация: если объект крякает как утка, это утка. Ошибки — в рантайме. Трейты: явное указание, какие методы реализованы. Ошибки — на этапе компиляции.
Наследование Есть классическое наследование классов. Нет наследования. Вместо него — трейты и композиция.
Скорость Медленнее (интерпретируемый язык). Очень быстрый (компилируется в машинный код).
Безопасность памяти Сборщик мусора. Можно случайно создать утечку. Система владения (ownership). Утечки памяти почти невозможны.
Создание объекта obj = MyClass() let obj = MyStruct::new()
Полиморфизм в коллекциях Любые объекты в одном списке (но нужно проверять типы). Vec<Box<dyn Trait>> — явно указываем трейт.
💡 Совет: Если ты знаешь Python, Rust покажется тебе более строгим и требовательным. Но эта строгость окупается: программа работает быстрее, а ошибки обнаруживаются до запуска, а не во время работы.

Типичные ошибки и подводные камни

Ошибка 1: Забыть указать pub

struct Point {
    x: i32, // Приватное поле!
    y: i32,
}

fn main() {
    let p = Point { x: 10, y: 20 }; // Ошибка! Поля приватные
}

Решение: Либо сделай поля публичными (pub x: i32), либо создай конструктор.

Ошибка 2: Путаница между self, &self и &mut self

struct Counter {
    count: u32,
}

impl Counter {
    fn increment(self) { // Ошибка! self — это владение, после вызова объект исчезнет
        self.count += 1;
    }
}

Решение: Используй &mut self, если метод изменяет объект, и &self, если только читает.

Ошибка 3: Попытка изменить приватное поле через публичный метод, который принимает &self

impl Thermostat {
    pub fn set_target(&self, target: f64) { // Ошибка! &self — неизменяемая ссылка
        self.target_temp = target; // Нельзя изменить!
    }
}

Решение: Используй &mut self для методов, которые изменяют объект.

Ошибка 4: Неправильное использование трейт-объектов

fn process(animal: dyn Animal) { // Ошибка! Трейт-объекты должны быть за ссылкой
    animal.make_sound();
}

Решение: Используй &dyn Animal или Box<dyn Animal>.

Ошибка 5: Забыть реализовать все методы трейта

trait Printable {
    fn print(&self);
    fn format(&self) -> String;
}

struct Document;

impl Printable for Document {
    fn print(&self) {
        println!("Печать документа");
    }
    // Забыли реализовать format() — ошибка компиляции!
}

Решение: Компилятор сам скажет, какие методы не реализованы. Внимательно читай сообщения об ошибках!

Лучшие практики и советы профессионалов

  1. Начинай с приватных полей. Делай поля публичными только если есть веская причина. Это защитит данные от случайного изменения.
  2. Используй трейты для определения поведения. Трейты — это контракты. Они делают код гибким и тестируемым.
  3. Предпочитай композицию наследованию. В Rust нет наследования — и это хорошо. Композиция (включение одной структуры в другую) даёт больше гибкости.
  4. Не бойся дженериков. Они делают код универсальным без потери производительности.
  5. Используй паттерн "Строитель" для сложных объектов. Когда у структуры много полей, строитель делает код читаемым.
  6. Пиши тесты для публичных методов. Инкапсуляция позволяет тестировать поведение, не заботясь о внутреннем устройстве.
  7. Документируй публичный API. Используй /// для документации. Другие программисты будут благодарны.
  8. Избегай больших трейтов. Лучше несколько маленьких трейтов, чем один большой. Это принцип разделения интерфейсов.
🔥 Золотое правило: Хороший код — это код, который легко читать, легко изменять и легко тестировать. Инкапсуляция и полиморфизм — твои главные инструменты для этого.

Вопросы для самопроверки

  1. Вопрос: Что такое инкапсуляция простыми словами?

    Ответ: Это сокрытие внутренних деталей работы объекта и предоставление только необходимых методов для взаимодействия с ним. Как пульт от телевизора: ты нажимаешь кнопки, не зная, как устроен телевизор внутри.

  2. Вопрос: Как в Rust сделать поле структуры приватным?

    Ответ: Просто не указывай pub перед полем. По умолчанию все поля и методы приватные.

  3. Вопрос: Что такое трейт в Rust?

    Ответ: Трейт — это набор методов, которые тип должен реализовать. Это как контракт: «Если ты реализуешь этот трейт, ты гарантируешь, что у тебя есть эти методы».

  4. Вопрос: В чём разница между &dyn Trait и impl Trait?

    Ответ: &dyn Trait — это трейт-объект, который работает во время выполнения (динамическая диспетчеризация). impl Trait — это статическая диспетчеризация, тип известен на этапе компиляции. impl Trait быстрее, но &dyn Trait гибче.

  5. Вопрос: Можно ли в Rust наследовать одну структуру от другой?

    Ответ: Нет. В Rust нет наследования. Вместо этого используется композиция (включение одной структуры в другую) и трейты.

  6. Вопрос: Что произойдёт, если попытаться вызвать приватный метод структуры извне?

    Ответ: Компилятор выдаст ошибку. Приватные методы доступны только внутри того же модуля, где определена структура.

  7. Вопрос: Какой модификатор доступа используется в Rust для публичных элементов?

    Ответ: Ключевое слово pub. Например: pub fn method() или pub field: Type.

  8. Вопрос: Что такое полиморфизм?