Наследование против композиции
Что вы узнаете: почему в Rust нет наследования классов, как трейты и структуры заменяют глубокие иерархии классов, и практические паттерны для достижения полиморфизма через композицию.
Сложность: 🟡 Средний
// C# — наследование на основе классов
public abstract class Animal
{
public string Name { get; protected set; }
public abstract void MakeSound();
public virtual void Sleep()
{
Console.WriteLine($"{Name} is sleeping");
}
}
public class Dog : Animal
{
public Dog(string name) { Name = name; }
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
public void Fetch()
{
Console.WriteLine($"{Name} is fetching");
}
}
// Контракты на основе интерфейсов
public interface IFlyable
{
void Fly();
}
public class Bird : Animal, IFlyable
{
public Bird(string name) { Name = name; }
public override void MakeSound()
{
Console.WriteLine("Tweet!");
}
public void Fly()
{
Console.WriteLine($"{Name} is flying");
}
}
Модель композиции в Rust
#![allow(unused)]
fn main() {
// Rust — композиция вместо наследования, через трейты
pub trait Animal {
fn name(&self) -> &str;
fn make_sound(&self);
// Реализация по умолчанию (как виртуальные методы в C#)
fn sleep(&self) {
println!("{} is sleeping", self.name());
}
}
pub trait Flyable {
fn fly(&self);
}
// Отделяем данные от поведения
#[derive(Debug)]
pub struct Dog {
name: String,
}
#[derive(Debug)]
pub struct Bird {
name: String,
wingspan: f64,
}
// Реализуем поведение для типов
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Woof!");
}
}
impl Dog {
pub fn new(name: String) -> Self {
Dog { name }
}
pub fn fetch(&self) {
println!("{} is fetching", self.name);
}
}
impl Animal for Bird {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Tweet!");
}
}
impl Flyable for Bird {
fn fly(&self) {
println!("{} is flying with {:.1}m wingspan", self.name, self.wingspan);
}
}
// Несколько трейт-границ (как несколько интерфейсов)
fn make_flying_animal_sound<T>(animal: &T)
where
T: Animal + Flyable,
{
animal.make_sound();
animal.fly();
}
}
graph TD
subgraph "Иерархия наследования C#"
CS_ANIMAL["Animal (абстрактный класс)"]
CS_DOG["Dog : Animal"]
CS_BIRD["Bird : Animal, IFlyable"]
CS_VTABLE["Диспетчеризация виртуальных методов<br/>Затраты во время выполнения"]
CS_COUPLING["[ERROR] Жёсткая связанность<br/>[ERROR] Ромбовидная проблема<br/>[ERROR] Глубокие иерархии"]
CS_ANIMAL --> CS_DOG
CS_ANIMAL --> CS_BIRD
CS_DOG --> CS_VTABLE
CS_BIRD --> CS_VTABLE
CS_ANIMAL --> CS_COUPLING
end
subgraph "Модель композиции Rust"
RUST_ANIMAL["trait Animal"]
RUST_FLYABLE["trait Flyable"]
RUST_DOG["struct Dog"]
RUST_BIRD["struct Bird"]
RUST_IMPL1["impl Animal for Dog"]
RUST_IMPL2["impl Animal for Bird"]
RUST_IMPL3["impl Flyable for Bird"]
RUST_STATIC["Статическая диспетчеризация<br/>Нулевая стоимость"]
RUST_FLEXIBLE["[OK] Гибкая композиция<br/>[OK] Нет ограничений иерархии<br/>[OK] Свободное комбинирование трейтов"]
RUST_DOG --> RUST_IMPL1
RUST_BIRD --> RUST_IMPL2
RUST_BIRD --> RUST_IMPL3
RUST_IMPL1 --> RUST_ANIMAL
RUST_IMPL2 --> RUST_ANIMAL
RUST_IMPL3 --> RUST_FLYABLE
RUST_IMPL1 --> RUST_STATIC
RUST_IMPL2 --> RUST_STATIC
RUST_IMPL3 --> RUST_STATIC
RUST_ANIMAL --> RUST_FLEXIBLE
RUST_FLYABLE --> RUST_FLEXIBLE
end
style CS_COUPLING fill:#ffcdd2,color:#000
style RUST_FLEXIBLE fill:#c8e6c9,color:#000
style CS_VTABLE fill:#fff3e0,color:#000
style RUST_STATIC fill:#c8e6c9,color:#000
Упражнения
🏋️ Упражнение: замените наследование трейтами (нажмите, чтобы раскрыть)
Этот C#-код использует наследование. Перепишите его на Rust, используя композицию трейтов:
public abstract class Shape { public abstract double Area(); }
public abstract class Shape3D : Shape { public abstract double Volume(); }
public class Cylinder : Shape3D
{
public double Radius { get; }
public double Height { get; }
public Cylinder(double r, double h) { Radius = r; Height = h; }
public override double Area() => 2.0 * Math.PI * Radius * (Radius + Height);
public override double Volume() => Math.PI * Radius * Radius * Height;
}
Требования:
- Трейт
HasAreaс методомfn area(&self) -> f64 - Трейт
HasVolumeс методомfn volume(&self) -> f64 - Структура
Cylinder, реализующая оба трейта - Функция
fn print_shape_info(shape: &(impl HasArea + HasVolume))— обратите внимание на композицию трейт-границ (наследование не нужно)
🔑 Решение
use std::f64::consts::PI;
trait HasArea {
fn area(&self) -> f64;
}
trait HasVolume {
fn volume(&self) -> f64;
}
struct Cylinder {
radius: f64,
height: f64,
}
impl HasArea for Cylinder {
fn area(&self) -> f64 {
2.0 * PI * self.radius * (self.radius + self.height)
}
}
impl HasVolume for Cylinder {
fn volume(&self) -> f64 {
PI * self.radius * self.radius * self.height
}
}
fn print_shape_info(shape: &(impl HasArea + HasVolume)) {
println!("Area: {:.2}", shape.area());
println!("Volume: {:.2}", shape.volume());
}
fn main() {
let c = Cylinder { radius: 3.0, height: 5.0 };
print_shape_info(&c);
}
Ключевая мысль: C# требует трёхуровневой иерархии (Shape → Shape3D → Cylinder). Rust использует плоскую композицию трейтов — impl HasArea + HasVolume объединяет возможности без глубины наследования.