Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Тема
Описание
Доп.

Downcasting - иногда проще сделать быстрое и грязное приведение от супертипа к подтипу (когда обратно от подтипа к супертипу - upcast)

Даункастинг — это всеми признанный дурной тон, путь к нарушению LSP и т. д. и т.п…

Когда у вас есть значение Generic типа или просто реализует trait, но вам нужно что-то сделать с конкретным типом.

Это не очень хорошая практика, потому что вы нарушаете границу абстракции: если ваша функция принимает общий T, то ей не нужно ничего конкретного из u8 или String.

Тем не менее, иногда проще сделать быстрый и грязный переход от супертипа к подтипу т.е. Downcasting, или от подтипа к супертипу Upcasting.

Способ enum

(без использования Generic или Trait object)

using-an-enum-to-store-multiple-types

У этого решения есть важное ограничение: оно работает только в том случае, если вам нужно учитывать фиксированную группу типов.

enum Value {
    Int(isize),
    Text(String)
}

// возьмите супертип и используйте подтип
fn print_value(val: Value) {
    match val {
        Value::Int(number) =>
            println!("A number w/ {} ones in binary", number.count_ones()),
        Value::Text(string) =>
            println!("A string as bytes: {:?}", string.as_bytes())
    }
}
fn main(){}

Краткий обзор трейт-обьекта

трейт-объекты:

fn main(){
//Объект типажа создается посредством Type coercions принуждения (или явным образом посредством приведения типов  через `as` )
//Стирание типа - как только мы создадим объект типажа, мы не сможем просто привести его обратно к исходному типу.
 
   let trait_obj: &dyn Display = &5u8;
   println!("I can display: {}", trait_obj);
 
//сам объект не обязательно должен находиться в куче:
    `&10 as &dyn Debug`
}

С помощью Any мы можем написать такой код, чтобы легко делать Downcasting

Гибко и меньше бинарный файл, но выполнение медленнее чем мономорфный код


use std::any::{Any, TypeId};
fn log(value: &dyn Any) {
    match value.downcast_ref::() {
        Some(text) => println!("Bytes of the string: {:?}", text.as_bytes()),
        None => println!("No string...")
    };
}
fn main() {
    // &T is coerced into &dyn Any:
    log(&String::from("hello"));
    log(&10);
}

Трейт Any реализуется автоматически для всех типов, которые не содержат нестатических ссылок.


struct Abc(u8);
fn main() {
    use std::any::{Any, TypeId};
    let val: Abc = Abc(10u8);
    println!("Type ID of u8: {:?}", val.type_id());
    assert_eq!(val.type_id(), TypeId::of::());
}

Any trait, который позволяет динамическое типирование любого 'static типа через отражение во время выполнения.

Тип для эмуляции динамической типизации.

Downcasting - иногда проще сделать быстрое и грязное приведение от супертипа к подтипу (когда обратно от подтипа к супертипу - upcast)

Any trait, который позволяет динамическое типирование любого 'static типа через отражение во время выполнения.

Использование в качестве типажа-обьекта: is() и downcast_ref() методы, чтобы проверить, является ли содержащееся значение заданным типом, и получить ссылку на внутреннее значение как тип

Как &mut Any метод downcast_mut() для получения изменчивой ссылки на внутреннее значение.

Box<Any> добавляет downcast() метод, который пытается преобразовать в Box<T>

Пример

Эту черту редко нужно использовать, потому что помимо того, что параметрический полиморфизм превосходит специальный полиморфизм в большинстве сценариев, последний также можно эмулировать с помощью перечислений, которые более безопасны для типов и требуют меньше косвенности.

Например, мы могли бы написать приведенный выше пример так:


#[derive(Default)]
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn inc(&mut self) {
        self.x += 1;
        self.y += 1;
    }
}

enum Stuff {
    Integer(i32),
    String(String),
    Point(Point),
}

fn map_stuff(mut stuff: Stuff) -> Stuff {
    match &mut stuff {
        Stuff::Integer(num) => *num += 1,
        Stuff::String(string) => *string += "!",
        Stuff::Point(point) => point.inc(),
    }
    stuff
}

fn main() {
    let mut vec = vec![
        Stuff::Integer(0),
        Stuff::String(String::from("a")),
        Stuff::Point(Point::default()),
    ];
    // vec = [0, "a", Point { x: 0, y: 0 }]
    vec = vec.into_iter().map(map_stuff).collect();
    // vec = [1, "a!", Point { x: 1, y: 1 }]
}

При динамической диспетчеризации нет общего способа узнать тип чего-либо, ваши значения - это просто байты в памяти без каких-либо метаданных. Черта Any частично решает эту проблему.

С помощью Any мы можем написать такой код, чтобы легко преобразовывать значения:


use std::any::{Any, TypeId};
fn log(value: &dyn Any) {
    match value.downcast_ref::() {
        Some(text) => println!("Bytes of the string: {:?}", text.as_bytes()),
        None => println!("No string...")
    };
}
fn main() {
    // &T is coerced into &dyn Any:
    log(&String::from("hello"));
    log(&10);
}

Как в Any работает downcast_ref, downcast_mut ?

выполняют приведение unsafe указателя

unsafe { &*(self as *const dyn Any as *const T) } // self is &dyn Any

На самом деле, мы можем сделать Any стиль downcast с любым типажом, проблема только в том, что нам нужно знать тип:


fn main(){
 let x = &String::from("hello") as &dyn Trait;
 let y: &String = unsafe { &*(x as *const dyn Trait as *const String) };
 println!("bytes: {:?}", y.as_bytes());
}

Мы могли бы даже реализовать нашу собственную небезопасную downcast функцию dyn Trait:


trait Trait {}
impl Trait for String {}
impl Trait for u8 {}

impl dyn Trait {
    // SAFETY: I hope you know what you're doing
    unsafe fn downcast(&self) -> &T {
        &*(self as *const dyn Trait as *const T)
    }
}
fn main() {
    let a: &dyn Trait = &42_u8;
    let b: &dyn Trait = &String::from("hello");

    let _number: u8 = *unsafe { a.downcast::() };
    let _text: &str = unsafe { b.downcast::() };
}

Суть в том, что нам даже не нужен трейт Any, что действительно важно TypeId::of::<T>(), так это то Any, что он упрощает нашу жизнь, предоставляя эту функциональность с помощью стабильного API

fn type_id(&self) -> TypeId

Трейт Any реализуется автоматически для всех типов, не содержащих не - 'static ссылки. Когда трейт находится в области видимости, вы можете вызвать type_id метод в значении, чтобы узнать, какой у них тип:


use std::any::{Any, TypeId};

fn is_string(s: &dyn Any) -> bool {
    TypeId::of::() == s.type_id()
}
fn main() {
    assert_eq!(is_string(&0), false);
    assert_eq!(is_string(&"cookie monster".to_string()), true);
}

pub fn is<T>(&self) -> bool

Мы также можем использовать этот is метод, если значение является dyn Any трейт-объектом:


use std::any::Any;
fn is_string(s: &dyn Any) {
    if s.is::() {
        println!("It's a string!");
    } else {
        println!("Not a string...");
    }
}
fn main() {
    is_string(&0);
    is_string(&"cookie monster".to_string());
}

Применение


use std::fmt::Debug;
use std::any::Any;
fn main(){
   let my_string:String = "Hello World".to_string();
    do_work(&my_string);

    let my_i8: i8 = 100;
    do_work(&my_i8);

   let mut mut_string:String = "Hello".to_string();
   let mut mut2_i8: i8 = 100;
   mut_log(&mut mut_string,&mut mut2_i8);
   println!("{} ,{}",mut_string,mut2_i8);// Hello World ,100
}

// Эта функция хочет записать свой параметр перед выполнением с ней работы.
fn do_work(value: &T) {
    log(value);
    // ...сделать другую работу
}

// Функция Logger для любого типа, который реализует Debug
fn log(value: &T) {
    let value_any = value as &Any;

    // попробуйем преобразовать наше значение в String или Number. 
    if value_any.is::() {
         if let Some(as_string) = value_any.downcast_ref::(){
            println!("String ({}): {}", as_string.len(), as_string);
         }
    }else if value_any.is::() {
         if let Some(as_number) = value_any.downcast_ref::(){
            println!("Number: {}", as_number);
         }
    }
}
fn mut_log<T: Any + Debug>(value: &mut T,value2: &mut Any) {
    
    let value_any = value as &mut Any;// достаем тип

   // попробуйем преобразовать наше значение в String или Number. 
    if value_any.is::<String>() {
         if let Some(as_string) = value_any.downcast_mut::<String>(){
             as_string.push_str(" World");
         }
    }else if value2.is::<i8>() {
         if let Some(as_number) = value2.downcast_mut::<i8>(){
            *as_number+=27;
         }
    }
}
fn main(){}

use std::any::Any;
fn main(){
   let a1:A1=A1{};
   let mut a2:A2=A2{};
   test(&a1,&mut a2);
   test(&String::from("Hello"),&mut 100i8);
}

trait B:Any{}
#[derive(Debug)]
struct A1{}
#[derive(Debug)]
struct A2{}
impl B for A1{}
impl B for A2{}

fn test(value: &T,value2: &mut Any){
 let value_any = value as &Any;
    if value_any.is::() {
         if let Some(as_string) = value_any.downcast_ref::(){
            println!("String ({}): {}", as_string.len(), as_string);
         }
    }else if value_any.is::() {
         if let Some(as_A1) = value_any.downcast_ref::(){
            println!("A1: {:?}", as_A1);
         }
    }else if value_any.is::() {
         if let Some(as_A2) = value_any.downcast_ref::(){
            println!("A2: {:?}", as_A2);
         }
    }
}

Типы с постоянным размером, известным во время компиляции.

Маркер Sized говорит что нельзя использовать trait object'ами и типами без размера

generic T в аргументах методов и полях структур по умолчанию Sized

Trait по умолчанию !Sized (с неизвестным размером во время компиляции)

Generic по умолчанию Sized

Ключевые выводы

  • только экземпляры размерных типов могут быть помещены в стек, т.е. могут передаваться по значению
  • экземпляры типов без размера не могут быть помещены в стек и должны передаваться по ссылке
  • указатели на безразмерные типы имеют двойную ширину, потому что помимо указания на данные им нужно сделать дополнительный бит учета, чтобы также отслеживать длину данных или указывать на vtable

Большинство типов в Rust имеют определенный размер в байтах, который можно узнать во время компиляции. Например, i32 размер an составляет 32 бита или 4 байта. Однако есть некоторые типы, которые полезно выражать, но они не имеют определенного размера (так называемые типы «без размера» или «типы с динамическим размером»). Один из примеров [T]: он представляет определенное количество T в последовательности, но мы не знаем, сколько их, поэтому размер неизвестен.

Все типы с постоянным размером, известным во время компиляции в Rust, реализуют Sized признак маркера. И все параметры типа (за исключением Self признаков) всегда имеют неявную границу Sized. Таким образом, обычно вам не следует беспокоиться об указании Sized признака маркера в коде.

Размер обобщенных типов

Generic по умолчанию Sized

По умолчанию обобщённые функции будут работать только с типами чей размер известен во время компиляции. Поэтому компилятор из этого:

   fn generic<T>(t: T) {...}

делает это:

   fn generic<T: Sized>(t: T) {...}

Но мы может сами снять это ограничение на размер Явно обозначить что T будет иметь или не будет иметь известный размер во время компиляции

   fn generic<T: ?Sized>(t: &T) {...}

Sized маркер запрета при использовании трейт-обьектами

Ссылки на тип Sized это тонкие указатели т.е. просто адрес в памяти на данные, а traite object реализуются толстыми указателями и имеет два адреса, одни для данных, другой для vtable для реализации этой черты для конкретного типа, стоящего за объектом черты и используя trait object настоящий тип стирается т.е. вы не знаете базовый тип поэтому вы не можете вызвать метод использующий Self потому что это конкретный тип, но можно сделать эти методы недоступными для trait object пометив их Sized


trait Foo {
  fn dont_need_sized(&self);
  fn need_sized(self) -> Self where Self: Sized;// пометили запрет для trait object
}
struct Bar{}

impl Foo for Bar { 
    fn dont_need_sized(&self) {
      println!("dont_need_sized");
   }
   // реализуем но не сможем использовать через trait object 
   fn need_sized(self) -> Self where Self: Sized{
       self
   }
}

fn test_DST(x:&dyn Foo){
    x.dont_need_sized();
}
fn test(x:Bar)->Bar{
    x.need_sized()
}
fn main() {
   let x: &dyn Foo = &Bar{};
   x.dont_need_sized();
  // x.need_sized();// error: the `need_sized` method cannot be invoked on a trait object
  
  test_DST(&Bar{});// Ok
  test(Bar{});// Ok
}

ограничение безразмерного типа ?Sized

?Sized привязка типажа, которая снимает неявную Sized привязку, позволяя использовать больше типов в общем коде (чтобы обеспечить лучший API и эргономику)

sizedness-in-rust

Есть три ограничения для типов динамического размера:

  • Мы можем работать с экземпляром безразмерного типа только с помощью указателя. &[T] будет работать, а [T] — нет.
  • Переменные и аргументы не могут иметь тип динамического размера.
  • Только последнее поле структуры может быть безразмерного типа; другие — нет. Варианты перечислений не могут содержать типы динамического размера в качестве данных.

Реализацию для безразмерного типа смогут использовать любые указатели, включая определённые пользователем умные указатели

  impl<T> Foo for [T] {}
  // impl Foo for &str {} вместо этого

struct Foo<T: ?Sized> { // `T: ?Sized` - читается как «Т может быть размерным (Sized)»
    f: T,
} 
struct MySuperSliceable<T: ?Sized> {
    info: u32,
    data: T,
}
fn main() {
    let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable {
        info: 17,
        data: [0; 8],
    };

    let dynamic: &MySuperSliceable<[u8]> = &sized;

    // prints: "17 [0, 0, 0, 0, 0, 0, 0, 0]"
    println!("{} {:?}", dynamic.info, &dynamic.data);
}

Реальный пример ?Sized

trait CommandHandler<C: Command> {
    type Context: ?Sized;
    type Result;

    fn handle_command(&self, cmd: &C, ctx: &Self::Context) -> Self::Result;
}
// что позволяет использовать «неразмерные» типы, такие как объекты типажей

impl CommandHandler<CreateUser> for User {
    type Context = dyn UserRepository;
    type Result = Result<(), UserError>;
    
    fn handle_command(&self, cmd: &CreateUser, user_repo: &Self::Context) -> Self::Result {
        // Here we operate with the `UserRepository`
        // via its trait object `&dyn UserRepository`
    }
}
fn main(){}

[T], str, SomeTrait - типы без размера

&[T], &str, &SomeTrait, Box… - типы с размером.

Обычно вы пишете, trait Foo: Sized если полагаетесь, что размер любого реализующего типа будет известен во время компиляции (само определение размера).

Таким образом, получение метода self может быть вызвано только в том случае, если тип имеет размер.

Аналогичным образом, метод, принимающий Self в качестве параметра, имеет такое же ограничение - вы должны знать его размер статически (время компиляции), чтобы передать его в стек.

trait Foo {
   fn dont_need_sized(&self);
   fn need_sized(self) -> Self where Self: Sized;
}
// Foo является объектно-безопасным, но need_sized не может вызывать типаж-объект этого типа.

Все параметры типа имеют неявную границу Sized, по этому использование неизвестно размера у типа следует явно пометить ?Sized

Все параметры типа имеют неявную границу Sized, по этому использование неизвестно размера у типа следует явно пометить ?Sized

struct Foo<T>(T); становится struct Foo<T:Sized>(T);

при ее использовании с безразмерным типом

struct FooUse(Foo<[i32]>); // error: Sized is not implemented for [i32] 
struct FooUse(Foo<Box<[i32]>>); или struct FooUse(Foo<i32>);// а дав размерный тип, все хорошо

Для типов размер которых неизвестен во время компиляции, динамический размер ?Sized задается явно

struct Bar<T: ?Sized>(T);
// использование компилируется в
struct BarUse(Bar<[i32]>);

Rust понимает некоторые из типов без размера, но у них есть три ограничения:

1. Мы можем управлять экземпляром безразмерного типа только через указатель. &[T] работает нормально, а [T] - нет.

struct Teat<'a,T>{
     d:&'a[T],
     v:String
}

2. Переменные и аргументы не могут иметь типы с динамическим размером.

3. Только последнее поле в a struct может иметь тип с динамическим размером; другие поля не должны. Варианты перечисления не должны иметь в качестве данных типы динамического размера.

❌ не скомпилируется:

struct Teat<T>{
     d:[T],
     v:String,
}

✅ скомпилируется: (но работать как с жирным указателем. Вся структура становится безразмерным типом)

struct Teat<T>{
     v:String,
     d:[T]
}

?Sized тип с динамическим размером

#[derive(Debug)] 
struct Foo<T: ?Sized> {
    f: T,
}
fn main() {
   let i = Foo{f:1_i32};
   let y = Foo{f:"hello".to_string()};
   let arr = [1,2,3];
   let z = Foo{f:&arr[..]};
   println!("{:?} {:?} {:?}",i,y,z);
}

?Sized можно произносить как «необязательно размер» или «возможно размер», и добавление его к границам параметра типа позволяет типу иметь размер или нет.

?Sized обычно называется «расширяющейся границей» или «ослабленной границей», поскольку она ослабляет, а не ограничивает параметр типа.

?Sized единственная ослабленная связка в Rust

Так почему это важно?

Что ж, каждый раз, когда мы работаем с универсальным типом, и этот тип находится за указателем, мы почти всегда хотим отказаться от Sized привязки по умолчанию, чтобы сделать нашу функцию более гибкой в ​​том, какие типы аргументов она будет принимать.

Кроме того, если мы не откажемся от Sized ограничения по умолчанию, мы в конечном итоге получим несколько неожиданных и сбивающих с толку сообщений об ошибках компиляции.

use std::fmt::Debug;

fn dbg<T: Debug>(t: &T) { // T: Debug + Sized так по умолчанию в итоге отправляя str а он !Sized. но мы же явно ослабили только типами Sized
    println!("{:?}", t);
}
fn main() {
     dbg("my str"); // &T = &str, T = str, str: Debug + !Sized ❌
     //dbg(&"my str");// так работает, но неудобно
}

Следует явно ослабить требования к размеру:

fn dbg<T: Debug + ?Sized>(t: &T) { 
    println!("{:?}", t);
}

trait object

Dynamically Sized Types (к примеру срез имеет динамический размер)

[T] и dyn Trait это примеры DST: размер объекта не определяется статически.

Обращение к DST — через толстый указатель.

Для не DST типов верно: Sized.

Типовые параметры по умолчанию Sized

Трейт-объекты не должны иметь Sized:

trait Foo { }
trait Bar: Sized { }

struct Impl;
impl Foo for Impl { }
impl Bar for Impl { }

fn main(){
    let x: &dyn Foo = &Impl;    // OK
    // let y: &dyn Bar = &Impl; // error:  trait `Bar` неможет превратится в обьект, так как Sized запрещает
}

Есть несколько ограничений, чтобы трейт был объектно-безопасным - неиспользование Self лишь одно из них. Когда вы создаете типажный объект, базовый тип стирается. Таким образом, получение метода Self недопустимо, потому что по определению мы больше не знаем этот тип - все, что мы знаем, это реализует черту, но не ее точный тип (который есть Self)

?Sized - значит размера может и не быть или может быть


fn main(){
    fn size_of() -> usize{1}// неявный where T: Sized
    // выключим bount trait Sized , передача только по ссылке так как размера не знаем
    fn size_of_val(val: &T) -> usize{
       println!("{}",val);
       1
    }
    // включим bount trait Sized, передача и через ссылку и по значению так как знаем размер
    fn size_of_val2(val: T, val_ref: &T) -> usize{
       println!("{}",val);
     1
    }
    size_of_val(&1_usize);
    size_of_val2(1_usize,&1_usize);
}

Размеры типов

sizedness-in-rust


use std::mem::size_of;
fn main() {
    // primitives
    assert_eq!(4, size_of::());
    assert_eq!(8, size_of::());

    // tuples
    assert_eq!(8, size_of::<(i32, i32)>());

    // arrays
    assert_eq!(0, size_of::<[i32; 0]>());
    assert_eq!(12, size_of::<[i32; 3]>());

    struct Point { x: i32, y: i32}

    // structs
    assert_eq!(8, size_of::());

    // enums
    assert_eq!(8, size_of::>());

    // get pointer width, will be
    // 4 bytes wide on 32-bit targets or
    // 8 bytes wide on 64-bit targets
    const WIDTH: usize = size_of::<&()>();

    // указатели на размерные типы имеют ширину 1
    assert_eq!(WIDTH, size_of::<&i32>());
    assert_eq!(WIDTH, size_of::<&mut i32>());
    assert_eq!(WIDTH, size_of::>());
    assert_eq!(WIDTH, size_of:: i32>());

    const DOUBLE_WIDTH: usize = 2 * WIDTH;

    // unsized struct
    struct Unsized {
        unsized_field: [i32],
    }

    // указатели на безразмерные типы имеют 2 ширины
    assert_eq!(DOUBLE_WIDTH, size_of::<&str>()); // slice
    assert_eq!(DOUBLE_WIDTH, size_of::<&[i32]>()); // slice
    assert_eq!(DOUBLE_WIDTH, size_of::<&dyn ToString>()); // trait object
    assert_eq!(DOUBLE_WIDTH, size_of::>()); // trait object
    assert_eq!(DOUBLE_WIDTH, size_of::<&Unsized>()); // user-defined unsized type

    // unsized types
    size_of::(); // compile error
    size_of::<[i32]>(); // compile error
    size_of::(); // compile error
    size_of::(); // compile error

Функция или тип данных, могут работать для нескольких типов аргументов

Параметрический полиморфизм - Типы или функции имеют несколько форм (poly — кратно, morph — форма) по данному параметру («параметрический»)

Generics - обобщённые типы данных

Одно определение — многостороннее использование.

  • Полезны для снижения дублирования кода
  • Применяется для ограничения типажом

Общее правило использовать Generics:

  • Используйте ассоциированные типы, когда для каждого типа должно быть только одно воплощение трейта.
  • Используйте универсальные типы, когда может быть много возможных импликаций черты для каждого типа.

Generics

Generics позволяют программисту писать код, который работает для некоторого произвольного типа T, а конкретные варианты использования универсального кода генерируются во время компиляции — процесс, известный как мономорфизация.

Другими словами, программист пишет одну универсальную функцию, но компилятор выводит разные мономорфизированные версии этой функции для каждого типа, с которым вызывается функция.

мономорфизация vs vtable

мономорфизация Статическая диспетчеризация:

генерируя необобщенную копию универсальных функций для каждой комбинации конкретных типов, с которыми они вызываются.

fn use_generics<T: Trait>(x: T) {}
fn use_impl(x: impl Trait) {}

трейт-объекты Динамическая диспетчеризация (vtable):

Объект типажа создается посредством Type coercions принуждения (или явным образом посредством приведения типов через as) Стирание типа - как только мы создадим объект типажа, мы не сможем просто привести его обратно к исходному типу.

fn main(){
    let trait_obj: &dyn Display = &5u8;
    println!("I can display: {}", trait_obj);

// сам объект не обязательно должен находиться в куче:
    &10 as &dyn Debug
}

Решение с Trait-Bound Generics

Обратной стороной мономорфизации является создание большего количества кода, поэтому, если вы используете универсальные функции с множеством разных типов, ваш двоичный файл может раздуться.

монорепликация (monomorphization): компилятор создаёт отдельную версию функции max для каждого типа, который мы используем.

fn max<T: PartialOrd>(a: T, b: T) -> T { 
    if a > b { a } else { b } 
} 
fn main(){
    let m1 = max(5, 10); // Работает с i32 
    let m2 = max(3.14, 2.71); // Работает с f64
}

Поэтому для i32 и f64 будут разные реализации.


пример мономорфизации, параметрический полиморфизм (Generics)
trait Value {
    fn as_int(&self) -> Option<isize> { None }
    fn as_text(&self) -> Option<&str> { None }
}
impl Value for isize {
    fn as_int(&self) -> Option<isize> { Some(*self) }
}
impl Value for String {
    fn as_text(&self) -> Option<&str> { Some(self) }
}
fn print_value<T: Value>(val: T) {
    if let Some(num) = val.as_int() {
        println!("A number w/ {} ones in binary", num.count_ones());
    }
    if let Some(string) = val.as_text() {
        println!("A string as bytes: {:?}", string.as_bytes());
    }
}
fn main() {
    print_value(110);
    print_value(String::from("hello"));
}

пример мономорфизации, полиморфизм подтипов (Subtyping / Inheritance)
// Этот код создаёт monomorphization, компилятор создаёт отдельную версию функции animal_sound для каждого типа, с которым мы используем функцию, что раздувает бинарный файл.
trait Animal {
    fn make_sound(&self);
}

struct Dog;
impl Animal for Dog {
    fn make_sound(&self) { println!("Woof!"); }
}

struct Cat;
impl Animal for Cat {
    fn make_sound(&self) { println!("Meow!"); }
}

fn animal_sound(animal: &impl Animal) {
    animal.make_sound();
}
fn main(){
    let dog = Dog;
    let cat = Cat;
    animal_sound(&dog);  // Woof!
    animal_sound(&cat);  // Meow!
}

Почему это увеличивает бинарник?

  • fn max<T: PartialOrd> не существует как одна универсальная функция в рантайме.
  • Rust делает примерно так:
  fn max_i32(a: i32, b: i32) -> i32 { if a > b { a } else { b } }
  fn max_f64(a: f64, b: f64) -> f64 { if a > b { a } else { b } }
  • При большом количестве типов → больше кода → code bloat.

Почему Rust так делает, а не как Java или C#?

  • Rust не имеет виртуальных функций по умолчанию и избегает динамической диспетчеризации ради скорости.

  • Такой подход даёт:

    • ✅ Inline и оптимизация → очень быстрый код.
    • ✅ Без лишних указателей и heap → безопасный и эффективный.
    • ❌ Больший бинарник при множестве типов.

Есть ли способ уменьшить размер бинарника в Rust?

Да, три основных приёма:

  1. Использовать динамическую диспетчеризацию (trait objects)

    fn max_dyn(a: &dyn PartialOrd, b: &dyn PartialOrd) -> &dyn PartialOrd { ... }
    

    Но это:

    • Медленнее (vtable lookup).
    • Теряет generic-производительность.
  2. LTO (Link-Time Optimization) Добавь:

    [profile.release]
    lto = true
    

    Компилятор выкинет неиспользуемый код и схлопнет дубли.

  3. Generic where нужно, перегрузка где можно Иногда проще написать две функции:

   fn max_i32(a: i32, b: i32) -> i32 { ... }
   fn max_f64(a: f64, b: f64) -> f64 { ... }

Это как раз аналог Ad-hoc полиморфизма в C++.

Используйте признаки маркера, чтобы различать поведения, которые не могут быть выражены в сигнатурах функций признака т.е. даже если вы не можете выразить трейтами поведение то вы можете сделать трейт маркер что бы явно дать понять какой поведение вы хотите.

Использовать можно как trait bound или trait object

Пример синтаксиса обобщений в ф-циях

 fn function<F>(f: F) where for<'a> F: FnOnce(&'a Type)

 struct Struct<F> where for<'a> F: FnOnce(&'a Type) { x: F }

 enum Enum<F> where for<'a> F: FnOnce(&'a Type) { Variant(F) }

 impl<F> Struct<F> where for<'a> F: FnOnce(&'a Type) { fn x(&self) -> &F { &self.x } }

 fn foo<'a, T>(s:&'a str) {}

 trait A<U> {}

 struct Ref<'a, T> where T: 'a { r: &'a T }

 struct InnerArray<T, const N: usize>([T; N]);

 struct EitherOrderWorks<const N: bool, U>(U);

Применяется для ограничения типажом

Мы ограничили входной параметр типажом Display и Debug, т.е. функция принимает только те типы которые реализуют Display и Debug

Ограничение сужает список типов, допустимых к использованию

fn printer<T: Debug + Display>(t: T) {
    println!("{:?} {}", &t,&t);
}
fn main(){}

1. Согласованность конфликтующих реализаций признаков

defeating-coherence-rust

Основная задача ввести параметр-характеристику, который предотвращает конфликт двух реализаций

trait Noise<M> {
  fn make_noise(&self);
}

struct Quiet;
struct Loud;

struct Cat;

impl Noise<Quiet> for Cat {
  fn make_noise(&self) {
    println!("meow");
  }
}
impl Noise<Loud> for Cat {
  fn make_noise(&self) {
    println!("MRRROOOOOOW");
  }
}
fn main() {
  <Cat as Noise<Quiet>>::make_noise(&Cat);// Для компиляции вам придется написать уродливый полный путь
}

2. Согласованность конфликтующих реализаций признаков

defeating-coherence-rust

Вам нужно тщательно проектировать блоки impl, чтобы реализация всегда могла быть выведена из контекста. Ключевое наблюдение заключается в том, что параметр неявного признака представляет собой кортеж параметров функции. Поэтому функции заданного типа всегда имеют уникальный неявный параметр, в отличие от случая, когда у Cat есть два возможных неявных параметра (Loud и Quiet)


trait Noise {
    fn make_noise(&self, args: M);
}
impl Noise<(T0,)> for F where F: Fn(T0) {
    fn make_noise(&self, args: (T0,)) {
        self(args.0);
    }
}
impl Noise<(T0, T1,)> for F where F: Fn(T0, T1) {
    fn make_noise(&self, args: (T0, T1)) {
        self(args.0, args.1);
    }
}
fn main() {
    let dog = |n: usize| { println!("{}", "BARK".repeat(n)); };
    dog.make_noise((3,)); // BARKBARKBARK

    let cat = |n: usize, sound: &str| { println!("{}", sound.repeat(n)); };
    cat.make_noise((2, "MEOW"));
}

3. Согласованность конфликтующих реализаций признаков

defeating-coherence-rust

Или когда у вас есть некий контекст, который всегда устраняет неоднозначность параметра неявной черты

use std::marker::PhantomData;
trait Noise<M> {
  fn make_noise(&self);
}
struct Quiet;
struct Loud;
struct Cat;

impl Noise<Quiet> for Cat {
  fn make_noise(&self) {
    println!("meow");
  }
}

impl Noise<Loud> for Cat {
  fn make_noise(&self) {
    println!("MRRROOOOOOW");
  }
}

struct PetList<M, T> {
  pets: Vec<T>,
  _marker: PhantomData<M> // needed to satisfy rustc
}

impl<M, T: Noise<M>> PetList<M, T> {
  fn new() -> Self {
    PetList { 
      pets: Vec::new(),
      _marker: PhantomData
    }
  }
  fn push(&mut self, pet: T) {
    self.pets.push(pet);
  }
  fn everyone_is_yapping(&self) {
    for pet in &self.pets {
      pet.make_noise();
    }
  }
}
fn main() {
  // Specify `Loud` once up front, and never again!
  let mut pets: PetList<Loud, Cat> = PetList::new();
  pets.push(Cat);
  pets.everyone_is_yapping();
}

📌 попробуй его обобщить

use core::ops::Div;

use core::ops::Mul;

use core::ops::Sub;

pub fn interpolation_search(x: usize, arr:&[usize]) -> Option<usize> {
    if arr.len() == 0 {
        return None;
    }
    if arr[0] == x{
       return Some(0); 
    }  
    let mut low = 0usize;
    let mut high = arr.len() - 1;
    let mut pos = 0usize; 
  
    while  low <= high && x >= arr[low] && x <= arr[high] {
        pos = low + (((high-low) / (arr[high]-arr[low]))*(x - arr[low]))  ;
         
        if arr[pos]==x {
            return Some(pos);
        }else if arr[pos]<x {
            low=pos+1;
        }else {
            high=low-1;
        }
    }
    None
}
fn main(){}

Пример для обобщения
use core::fmt::Debug;
use core::ops::Div;
use core::ops::Mul;
use core::ops::Sub;
 
pub fn interpolation_search<T,R>(x: T, arr:&[T]) -> Option<R> 
    where T: Clone + Copy + PartialOrd + Mul + Div + Sub + TryInto<usize> + Debug
,<T as Div>::Output: PartialOrd<T>
,<T as Mul>::Output: PartialOrd<T>
,<T as Sub>::Output: PartialOrd<T>,  usize: From<<T as Sub>::Output>,
R:TryInto<usize> + std::convert::From<usize>
{
        if arr.len() == 0 {
            return None;
        }
        if arr[0] == x{
           return Some(0usize.try_into().unwrap()); 
        }  
        let mut low = 0usize;
        let mut high = arr.len() - 1;
        let mut pos = 0usize; 
      
        while  low <= high && x >= arr[low] && x <= arr[high] {
            let t1:usize = (arr[high]-arr[low]).try_into().unwrap();
            let t2:usize = (x - arr[low]).try_into().unwrap();
            pos = low + (((high-low) / t1 )*( t2 ));
             
            if arr[pos]==x {
                return Some(pos.try_into().unwrap());
            }else if arr[pos]<x {
                low=pos+1;
            }else {
                high=low-1;
            }
        }
        None
    }

fn main() { 
    let mut arr:Vec<i32> = vec![1,2,4,5,6,7,8,90/*,150,151,152,153,154,155,156,157,158,159,160,161*/];
    //arr.sort();
    //println!("{:?}",arr);
    let search_value = arr[0];
    if let Some(index) = interpolation_search::<i32,usize>(search_value,&arr){
        println!("result: index={} value={}",index,arr[index]);
    }else{
        println!("notfound");
    }
}

Общая реализация для любого типа T действует на весть код у которого есть реализация Debug даже для внутренних типов


use std::fmt::Debug;
trait Something{
    fn print(self);
    fn print_self(&self) where Self:Debug{
        println!("{:?}",self);
    }
}
impl Something for T where T:Debug {
    fn print(self) {
        println!("{:?}", self);
    }
}
fn main() {
    let vec = vec![1, 2, 3];
    vec.print_self();
    vec.print();
    
    let s = String::from("hello");
    s.print_self();
    s.print();
}

impl Trait for [T]


trait Trait {
    fn method(&self) {}
}
impl Trait for str {
    // теперь можно вызвать "метод" на
    // 1) str or
    // 2) String since String: Deref
}
impl Trait for [T] {
    // теперь можно вызвать "метод" на
    // 1) any &[T]
    // 2) any U where U: Deref, e.g. Vec
    // 3) [T; N] for any N, since [T; N]: Unsize<[T]>
}
fn str_fun(s: &str) {}
fn slice_fun(s: &[T]) {}
fn main() {
    let str_slice: &str = "str slice";
    let string: String = "string".to_owned();

    // function args
    str_fun(str_slice);
    str_fun(&string); // deref приведение

    // method calls
    str_slice.method();
    string.method(); // deref приведение

    let slice: &[i32] = &[1];
    let three_array: [i32; 3] = [1, 2, 3];
    let vec: Vec = vec![1];
 
    // method calls
    slice.method();
    vec.method(); // deref приведение
    three_array.method(); // безразмерное приведение
}

Реализация трейта для внутреннего типа

будет доступно внутри scope где импортирован или определен трейт


trait Say{
    fn say(&self) where Self: std::fmt::Display{
        println!("say:{}",self);
    }
}
impl Say for i32{}
impl Say for String{}
 
fn main(){
    8.say();
    String::from("hello").say();
}

конкретные реализации для типов

MyStruct<u32> и MyStruct<usize> это разные типы!

trait AGATOR{  
    fn new() -> Self;
}
struct AG<T=u32>{
    value:Vec<T>
  
}
impl AGATOR for AG<u8>{
    fn new() -> Self{
        Self{value:vec![]}
    }
}
impl AGATOR for AG<u16>{
    fn new() -> Self{
        Self{value:vec![]}
    }
}
fn main(){}

use std::marker::PhantomData;

trait Creator{ // только для реализации метода new для MyStruct с FROM=u32 по дефолту
    fn new(_:&[u8]) -> Self;
}
struct MyStruct<FROM=u32>{
    value:Vec<u8>,
    _marker:PhantomData<FROM>
}
impl<FROM> Creator for MyStruct<FROM>{
    fn new(value:&[u8]) -> Self{
        Self{value:value.to_vec(),_marker:PhantomData::<FROM>}
    }
}
// конкретные реализации для типов
impl MyStruct<u32> {
    fn show(&self)->u32{
      let res:u32 = u32::from_be_bytes(TryFrom::try_from(self.value.clone()).unwrap());
      println!("u32:{}",&res);
      res
    }
}
impl MyStruct<usize> {
    fn show(&self)->usize{
      let res:usize = usize::from_be_bytes(TryFrom::try_from(self.value.clone()).unwrap());
      println!("usize:{}",&res);
      res
    }
}
fn main() {
    let s:MyStruct<u32> = MyStruct::<u32>::new(&[0,0,0,4]);
    s.show();

    let s:MyStruct<usize> = MyStruct::new(&[0,0,0,4,0,0,0,4]);
    s.show();

    let s:MyStruct<usize> = MyStruct::<usize>::new(&[0,0,0,4,0,0,0,4]);
    s.show();
}

Размер обобщенных типов

(относится к динамической диспетчиризации)

По умолчанию обобщённые функции будут работать только с типами чей размер известен во время компиляции Sized.

// Поэтому компилятор из этого:
   fn generic<T>(t: T) {...}

делает это:

   fn generic<T: Sized>(t: T) {...}

Но мы может сами снять это ограничение на размер. Явно обозначить что T с неизвестным размером во время компиляции

fn generic<T: ?Sized>(t: &T) {...}

Прокидывание типов для обозначения другого типа.

Эту проблему решает Ассоциированные типы ('семейства типа') через type

fn difference<A, B, C>(container: &C) -> i32 where
    C: Contains<A, B> {
    container.last() - container.first()
}

struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains<A, B> {
    fn contains(&self, &A, &B) -> bool; // Explicitly requires `A` and `B`.
    fn first(&self) -> i32; // Doesn't explicitly require `A` or `B`.
    fn last(&self) -> i32;  // Doesn't explicitly require `A` or `B`.
}

impl Contains<i32, i32> for Container {
    // True if the numbers stored are equal.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }

    // Grab the first number.
    fn first(&self) -> i32 { self.0 }

    // Grab the last number.
    fn last(&self) -> i32 { self.1 }
}

// `C` содержит` A` и `B`. В свете этого, чтобы выразить «А» и
//  `B` снова является неприятностью.
fn difference<A, B, C>(container: &C) -> i32 where
    C: Contains<A, B> {
    container.last() - container.first()
}

fn main() {
    let number_1 = 3;
    let number_2 = 10;

    let container = Container(number_1, number_2);

    println!("Does container contain {} and {}: {}",
             &number_1, &number_2,
             container.contains(&number_1, &number_2));
    println!("First number: {}", container.first());
    println!("Last number: {}", container.last());

    println!("The difference is: {}", difference(&container));
}

Как не надо делать

Структуры данных не дублируют производные границы признаков

data-structures-do-not-duplicate-derived-trait-bounds-c-struct-bounds

// ✅ Хорошо  
#[derive(Clone, Debug, PartialEq)]
struct Good<T> { /* ... */ }

// ❌ Плохо  
#[derive(Clone, Debug, PartialEq)]
struct Bad<T: Clone + Debug + PartialEq> { /* ... */ }

Исключения

Есть три исключения, когда требуются границы признаков для структур:

  • Структура данных относится к ассоциированному типу признака.
  • Граница есть ?Sized (потому что атоматически компилятор ставит Sized)
  • В структуре данных есть Drop имплант, требующий границ признака. В настоящее время Rust требует, чтобы Drop в структуре данных присутствовали все границы признаков в impl.

Как не надо делать

Описывайте bond сигнатурой не данные, а поведение т.е. методы

Размещение границ признаков на impl блоках, методах и функциях, а не на типах, это уменьшает загрязнение границ признаков

trait UserRepo{}

// ❌ Плохо 
struct UserService<R: UserRepo> {
    repo: R,
}

Мы указываем R: UserRepo здесь bound, поскольку мы хотим ограничить типы в repo поле для реализации UserRepo поведения.

Однако такое ограничение непосредственно на тип приводит к так называемому «загрязнению границ признаков»: мы должны повторять эту границу в каждом отдельном случае impl, даже в тех, которые вообще не имеют отношения к UserRepo поведению.

impl<R> Display for UserService<R>
where
    R: Display + UserRepo, // Здесь нас не интересует UserRepo,  все, что нам нужно, это просто Display.
{                          
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "UserService with repo {}", self.repo)
    }
}

В сложной кодовой базе такое загрязнение умножается из-за разных типов и в какой-то момент может стать кошмаром.

Решением этой проблемы было бы понимание того, что черта представляет собой определенное поведение, и на самом деле нам нужно это поведение только тогда, когда мы его декларируем.

В объявлении типа ничего не говорится о поведении, все дело в данных. Именно в функциях и методах происходит поведение. Итак, давайте просто ожидаем определенного поведения, когда нам это действительно нужно:

// ✅ Хорошо 
struct UserService<R> {
    repo: R,
}

// Ожидайте отображения, когда мы выражаем поведение отображения.
impl<R: Display> Display for UserService<R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "UserService with repo {}", self.repo)
    }
}

// Ожидайте UserRepo, когда мы выражаем фактическое поведение UserService, который имеет дело с пользователями.
impl<R: UserRepo> UserService<R> {
    fn activate(&self, user: User) {
        // Изменение состояния пользователя в UserRepo ...
    }
}
fn main(){}

Как не надо делать

Уберите не нужные границы

Поднять границы признаков для расширения возможностей использования типа


вы должны попытаться максимально поднять границы признаков (особенно в коде библиотеки), поскольку это расширяет возможности использования типа.
❌ Плохо 

#[derive(Clone)]
struct Loader {
    state: Arc>>,
}
struct My;

fn main(){
 let loader: Loader = ..;
 let copy = loader.clone(); // compile error as `My` doesn't impl `Clone`
}

Это происходит из-за того, что #[derive(Clone)] применяются к K: Clone и V: Clone в производном коде, несмотря на то, что они вообще не нужны

✅ Хорошо

Предоставляя ручную реализацию, мы можем Loader<My, My> без проблем клонировать значения типа:


struct Loader {
    state: Arc>>,
}
// Ручная реализация используется для исключения применения ненужных границ клонирования.
impl Clone for Loader {
    fn clone(&self) -> Self {
        Self {
            state: self.state.clone(),
        }
    }
}
fn main(){
 let loader: Loader = ..;
 let copy = loader.clone(); // it compiles now!
}

Const generics

Const generics позволяют элементам быть generic-ами по const-значениям.

S<const N: usize>


// S<10> - При использовании константные границы могут быть предоставлены как примитивные значения.
// S<{5+5}> - Выражения необходимо заключать в фигурные скобки.

struct Foo([i32; N]);
impl Foo {
    const CONST: usize = N * 4;
    // .... fn
}
 
fn main() {
    let foo = Foo([1,2]);
    assert_eq!(8,Foo::<2>::CONST);
    print!("{:?}", Foo::<2>::CONST);
}

Так как [T; 10] и [T; 11] это разные типы массивов то их содержащие структуры потребуют отдельной реализации трейтов и т.д. что бы упростить это были введены Const generics, теперь это один тип [T; N]:


#[derive(Debug)]                      
struct Buffers {
    array_one: [T; N],
    array_two: [T; N],
}
 
fn main() {
    let buffer_1 = Buffers {
        array_one: [0u8; 3],
        array_two: [0; 3],
    };
 
    let buffer_2 = Buffers {
        array_one: [0i32; 4],
        array_two: [10; 4],
    };
 
    println!("{buffer_1:#?}, {buffer_2:#?}");
}
fn double<const N: i32>() {
    println!("doubled: {}", N * 2);
}

const SOME_CONST: i32 = 12;

fn example() {
    // Example usage of a const argument.
    double::<9>();
    double::<-123>();
    double::<{7 + 8}>();
    double::<SOME_CONST>();
    double::<{ SOME_CONST + 5 }>();
}
fn main(){}

Решение конфликта имен:

type N = u32; // Псевдоним типа
struct Foo<const N: usize>;

// Следующее является ошибкой, так как `N` интерпретируется как псевдоним типа `N`
fn foo<const N: usize>() -> Foo<N> { todo!() } // ❌ ERROR

// Можно исправить, заключив его в фигурные скобки, чтобы он интерпретировался как константный параметр `N`:
fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ✅ OK

fn main(){}

// Примеры использования общих параметров const.

// Используется в подписи самого предмета.
fn foo(arr: [i32; N]) {
    // Используется как тип в теле функции.
    let x: [i32; N];
    // Используется как выражение.
    println!("{}", N * 2);
}

// Используется как поле структуры.
struct Foo([i32; N]);

impl Foo {
    // Используется как связанная константа
    const CONST: usize = N * 4;
}

trait Trait {
    type Output;
}

impl Trait for Foo {
    // Используется как связанный тип.
    type Output = [i32; N];
}
fn main() {
    let foo = Foo([1,2]);
    assert_eq!(8,Foo::<2>::CONST);
    print!("{:?}", Foo::<2>::CONST);
}

Смешивание типов


struct Point {
    x: T,
    y: U,
}
impl Point {
    fn mixup(self, other: Point) -> Point {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}
fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: ""Hello"", y: 'c'};

    let p3 = p1.mixup(p2);
    println!(""p3.x = {}, p3.y = {}"", p3.x, p3.y);
}

Параметры обобщённого типа по умолчанию


trait MyGeneric { // Rhs это параметр обобщённого типа по умолчанию
    type Output; // это тип-заполнитель
    fn add(self, rhs: Rhs) -> Self::Output;
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point2 {x: i32, y: i32}

// Rhs = (i32,i32) --------------------------------
impl MyGeneric<(i32,i32)> for Point2 {
    type Output = Point2;
    fn add(self, other: (i32,i32)) -> Point2 {
        Point2 {x: self.x + other.0, y: self.y + other.1}
    }
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {x: i32, y: i32}

// Rhs = Point (Self) default -----------------
impl MyGeneric for Point {
    type Output = Point;
    fn add(self, other: Point) -> Point {
        Point { x: self.x + other.x, y: self.y + other.y}
    }
}
fn main() {
    assert_eq!( Point { x: 1, y: 0 }.add(Point { x: 2, y: 3 }), Point { x: 3, y: 3 } );
    assert_eq!(Point2 { x: 1, y: 0 }.add((2,3)), Point2 { x: 3, y: 3 } );  
}

Полностью квалифицированный синтаксис для устранения неоднозначности: вызов методов с одинаковым именем


trait Pilot {
    fn fly(&self);
}
trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) { println!("This is your captain speaking.");}
}
impl Wizard for Human {
    fn fly(&self) {println!("Up!");}
}
impl Human {
    fn fly(&self) { println!("*waving arms furiously*");}
}
fn main() {
    let person = Human;
    Pilot::fly(&person);// конкретно для impl Pilot
    ::fly(&person);// конкретно для impl Pilot
    
    Wizard::fly(&person);// конкретно для impl Wizard
    ::fly(&person);// конкретно для impl Wizard

    person.fly();// ближняя реализация в самом Human
}

for<'a> обьяснение

 fn copy_if<F>(slice: &[i32], pred: F) -> Vec<i32> where  F: Fn(&'a i32) -> bool {
      let mut result = vec![];
       for &element in slice {
          if pred(&element) {
            result.push(element);
          }
       }
    result
}
fn main(){}

Компилятор дает следующую ошибку:

error: `element` does not live long enough
if pred(&element) {       
         ^~~~~~~

потому что локальная переменная element не живет так долго, как 'a жизни» (как мы видим из комментариев кода).

Время жизни не может быть объявлено на уровне функции, потому что нам нужно другое время жизни. Вот почему нам нужен for<'a>: чтобы указать, что ссылка может быть действительной для любого времени жизни (следовательно, можно использовать меньшее время жизни).

fn copy_if<F>(slice: &[i32], pred: F) -> Vec<i32> where for<'a> F: Fn(&'a i32) -> bool {
    let mut result = vec![];
    for &element in slice {
        if pred(&element) {
            result.push(element);
        }
    }
    result
}
fn main(){}

Границы характеристик с более высоким рейтингом

for<'a>

Как передать время жизни во входные аргументы передаваемой функции?

higher-ranked-trait-bounds

fn function<F>(f: F) where for<'a> F: FnOnce(&'a Type)

struct Struct<F> where for<'a> F: FnOnce(&'a Type) { x: F }

enum Enum<F> where for<'a> F: FnOnce(&'a Type) { Variant(F) }

impl<F> Struct<F> where for<'a> F: FnOnce(&'a Type) { fn x(&self) -> &F { &self.x } }

Границы типа могут иметь более высокий рейтинг в течение срока службы. Эти границы указывают, что ограничение истинно для всех времен жизни. Например, для такой привязки for<'a> &'a T: PartialEq<i32> потребуется реализация типа

struct T(i32);
 
impl<'a> std::cmp::PartialEq<i32> for &'a T {
   fn eq(&self, other: &i32) -> bool {self.0==*other}
}

impl<'a> std::cmp::PartialEq<i32> for T {
   fn eq(&self, other: &i32) -> bool {self.0==*other}
}
// и затем может использоваться для сравнения `&'a T` с любым временем жизни с `i32`
fn main() {
  let t = T(4);
  
  if &t == 4{
      println!("Отработал \"for &'a T\"");
  }
  if t == 4{
      println!("Отработал \"for T\"");
  }
}

Здесь может использоваться только граница с более высоким рейтингом, потому что время жизни ссылки короче, чем любой возможный параметр времени жизни в функции:

fn call_on_ref_zero< F>(f: F)  where for<'a> F: Fn(&'a str)->&'a str {
    let zero:String = "0".to_owned();
     f(&zero) ;
}

Время жизни с более высоким рейтингом также может быть указано непосредственно перед признаком: единственное различие - это область действия параметра времени жизни, который распространяется только на конец следующего признака, а не на всю границу.

Эта функция эквивалентна предыдущей.

fn call_on_ref_zero_2<F>(f: F) where F: for<'a> Fn(&'a str)->&'a str {
    let zero:String = "0".to_owned();
    f(&zero)
}

fn foo(z:&str)->&str{
    println!("z={}",z);
    z
}
fn main(){
    call_on_ref_zero(foo);
}

Обязательное утверждения where для ограничения типажом

// Вариант в стиле generic, но внутренний тип Item ожидает кокретный тип Sized а не трейт Ord
// fn find_max<I: Iterator<Item = Ord>>(iter: I) -> Option<I::Item>{ ❌

// с помощью where мы сможем указать ограничения трейтом
fn find_max<I>(iter: I) -> Option<I::Item>  ✅ 
  where I:Iterator,
             I::Item: Ord {    
    iter.reduce(|a, b| {
        if a >= b { a } else { b }
    })
}
fn main(){}

Чем меньше предположений функция делает о своих входных данных, тем шире она становится.

fn foo<I: IntoIterator<Item = i64>>(iter: I) { /* ... */ }

// заменяет все:

fn foo(c: &[i64]) { /* ... */ }

fn foo(c: &Vec<i64>) { /* ... */ }

fn foo(c: &SomeOtherCollection<i64>) { /* ... */ }

fn get_nums(a: u32, b: u32) -> impl Iterator<Item = u32> {
    (a..b).filter(|x| x % 100 == 0)
}
fn main(){}

трейт std::ops::Try вернуть Option либо Result

nightly-only

trait.Try

#![feature(try_trait_v2)]
use std::ops::Try;
fn simple<T: Copy, R: Try<Output = T>>(value:T) -> (impl Try<Output = T>,impl Try<Output = T>){
     (Some(value),Ok::<T,Box<dyn std::error::Error>>(value))
}
fn main(){}

Newtype можно использовать, чтобы скрыть детали представления, давая точные обещания клиенту. Например, рассмотрим функцию, my_transform которая возвращает составной тип итератора.

use std::iter::{Enumerate, Skip};
pub fn my_transform<I: Iterator>(input: I) -> Enumerate<Skip<I>> {
       input.skip(3).enumerate()
}
fn main() {}

Мы хотим скрыть этот тип от клиента, чтобы клиент видел возвращаемый тип примерно Iterator<Item = (usize, T)>. Мы можем сделать это с помощью шаблона newtype: Клиент не знает, как создается или представляется итератор результата, что означает, что представление может измениться в будущем без нарушения клиентского кода.

use std::iter::{Enumerate, Skip};
pub struct MyTransformResult<I>(Enumerate<Skip<I>>);

impl<I: Iterator> Iterator for MyTransformResult<I> {
   type Item = (usize, I::Item);
   fn next(&mut self) -> Option<Self::Item> {
      self.0.next()
   }
}
pub fn my_transform<I: Iterator>(input: I) -> MyTransformResult<I> {
   MyTransformResult(input.skip(3).enumerate())
}

// impl Trait функция, которая более лаконична, чем шаблон newtype, но с некоторыми дополнительными компромиссами, а именно: impl Trait вы ограничены в том, что вы можете выразить. 
pub fn my_transform<I: Iterator>(input: I) -> impl Iterator<Item = (usize, I::Item)> {
   input.skip(3).enumerate()
}
fn main() {

}

Механизм определения версии полиморфного объекта т.е. диспетчеризация ( статическая и динамическая)

Динамическая диспетчеризация через типажи-объекты (В ООП называют инъекцией зависимостей)

&Trait или Box<Trait> уже не является трейт-обьектами, только &dyn Trait и Box<dyn Trait> - обычные контейнеры для trait object

Trait object — это значение, у которого тип известен только во время выполнения, но которое гарантирует реализацию определённого трейта.

В нём два компонента:

  • Data pointer — указатель на конкретный объект
  • Vtable pointer — указатель на таблицу методов для этого объекта (виртуальная таблица)

Абстракция без накладных расходов: особенности в Rust

traits

Static dispatch (early binding)

Статическая отправка (также называемая «ранним связыванием») Static dispatch ("early binding") происходит только во время компиляции.

Компилятор генерирует отдельный код для каждого конкретного используемого типа. В Rust статическая диспетчеризация является способом полиморфизма по умолчанию и вводится просто через дженерики (параметрический полиморфизм): MyType<T, S, F>.

Dynamic dispatch (late binding)

Динамическая отправка (иногда называемая «поздней привязкой») происходит во время выполнения. Конкретный используемый тип стирается во время компиляции , поэтому компилятор не знает его и поэтому генерирует vtable вызов диспетчеризации во время выполнения, что приводит к снижению производительности. В Rust динамическая диспетчеризация вводится через объекты-типы: &dyn MyTrait, Box<dyn MyTrait>.

Вам придется использовать динамическую отправку в ситуациях, когда требуется стирание типа. Если проблему можно решить с помощью статической диспетчеризации, лучше сделать это, чтобы избежать снижения производительности. Самый распространенный пример, когда вы не можете использовать статическую отправку и вынуждены использовать динамическую отправку, — это гетерогенные коллекции (где каждый элемент потенциально представляет собой отдельный конкретный тип, но каждый из них реализует MyTrait).

dispatch - механизм определения конкретного объекта при абстракции

Если проблему можно решить с помощью static dispatch (статический механизм), вам лучше сделать это, чтобы избежать штрафов производительности определения типа во время выполнения. Наиболее распространенным примером, когда вы не можете использовать static dispatch и должны использовать dynamic dispatch, являются гетерогенные коллекции (где каждый элемент потенциально отличается от конкретного типа, но каждый реализует MyTrait)

Когда тип &dyn Traite (типаж-объект) то компилятор не знает при компиляции какой конкретный тип реализуемый этим типажем, так же типаж-объект то происходит динамическая диспетчеризация, иначе статическая диспетчеризация когда компилятор при компиляции знает точно какой тип используется

dyn Ttrait

Признак должен соответствовать особым требованиям безопасности объекта :

  • Черта не может требовать Self: Sized.
  • Метод ссылается на Self тип в своих аргументах или возвращаемом типе.
  • Метод имеет параметры универсального типа.
  • Метод не имеет получателя.
  • Признак не может содержать связанные константы.
  • Признак не может использоваться Self в качестве параметра типа в списке суперпризнаков.

Trait object Безопасность объекта

Object Safety

Николас Мацакис: Особенности асинхронности Dyn, часть 2

Static dispatch (также называемая «раннее связывание») происходит только во время компиляции. Компилятор генерирует отдельный код для каждого используемого конкретного типа. В [Rust] статическая отправка является способом default для полиморфизма и вводится просто с помощью дженериков (параметрический полиморфизм): MyType <T, S, F>.

Этот процесс, называемый мономорфизацией, в первую очередь происходит с дженериками. Так как все реализуемые типы уже известны то компилятор создать для них свои функции

Подход Rust имеет то преимущество, что объект не должен хранить указатель vtable, если он никогда не используется в полиморфном контексте

Мономорфизация - это процесс превращения обобщённого кода в конкретный код во время компиляции, при котором из кода с обобщёнными типами генерируется код содержащий конкретные типы которые могут встретиться в вашем приложении. Благодаря чему код не становиться медленнее чем был без обобщения. По сути ради уменьшения дублирования делаем обобщение а Rust обратно его разворачивает в конкретные реализации для нужных типов.

И у мономорфизации есть обратная сторона: генерируется больше кода, поэтому, если вы используете универсальные функции с большим количеством разных типов, ваш двоичный файл может раздуться.

Трейты могут быть отправлены статически . Как и в случае с шаблонами C++, компилятор может генерировать отдельную копию абстракции для каждого способа ее создания.

Dynamic dispatch (иногда называемая «поздним связыванием») происходит во время выполнения. Конкретный используемый тип стирается во время компиляции, поэтому компилятор не знает его, поэтому генерирует vtable, который отправляет вызов во время выполнения, но приводит к снижению производительности. В [Rust] динамическая отправка вводится через [trait objects]: &dyn MyTrait.

Вы должны использовать [dynamic dispatch] в ситуациях, когда требуется [стирание типа] . Если проблема может быть решена с помощью [статической отправки] , вам лучше сделать это, чтобы избежать штрафов за производительность. Наиболее распространенный пример, когда вы не можете использовать [static dispatch] и должны использовать [динамическую отправку] , - это heterogeneous коллекции (где каждый элемент потенциально является отдельным конкретным типом, но каждый из них реализует MyTrait).

Правила делающие типажи-объекты безопасными если трейт:

  • не возвращать из методов Self
  • нет обобщенных типов параметров методов

Объекты-объекты должны быть безопасными для объекта, поскольку, как только вы использовали объект-объект, Rust больше не знает конкретного типа, реализующего эту черту. Если метод признака возвращает конкретный Self тип, но объект-признак забывает точный тип, который Self не существует, метод не может использовать оригинальный конкретный тип. То же самое относится к параметрам типового типа, которые заполняются конкретными параметрами типа при использовании признака: конкретные типы становятся частью типа, реализующего этот признак. Когда тип забывается с помощью трейт-объекта, нет способа узнать, какие типы должны заполнять параметры типового типа. Пример Vec<T> у него нет Clone из-за того что если б был то при возврате Self он бы затер свой тип

#![feature(trait_upcasting)]

upcasting-in-future-versions-of-rust

#![feature(trait_upcasting)] предоставляет мощный инструмент для работы с трейт-объектами в Rust, упрощая преобразования между ними и делая код более гибким и выразительным. upcasting в контексте Rust означает преобразование ссылок или умных указателей от одного трейт-объекта к другому, который является супертрейтом. Это аналогично концепции наследования в объектно-ориентированных языках, где подкласс может быть приведён к суперклассу.


#![feature(trait_upcasting)]

trait A {
    fn foo(&self);
}

trait B: A {
    fn bar(&self);
}

struct MyStruct;

impl A for MyStruct {
    fn foo(&self) {
        println!("A::foo");
    }
}

impl B for MyStruct {
    fn bar(&self) {
        println!("B::bar");
    }
}

fn main() {
    let my_struct = MyStruct;
    let b: &dyn B = &my_struct;
    let a: &dyn A = b as &dyn A; // Апкастинг с помощью `as` // b.upcast();
    a.foo(); // Вывод: A::foo
}

&Trait или Box<Trait> уже не является трейт-обьектами, только &dyn Trait и Box<dyn Trait>


trait Foo {
    fn method(&self) -> String;
}
impl Foo for u8 {
    fn method(&self) -> String { format!("u8: {}", *self) }
}
impl Foo for String {
    fn method(&self) -> String { format!("string: {}", *self) }
}
/*
Статическая диспетчеризация позволяет встраивать вызовы функций, потому что вызываемый объект известен во время компиляции.
Мономорфизация:При компиляции Rust создаст конкретные типы generic согласно вызовам в коде для u8 и String
fn static_dispatch_u8(x: u8) {
    x.method();
}

fn static_dispatch_string(x: String) {
    x.method();
}
*/
fn static_dispatch(x: &T) {
    println!("Static dispatch: {}",x.method());
}
/*
Динамическая диспетчеризация - точный тип может быть известен только во время выполнения.
`type erasure` стрирание оригинально типа.
&Trait или Box уже не является трейт-обьектами, только `dyn Trait`
*/
fn dynamic_dispatch(x: &dyn Foo) {
    println!("Dynamic dispatch: {}",x.method());
}
fn main() {
    let x = 5u8;
    let y = "Hello".to_string();

    static_dispatch(&x);
    static_dispatch(&y);

    dynamic_dispatch(&x as &dyn Foo);
    dynamic_dispatch(&y as &dyn Foo);

    let buf:Vec<&dyn Foo> = vec![&x as &dyn Foo,&y as &dyn Foo];
    for b in buf.iter(){
        dynamic_dispatch(*b);
    }
    for b in buf{
        dynamic_dispatch(&*b);
    }
}

Оптимизация от dynamic к static dispatch для набора закрытых типов Для помощи преобразования в enum crate enum_dispatch

Динамически-статическая оптимизация для набора закрытых типов

rustcamp/1_6_dispatch

enum_dispatch

В ситуациях, когда вам нужно иметь дело с разными типами, но все возможные типы образуют закрытый набор (вы знаете все используемые типы), динамическую отправку можно заменить статической отправкой по цене некоторого enum шаблона.

Пример динамической диспетчеризации


trait SayHello {
    fn say_hello(&self);
}
struct English;
impl SayHello for English {
    fn say_hello(&self) {
        println!("Hello!")
    }
}
struct Spanish;
impl SayHello for Spanish {
    fn say_hello(&self) {
        println!("Hola!")
    }
}
fn main(){
 // Здесь мы должны использовать трейт-объект, чтобы содержать разные типы.
 let greetings: Vec> = vec![
    Box::new(English),
    Box::new(Spanish),
 ];
}

Может быть переработан следующим образом (насколько мы знаем , что только English и Spanish будут использоваться типы): Перепишем для статической диспетчеризации


trait SayHello {
    fn say_hello(&self);
}
struct English;
impl SayHello for English {
    fn say_hello(&self) {
        println!("Hello!")
    }
}
struct Spanish;
impl SayHello for Spanish {
    fn say_hello(&self) {
        println!("Hola!")
    }
}
enum Language {
    English(English),
    Spanish(Spanish),
}
impl SayHello for Language {
    fn say_hello(&self) {
        match self {
            Language::English(l) => l.say_hello(),
            Language::Spanish(l) => l.say_hello(),
        }
    }
}
fn main(){
 // Мы содержим разные типы без использования типовых объектов.
 let greetings: Vec = vec![English, Spanish];
}

Заимствовать тип как черту dyn

6-things-you-can-do-with-the-cow-in-rust


use std::borrow::{Borrow, Cow};
use std::fmt::Debug;
use std::ops::Deref;

trait MyTrait: Debug {
    fn data(&self) -> &str;
}

#[derive(Debug)]
struct MyString {
    data: String
}
impl MyTrait for MyString {
    fn data(&self) -> &str {
        &self.data
    }
}
impl<'a> Borrow for MyString {
    fn borrow(&self) -> &(dyn MyTrait + 'a) {
        self
    }
}
impl ToOwned for dyn MyTrait {
    type Owned = MyString;

    fn to_owned(&self) -> MyString {
        MyString {
            data: self.data().to_owned()
        }
    }
}
fn main()  {
    let data = MyString { data: "Hello world".to_owned() };

    let borrowed_cow: Cow<'_, dyn MyTrait> = Cow::Borrowed(&data);
    println!("{:?}", borrowed_cow);

    let owned_cow: Cow<'_, dyn MyTrait> = Cow::Owned(data);
    println!("{:?}", owned_cow);
   // --------------------------------------------------------------------------
    let data = MyString { data: "Hello world".to_owned() };
    let cow1: Cow<'_, dyn MyTrait> = Cow::Borrowed(&data);

    let data = MyString { data: "Hello world".to_owned() };
    let cow2: Cow<'_, dyn MyTrait> = Cow::Owned(data);

    let mut vector: Vec> = vec![cow1, cow2];
}

Пример static dinamyc dispatch

Тестирование generic структуры способами static и dinamyc dispatch

#![allow(dead_code)]
mod queue {
    pub const SIZE_ARRAY: usize = 5;
    #[derive(Debug)]
    pub struct Queue<T> {
        pub value: [T; SIZE_ARRAY],
        index: usize,
    }
    impl<T> Queue<T> {
        pub fn push(&mut self, value: T) -> bool {
            if self.index < SIZE_ARRAY - 1 { self.value[self.index] = value;self.index += 1; return true; } return false;
        }
        pub fn pop(&mut self) -> Option<T> where T: Clone {
            if self.index > 0 { self.index -= 1; return Some(self.value[self.index].clone()); } return None;
        }
        pub fn new(value: [T; SIZE_ARRAY]) -> Self {
            Queue { value: value, index: Default::default() }
        }
    }
    #[cfg(test)]
    mod test {
        use super::*;
        trait Base: std::fmt::Debug {}
        impl Base for i32 {}
        impl Base for bool {}

        #[derive(Debug)]
        struct Item {  data: i32 }
        impl Base for Item {}

        #[test]
        fn test_queue() {
            // Test dynamic dispatch
            let arr: [&dyn Base; SIZE_ARRAY] = [&false; SIZE_ARRAY];
            let mut buffer: Queue<&dyn Base> = Queue::new(arr);

            buffer.push(&true);
            buffer.push(&Item { data: 4 });
            buffer.push(&5i32);

            if let Some(_var) = buffer.pop() {  assert!(true);} else {  assert!(false); }

            // Test static dispatch
            let arr: [i32; SIZE_ARRAY] = [0i32; SIZE_ARRAY];
            let mut buffer: Queue<i32> = Queue::new(arr);

            buffer.push(4);
            buffer.push(5);
            if let Some(var) = buffer.pop() { assert_eq!(5, var); } else { assert!(false); }
    }
   }
}
fn main(){}

Динамическая диспетчеризация на стеке

idioms/on-stack-dyn-dispatch


use std::io;
use std::fs;

// Каждая переменная содержит значения только одного типа. В нашем примере stdin имеет тип Stdin, file имеет тип File и readable имеет тип &mut dyn Read
fn main(){
 let (mut stdin_read, mut file_read);

 let readable: &mut dyn io::Read = if arg == "-" {
    stdin_read = io::stdin();
    &mut stdin_read
 } else {
    file_read = fs::File::open(arg)?;
    &mut file_read
 };
}

Преимущества

Нам не нужно ничего размещать в куче. Нам также не нужно инициализировать что-то, что мы не будем использовать позже, и нам не нужно мономорфизировать весь следующий код для работы с обоими File или Stdin.


Версия аналогичного кода но с выделением памяти в куче:


fn main(){
 let readable: Box = if arg == "-" {
    Box::new(io::stdin())
 } else {
    Box::new(fs::File::open(arg)?)
 };
}

1. Пример static dinamyc dispatch

У нас есть два типа ошибок ErrorOne и ErrorTwo

Мы можем использовать их как static dispatch и как dinamyc dispatch

rust-polymorphism-dyn-impl-trait-objects-for-testing-and-extensibiity

use std::error::Error;
use std::fmt::Display;
// ErrorOne.
mod error_one {
    use super::*;
    #[derive(Debug)]
    pub struct ErrorOne;
    impl Display for ErrorOne {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(f, "ErrorOne")
        }
    }
    impl Error for ErrorOne {}
}
use error_one::ErrorOne;

// ErrorTwo.
mod error_two {
    use super::*;
    #[derive(Debug)]
    pub struct ErrorTwo;
    impl Display for ErrorTwo {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(f, "ErrorTwo")
        }
    }
    impl Error for ErrorTwo {}
}
use error_two::ErrorTwo;
fn main(){}

2. Пример static dinamyc dispatch

Static dispatch.

У нас есть два типа ошибок ErrorOne и ErrorTwo

Мы можем использовать их как static dispatch и как dinamyc dispatch

rust-polymorphism-dyn-impl-trait-objects-for-testing-and-extensibiity

// Static dispatch.
mod static_dispatch {
    use super::*;
    mod receives {
        use super::*;
        pub fn accept_error<E: Error>(error: E) {
            println!("Handling ErrorOne Debug: {:?}", error);
            println!("Handling ErrorOne Display: {}", error);
        }
        pub fn accept_error_with_syntactic_sugar(error: impl Error) {
            println!("Handling ErrorOne Debug: {:?}", error);
            println!("Handling ErrorOne Display: {}", error);
        }
    }
    mod returns {
        use super::*;
        pub fn return_error_one() -> ErrorOne {
            ErrorOne
        }
        pub fn return_error_two() -> ErrorTwo {
            ErrorTwo
        }

        // 🚨 DOES NOT WORK! Need dynamic dispatch.
        // pub fn return_single_error() -> impl Error {
        //     if random_bool() {
        //         ErrorOne
        //     } else {
        //         ErrorTwo
        //     }
        // }

        pub fn return_single_error() -> impl Error {
            return ErrorOne;
        }
    }
}
fn main(){}

3. Пример static dinamyc dispatch

Dynamic dispatch.

У нас есть два типа ошибок ErrorOne и ErrorTwo

Мы можем использовать их как static dispatch и как dinamyc dispatch

rust-polymorphism-dyn-impl-trait-objects-for-testing-and-extensibiity

// Dynamic dispatch.
mod dynamic_dispatch {
    use super::*;
    mod receives {
        use super::*;
        pub fn recieve_error_by_ref(error: &dyn Error) {
            println!("Handling Error Debug: {:?}", error);
            println!("Handling Error Display: {}", error);
        }
        pub fn example_1() {
            let error_one = ErrorOne;
            recieve_error_by_ref(&error_one);
            let error_two = ErrorTwo;
            recieve_error_by_ref(&error_two);
        }
        pub fn receive_error_by_box(error: Box<dyn Error>) {
            println!("Handling Error Debug: {:?}", error);
            println!("Handling Error Display: {}", error);
        }
        pub fn example_2() {
            let error_one = ErrorOne;
            let it = Box::new(error_one);
            receive_error_by_box(it);
            let error_two = ErrorTwo;
            receive_error_by_box(Box::new(error_two));
        }
        pub fn receive_slice_of_errors(arg: &[&dyn Error]) {
            for error in arg {
                println!("Handling Error Debug: {:?}", error);
                println!("Handling Error Display: {}", error);
            }
        }
    }
    mod returns {
        use super::*;
        pub fn return_one_of_two_errors() -> Box<dyn Error> {
            if random_bool() {
                Box::new(ErrorOne)
            } else {
                Box::new(ErrorTwo)
            }
        }
        pub fn return_one_of_two_errors_with_arc() -> std::sync::Arc<dyn Error> {
            if random_bool() {
                std::sync::Arc::new(ErrorOne)
            } else {
                std::sync::Arc::new(ErrorTwo)
            }
        }
        pub fn return_slice_of_errors() -> Vec<&'static dyn Error> {
            let mut errors: Vec<&dyn Error> = vec![];
            if random_bool() {
                errors.push(&(ErrorOne));
            } else {
                errors.push(&(ErrorTwo));
            }
            errors
        }
        pub fn mut_vec_containing_different_types_of_errors(mut_vec: &mut Vec<&dyn Error>) {
            mut_vec.push(&ErrorOne);
            mut_vec.push(&ErrorTwo);
        }
    }
}
fn main(){}

Оптимизация уменьшения раздувания кода

crate momo

momo

llogiq/momo

Статическая отправка с параметрами типа имеет обратную сторону, заключающуюся в генерации довольно большого количества кода (для каждого типа), раздувании двоичного размера и потенциально пессимизирующем использовании кэша выполнения. Однако часто дженерики нужны не для скорости, а для эргономики.

Каноническое решение этой проблемы состоит в том, чтобы исключить внутренний метод, который содержит весь код за вычетом общих преобразований, и оставить внешний метод как оболочку.

Например:

pub  fn  this <I: Into<String>> (i: I) -> usize {
     // делаем что-то действительно сложное с `i.into ()` 
    // потенциально занимающее несколько страниц кода 
}

Становится:

#[inline]
pub fn this<I: Into<String>>(i: I) -> usize {
    _this_inner(i.into())
}

fn _this_inner(i: String) -> usize {
    // тот же код, что и выше, без преобразования
    ..
}

Это гарантирует, что только преобразование будет мономорфным, что приведет к более компактному коду и повышению производительности во время компиляции. Обратной стороной является то, что это обременяет код случайной сложностью, потому что читатели должны следовать косвенным указаниям. Можем ли мы сделать лучше?

use momo::momo;

#[momo]
pub fn this<I: Into<String>>(i: I) -> usize {
    // делаем что-то действительно сложное с `i.into()` 
    // потенциально занимающий несколько страниц кода
    ..
}

Это заставляет всю внутреннюю функцию танцевать для вас, позволяя вам сосредоточиться на своем коде, наслаждаясь быстрой компиляцией, не мешая. momo обрабатывает три преобразования:

Into<T>: внутренняя функция получает простой T

AsRef<T>: внутренняя функция получает &T

AsMut<T>: внутренняя функция получает &mut T

Обратите внимание, что momo пока работает только с простыми функциями.

В языке программирования Rust понятия подтипирования (Subtyping) и вариантности (Variance) играют важную роль, особенно при работе с обобщёнными типами (generics) и временем жизни (lifetimes).

В традиционных объектно-ориентированных языках, таких как Java или C#, подтипирование позволяет создавать иерархии типов, где один тип (подтип) наследует свойства другого (супертип). В Rust же подтипирование ограничено и отличается от классических ООП-подходов.

Подтипирование (Subtyping) по времени жизни (Lifetime Subtyping)

В Rust подтипирование применяется главным образом к времени жизни. Если у вас есть два времени жизни 'a и 'b, и 'a длится меньше или равно 'b (то есть 'a: 'b), то ссылка с временем жизни 'a может использоваться там, где ожидается ссылка с временем жизни 'b

// Здесь `'a` должно быть достаточно длинным, чтобы покрывать оба параметра x и y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
fn main(){}

Отсутствие классического подтипирования для типов

Rust не поддерживает наследование типов в традиционном смысле.

Вместо этого Rust использует трейты (traits) для реализации полиморфизма.

Трейты позволяют определять общие поведения, которые могут быть реализованы различными типами.

Вариантность (Variance)

Важность понимания вариантности Понимание вариантности важно для безопасного использования обобщённых типов и ссылок с различными временами жизни. Это помогает избежать ошибок, связанных с жизненным циклом данных и гарантировать, что ссылки не станут висячими.

Вариантность описывает, как отношения подтипов между типами-аргументами отражаются на подтипах самих обобщённых типов. В Rust есть три основные категории вариантности

Ковариантность (Covariance)

Ковариантность применяется к ссылкам и другим типам, которые только читают данные. Если тип T является подтипом типа U, то обобщённый тип Container<T> является подтипом Container<U>


struct Container<'a, T> {
    reference: &'a T,
}
// Если 'short: 'long (то есть 'short живёт дольше 'long)
// Тогда Container<'short, T> является подтипом Container<'long, T>

fn main() {
    let s: &'static str = "Hello";
    let s_short: &str = s; // &'static str: подтип &str

    let container_static: Container<'static, str> = Container { reference: s };
    let container_short: Container<'short, str> = container_static; // Ковариантность позволяет это
}

Пример:

Если у вас есть два времени жизни: 'short и 'long, где 'short: 'long (то есть 'short меньше или равно 'long), и тип Container<'a, T> ковариантен по 'a, тогда: Container<'short, T> будет подтипом Container<'long, T> Это означает, что контейнер с более коротким временем жизни может быть использован там, где ожидается контейнер с более длинным временем жизни, но не наоборот.

И пример ковариантность через типы функций Ковариантность в Rust проявляется, когда обобщённый тип может быть подтипом другого обобщённого типа, если их параметры также находятся в отношениях подтипирования. Вот пример, не связанный с ссылками, использующий ковариантность через типы функций, которые ковариантны по возвращаемому значению:

struct Animal; struct Cat;

impl Animal { fn speak(&self) { println!("Animal speaking"); } } impl Cat { fn speak(&self) { println!("Meow"); } } fn get_animal() -> Animal { Animal } fn get_cat() -> Cat { Cat } fn take_animal_maker(f: fn() -> Animal) { let animal = f(); animal.speak();// вот суть ковариантности, используется общая функция я этих типов } fn main() { take_animal_maker(get_animal); // Animal speaking take_animal_maker(get_cat); // Meow (Cat можно использовать вместо Animal) }


Инвариантность (Invariance)

Инвариантность применяется, если обобщённый тип может как читать, так и изменять данные. Обобщённый тип Container<T> не является ни подтипом, ни супертипом Container<U>, даже если T является подтипом U. Большинство обобщённых типов в Rust являются инвариантными, если только они явно не ковариантны или контравариантны.


Контравариантность (Contravariance)

Контравариантность редко встречается напрямую, но может проявляться в некоторых контекстах, связанных с функциями. Если тип T является подтипом типа U, то обобщённый тип Handler<U> является подтипом Handler<T> Rust в основном не использует контравариантность, но это важно понимать для некоторых случаев, связанных с функциями и трейты

Ковариантность

Контравариантность

Инвариантность

Что такое вариативность

Для типа T<'a>:

  • Ковариантный по 'a → можно заменить 'a на более короткое время жизни
  • Контравариантный → можно заменить 'a на более длинное
  • Инвариантный → замены запрещены

Ковариантность (самый частый случай)

#![allow(unused)]
fn main() {
struct Foo<'a> {
    x: &'a i32,
}
}

Foo<'a> ковариантен по 'a, потому что:

  • &'a T ковариантен по 'a
  • поле только читает данные

Foo<'short> можно использовать там, где ожидается Foo<'long>

Инвариантность (важно!)

#![allow(unused)]
fn main() {
struct Bar<'a> {
    x: &'a mut i32,
}
}

Bar<'a> инвариантен по 'a

Почему?

  • &mut Tинвариантен
  • потому что позволяет и читать, и писать

Это защищает от нарушения правил заимствования.

Контравариантность (редко, но есть)

Контравариантность возникает в аргументах функций:

#![allow(unused)]
fn main() {
type FnType<'a> = fn(&'a i32);
}

FnType<'a> контравариантен по 'a

Интуиция:

  • функция, которая принимает &'static i32, может быть использована там, где ожидается функция, принимающая &'short i32

Но:

  • на практике это редко заметно
  • чаще встречается внутри сложных типов (Fn, dyn Trait)

Важный момент

В Rust нельзя явно написать:

«сделай этот тип ковариантным»

Но можно управлять вариативностью через поля, например:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

struct Covariant<'a, T> {
    _marker: PhantomData<&'a T>,
}
}

PhantomData — основной инструмент для этого в unsafe / FFI / low-level коде.