Ассоциированный тип

Подробное объяснение на пальцах ассоциированного типа в Rust

Создадим свой трейт с ассоциированным типом:

Пример 1: Трейт Container

#![allow(unused)]
fn main() {
// Определяем трейт с ассоциированным типом
trait Container {
    type Item;  // Ассоциированный тип - "какой-то тип элемента"
    
    fn get(&self) -> &Self::Item;
    fn put(&mut self, value: Self::Item);
}

// Реализация для структуры, хранящей i32
struct NumberBox {
    value: i32,
}

impl Container for NumberBox {
    type Item = i32;  // Конкретизируем: здесь элементы - i32
    
    fn get(&self) -> &Self::Item {
        &self.value
    }
    
    fn put(&mut self, value: Self::Item) {
        self.value = value;
    }
}

// Реализация для структуры, хранящей String
struct StringBox {
    value: String,
}

impl Container for StringBox {
    type Item = String;  // Конкретизируем: здесь элементы - String
    
    fn get(&self) -> &Self::Item {
        &self.value
    }
    
    fn put(&mut self, value: Self::Item) {
        self.value = value;
    }
}
}

Пример 2: Трейт Producer

#![allow(unused)]
fn main() {
trait Producer {
    type Output;  // "Я производлю значения какого-то типа"
    
    fn produce(&self) -> Self::Output;
}

// Реализация для генератора чисел
struct NumberGenerator {
    start: i32,
}

impl Producer for NumberGenerator {
    type Output = i32;  // Производит i32
    
    fn produce(&self) -> Self::Output {
        self.start + 42
    }
}

// Реализация для генератора строк
struct GreetingGenerator;

impl Producer for GreetingGenerator {
    type Output = String;  // Производит String
    
    fn produce(&self) -> Self::Output {
        "Hello!".to_string()
    }
}
}

Пример 3: Трейт Converter

#![allow(unused)]
fn main() {
trait Converter {
    type Input;
    type Output;  // Два ассоциированных типа!
    
    fn convert(value: Self::Input) -> Self::Output;
}

// Конвертер i32 -> String
struct StringConverter;

impl Converter for StringConverter {
    type Input = i32;
    type Output = String;
    
    fn convert(value: Self::Input) -> Self::Output {
        value.to_string()
    }
}

// Конвертер String -> usize  
struct LengthConverter;

impl Converter for LengthConverter {
    type Input = String;
    type Output = usize;
    
    fn convert(value: Self::Input) -> Self::Output {
        value.len()
    }
}
}

Как это использовать:

fn use_container<C: Container>(container: &C) {
    let item = container.get();
    println!("Item: {:?}", item);
}

fn use_producer<P: Producer>(producer: &P) 
where 
    P::Output: std::fmt::Display,  // Ограничение на ассоциированный тип
{
    let output = producer.produce();
    println!("Produced: {}", output);
}

fn main() {
    let num_box = NumberBox { value: 10 };
    let str_box = StringBox { value: "hello".to_string() };
    
    use_container(&num_box);  // Item: 10
    use_container(&str_box);  // Item: "hello"
    
    let num_gen = NumberGenerator { start: 5 };
    let greet_gen = GreetingGenerator;
    
    use_producer(&num_gen);   // Produced: 47
    use_producer(&greet_gen); // Produced: Hello!
}

Ключевая идея:

Ассоциированный тип — это "дырка для типа", которую заполняют при реализации трейта:

  • В трейте: type Item — "будет какой-то тип"
  • В impl: type Item = КонкретныйТип — "вот именно этот тип"

Чем отличается от дженериков:

#![allow(unused)]
fn main() {
// С дженериком - можно много реализаций для одного типа
trait GenericContainer<T> {
    fn get(&self) -> &T;
}

impl GenericContainer<i32> for NumberBox { /* ... */ }
impl GenericContainer<String> for NumberBox { /* ... */ }  // Две реализации!

// С ассоциированным типом - только одна реализация
impl Container for NumberBox {
    type Item = i32;  // Только один тип может быть Item!
    // ...
}
}

Ассоциированный тип связывает тип элемента с самой реализацией трейта, а не с каждым вызовом метода.