OOP: Kapselung und Polymorphie in Rust – eine ausführliche Anleitung
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — шеф-повар в ресторане. У тебя есть холодильник, плита, ножи и продукты. Ты знаешь, как приготовить борщ, пиццу и десерт. Но представь, что к тебе на кухню заходит посетитель и начинает сам лезть в холодильник, резать овощи твоим ножом и включать плиту. Что будет? Хаос! Продукты испортятся, нож затупится, а посетитель может обжечься.
В программировании происходит то же самое. Когда программа маленькая, один программист может держать всё в голове. Но когда над проектом работают 10, 100 или 1000 человек, нужно чётко договориться: «Вот это — моя кухня, сюда не лезь. Вот это — твоя зона, я туда не суюсь». Для этого и нужны инкапсуляция и полиморфизм.
Инкапсуляция — это как закрытая дверь на кухню. Ты (программист) прячешь внутренние детали работы объекта (например, как именно хранятся данные) и показываешь только то, что нужно: кнопки управления. Посетитель (другой программист) не знает, как устроен холодильник внутри, но знает, что если нажать кнопку «Заказать борщ», ему принесут борщ.
Полиморфизм — это как универсальная розетка. В неё можно воткнуть и чайник, и тостер, и фен. Они все разные, но работают от одного и того же разъёма. В программировании это значит: разные объекты могут выполнять одно и то же действие (например, «нарисовать себя»), но каждый делает это по-своему. Круг рисуется как круг, квадрат — как квадрат, а треугольник — как треугольник. Но команда одна: «Нарисуй!».
💡 Запомни главное: Инкапсуляция — это про безопасность и простоту использования. Полиморфизм — это про гибкость и расширяемость. Вместе они делают код понятным, надёжным и удобным для изменений.
Теория: как это работает изнутри
Инкапсуляция: принцип «чёрного ящика»
Представь, что каждый объект в программе — это чёрный ящик. Снаружи у него есть несколько кнопок и экран. Внутри — сложная электроника. Ты нажимаешь кнопку «Вкл» — ящик работает. Тебе не нужно знать, какие там микросхемы и провода. Это и есть инкапсуляция: скрытие внутренней реализации и предоставление публичного интерфейса.
В Rust инкапсуляция достигается через:
- Модификаторы доступа (ключевое слово
pub) — делает поле или метод доступным снаружи модуля. - Структуры (struct) — группируют данные.
- Методы (impl) — функции, привязанные к структуре.
Без pub всё внутри модуля считается приватным. Ты можешь менять внутреннее устройство структуры, и код, который её использует, не сломается — если ты не меняешь публичные методы.
Полиморфизм: принцип «один интерфейс — много реализаций»
Полиморфизм бывает разным. В Rust есть два основных вида:
- Полиморфизм через трейты (traits) — это как контракт. Трейт описывает, какие методы должен иметь тип. Любой тип, который реализует трейт, может использоваться там, где этот трейт требуется.
- Полиморфизм через перегрузку (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() — ошибка компиляции!
}
Решение: Компилятор сам скажет, какие методы не реализованы. Внимательно читай сообщения об ошибках!
Лучшие практики и советы профессионалов
- Начинай с приватных полей. Делай поля публичными только если есть веская причина. Это защитит данные от случайного изменения.
- Используй трейты для определения поведения. Трейты — это контракты. Они делают код гибким и тестируемым.
- Предпочитай композицию наследованию. В Rust нет наследования — и это хорошо. Композиция (включение одной структуры в другую) даёт больше гибкости.
- Не бойся дженериков. Они делают код универсальным без потери производительности.
- Используй паттерн "Строитель" для сложных объектов. Когда у структуры много полей, строитель делает код читаемым.
- Пиши тесты для публичных методов. Инкапсуляция позволяет тестировать поведение, не заботясь о внутреннем устройстве.
- Документируй публичный API. Используй
///для документации. Другие программисты будут благодарны. - Избегай больших трейтов. Лучше несколько маленьких трейтов, чем один большой. Это принцип разделения интерфейсов.
🔥 Золотое правило: Хороший код — это код, который легко читать, легко изменять и легко тестировать. Инкапсуляция и полиморфизм — твои главные инструменты для этого.
Вопросы для самопроверки
-
Вопрос: Что такое инкапсуляция простыми словами?
Ответ: Это сокрытие внутренних деталей работы объекта и предоставление только необходимых методов для взаимодействия с ним. Как пульт от телевизора: ты нажимаешь кнопки, не зная, как устроен телевизор внутри.
-
Вопрос: Как в Rust сделать поле структуры приватным?
Ответ: Просто не указывай
pubперед полем. По умолчанию все поля и методы приватные. -
Вопрос: Что такое трейт в Rust?
Ответ: Трейт — это набор методов, которые тип должен реализовать. Это как контракт: «Если ты реализуешь этот трейт, ты гарантируешь, что у тебя есть эти методы».
-
Вопрос: В чём разница между
&dyn Traitиimpl Trait?Ответ:
&dyn Trait— это трейт-объект, который работает во время выполнения (динамическая диспетчеризация).impl Trait— это статическая диспетчеризация, тип известен на этапе компиляции.impl Traitбыстрее, но&dyn Traitгибче. -
Вопрос: Можно ли в Rust наследовать одну структуру от другой?
Ответ: Нет. В Rust нет наследования. Вместо этого используется композиция (включение одной структуры в другую) и трейты.
-
Вопрос: Что произойдёт, если попытаться вызвать приватный метод структуры извне?
Ответ: Компилятор выдаст ошибку. Приватные методы доступны только внутри того же модуля, где определена структура.
-
Вопрос: Какой модификатор доступа используется в Rust для публичных элементов?
Ответ: Ключевое слово
pub. Например:pub fn method()илиpub field: Type. -
Вопрос: Что такое полиморфизм?