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

Историческая справка

В Rust Book, в 17 главе приводят: «Если язык должен иметь наследование, чтобы быть объектно‑ориентированным, то Rust таким не является. Здесь нет способа определить структуру, наследующую поля и реализации методов родительской структуры, без использования макроса» . Наследовать поля и правда невозможно, чего не скажешь о реализациях, и используя знания из первой части мы можем построить Get/Set для доступа к этим самым полям, хоть и не без проблем вытекающих из ООП.

Белая магия

Начнем с маленького ядра нашей логики, объявляем очень простой трейт, наверное, в большей его части знакомый всем Rust-программистам:

trait GetSet<Value> {
    type Source: GetSet<Value>;

    fn source(&mut self) -> &mut Self::Source;

    fn get(&mut self) -> &mut Value {
        self.source().get()
    }

    fn set(&mut self, value: Value) {
        self.source().set(value)
    }
}
  1. Объявляем трейт принимающий любой тип данных к которому желаем «подключиться»

  2. В качестве источника данных выступает любая структура данных, также реализующая этот трейт.Parent из первой части был заменен на Source, в этом контексте «источник» выглядит более подходящим

  3. Функция указывающая на источник данных, его реализовывать мы будем самостоятельно

  4. Единственное, конечно, что выбивается из общей парадигмы, это вызов источника (родителя) в качестве стандартной логики для get/set, который может вызываться бесконечно и привести к панике, если не будет найдена конечная реализация

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

// Ядро нашей логики
trait GetSet<Value> {
    type Source: GetSet<Value>;

    fn source(&mut self) -> &mut Self::Source;

    fn get(&mut self) -> &mut Value {
        self.source().get()
    }

    fn set(&mut self, value: Value) {
        self.source().set(value)
    }
}


// Контейнер каких-то данных
#[derive(Debug)]
struct Container {
    value: u32,
}


// Структура владеющая какими-нибудь компонентами/контейнерами данных
struct ExternalBridge {
    container: Container,

    // Чтобы пример был чуть более круче, здесь будет счет обращений к контейнеру 
    access_count: u32,
}

// Конструктор с нулевым счетчиком обращений
impl ExternalBridge {
    fn new(container: Container) -> Self {
        Self { container, access_count: 0 }
    }
}

// Рекомендуется покрывать все рекурсивные методы для Source = Self реализаций
impl GetSet<Container> for ExternalBridge {
    type Source = Self;

    // Источником данных является сама структура (Self), ее мы и возвращаем
    fn source(&mut self) -> &mut Self::Source {
        self
    }

    // Возвращаем этот самый контейнер и обновляем счетчик
    fn get(&mut self) -> &mut Container {
        self.access_count += 1;
        &mut self.container
    }

    // Вмешиваемся в процесс и назначаем собственное значение каждому контейнеру
    fn set(&mut self, mut value: Container) {
        value.value = 666;

        // Можно было бы и *self.get() = value
        self.container = value;
    }
}


// Какая-то надструктура, для примера
struct SuperExternedBridge {
    external: ExternalBridge,
}

impl GetSet<Container> for SuperExternedBridge {
    type Source = ExternalBridge;

    // Указываем источник контейнера
    fn source(&mut self) -> &mut Self::Source {
        &mut self.external
    }

    // реализовывать get/set больше не требуется,
    //   если только нет желания перегрузить вызовы
}


// Мягкий тест на полиморфизм, принимает любой get/set контейнера
fn soft_polymorph_test(container: &mut impl GetSet<Container>) {
    println!("{:?}", container.get());
}

// Строгий тест, принимает только тех, у кого источником выступает ExternalBridge 
fn strong_polymorph_test<C: GetSet<Container, Source = ExternalBridge>>(container: &mut C) {
    println!("{:?}", container.get());
}


fn main() {
    let mut bridge = SuperExternedBridge {
        external: ExternalBridge::new(
            Container { value: 13 }
        )
    };

    // Но значение будет 666, так как у нас собственный set
    bridge.set(Container { value: 0 });

    // Структура пройдет оба теста
    soft_polymorph_test(&mut bridge);
    strong_polymorph_test(&mut bridge);

    // Выводим количество обращений к контейнеру
    println!("{}", bridge.external.access_count);
}
  1. Для конечного «клиента» нет разницы насколько глубока кроличья нора, можете даже убрать SuperExternedBridge из объявления переменной, только начнет ругаться последний принт — можно было бы и вынести счетчик в контейнер, но для наглядности роли посредника (Middleware) он остался в ExternalBridge.

  2. Для последующих обертках, требуется только указать источник данных, не заботясь о реализации get/set.

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

  4. Вы можете убрать set элемент и &mut для доступа только по-чтению.

  5. Компилятор не предупреждает об отсутствии конечной точки, следует знать об этом.

Добавляем именованные контейнеры данных

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

Черная магия

#![feature(adt_const_params)]


trait Throughfield<const NAME: &'static str, Value> {
    type Source: Throughfield<NAME, Value>;

    fn source(&mut self) -> &mut Self::Source;
    
    fn get(&mut self) -> &mut Value {
        self.source().get()
    }

    fn set(&mut self, value: Value) {
        self.source().set(value)
    }
}

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

// Для строк в константах, но их можно заменить структурами-тэгами
#![feature(adt_const_params)]


// Ядро именованной логики
trait Throughfield<const NAME: &'static str, Value> {
    type Source: Throughfield<NAME, Value>;

    fn source(&mut self) -> &mut Self::Source;
    
    fn get(&mut self) -> &mut Value {
        self.source().get()
    }

    fn set(&mut self, value: Value) {
        self.source().set(value)
    }
}


// В этот раз счетчик внутри контейнера
struct Container {
    value: u32,
    access_count: u32,
}

impl Container {
    fn new(value: u32) -> Self {
        Self { value, access_count: 0 }
    }
}

// Теперь благодаря имени можно иметь несколько реализаций для одного типа данных
impl Throughfield<"value", u32> for Container {
    type Source = Self;

    fn source(&mut self) -> &mut Self::Source {
        self
    }

    // Геттер со счетчиком обращений, как во всех ООП учебниках
    fn get(&mut self) -> &mut u32 {
        self.access_count += 1;
        &mut self.value
    }

    // Свообразный логгер, тоже как во всех учебниках
    fn set(&mut self, value: u32) {
        println!("Устанавливаем значение: {}", value);
        self.value = value
    }
}


struct ExternalBridge {
    container: Container,
}

// Контейнер уже реализует сквозной доступ к полю, поэтому остается указать только источник
impl Throughfield<"value", u32> for ExternalBridge {
    type Source = Container;

    fn source(&mut self) -> &mut Self::Source {
        &mut self.container
    }
}

// Но мы можем также организовать доступ к самому контейнеру
impl Throughfield<"container", Container> for ExternalBridge {
    type Source = Self;

    fn source(&mut self) -> &mut Self::Source {
        self
    }

    fn get(&mut self) -> &mut Container {
        &mut self.container
    }

    fn set(&mut self, value: Container) {
        self.container = value;
    }
}


struct SuperExternedBridge {
    external: ExternalBridge,
}

// Для надструктуры остается только указать источник данных
impl Throughfield<"value", u32> for SuperExternedBridge {
    type Source = ExternalBridge;

    // Даже не нужно указывать конечный источник данных, только на узел ниже
    fn source(&mut self) -> &mut Self::Source {
        &mut self.external
    }
}

// Идентичная структура и для доступа к контейнеру, отличается только название и тип данных
impl Throughfield<"container", Container> for SuperExternedBridge {
    type Source = ExternalBridge;

    // Тот же самый источник, что и для доступа к value в контейнере
    fn source(&mut self) -> &mut Self::Source {
        &mut self.external
    }
}


fn soft_polymorph_test(container: &mut impl Throughfield<"value", u32>) {
    println!("{}", container.get());
}

fn strong_polymorph_test<C: Throughfield<"value", u32, Source = ExternalBridge>>(container: &mut C) {
    println!("{}", container.get());
}


fn main() {
    let mut bridge = SuperExternedBridge {
        external: ExternalBridge {
            container: Container::new(13)
        }
    };

    // В случаях наличия одного сквозного поля, что маловероятно
    bridge.set(666);

    // В случае множества сквозных полей одного типа, вероятнее всего
    Throughfield
        ::<"value", u32>
        ::set(&mut bridge, 666);

    soft_polymorph_test(&mut bridge);
    strong_polymorph_test(&mut bridge);

    // Красиво достаем счетчик из геттера, так как у нас есть доступ к контейнеру
    let Container { access_count, .. } = bridge.get();

    println!("{access_count}");
}
  1. С таким подходом у нас появляется возможность именовать возвращаемые типы, хоть и не без последствий. Теперь при наличии множества сквозных полей одного типа, требуется явно указывать используемую реализацию. Rust, как-никак, ориентирован на безопасную работу с данными, поэтому и рекомендую использовать первый вариант.

  2. Мы также можно убрать SuperExternedBridge, это никак не повлияет на дальнейшую работу get/set, единственное только появится несоответствие с жестким тестом, из-за того что он требовал конкретный источник данных.

Бонус

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

trait Uroboros {
    type Source: Uroboros;

    fn eat() {
        Self::Source::eat();
    }
}


struct Head;

impl Uroboros for Head {
    type Source = Tail;
}


struct Tail;

impl Uroboros for Tail {
    type Source = Head;
}


fn main() {
    Head::eat();
}

В заключении

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

Комментарии (1)


  1. PsyHaSTe
    14.10.2023 12:40
    +9

    Не могу не одобрять изучение раста, но все же не до конца понятно какую проблему это решает. Проперти в Java/C# решают проблему того, что кто-нибудь может что-нибудь не то случайно намутировать, потоу что там мутабельность — свойство типа, а не переменной. В расте соответственно такой проблемы нет.


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


    pub trait EntityId {
       fn id() -> Id;
    }

    Ну и утилити макрос чтоб в 1 строчку реализовывать его:


    macro_rules! implement_entity_id {
        ($typ:ty) => {
            impl EntityId for $typ {
                fn id(&self) -> Id {
                    self.id
                }
            }
        };
    }