
Вторая статья по теме, развивающая мои теоретические выкладки про наследование реализаций из первой части. В этой части пойдет речь о доступе к данным через цепочку вложенных структур. Все также никакой лишней нагрузки в виде: 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) } }
Объявляем трейт принимающий любой тип данных к которому желаем «подключиться»
В качестве источника данных выступает любая структура данных, также реализующая этот трейт.
Parentиз первой части был заменен наSource, в этом контексте «источник» выглядит более подходящимФункция указывающая на источник данных, его реализовывать мы будем самостоятельно
Единственное, конечно, что выбивается из общей парадигмы, это вызов источника (родителя) в качестве стандартной логики для 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); }
Для конечного «клиента» нет разницы насколько глубока кроличья нора, можете даже убрать
SuperExternedBridgeиз объявления переменной, только начнет ругаться последний принт — можно было бы и вынести счетчик в контейнер, но для наглядности роли посредника (Middleware) он остался вExternalBridge.Для последующих обертках, требуется только указать источник данных, не заботясь о реализации get/set.
В промежуточное звено может быть бесшовно встроен дебаггер, бенчер или взят сторонний аргумент.
Вы можете убрать set элемент и &mut для доступа только по-чтению.
Компилятор не предупреждает об отсутствии конечной точки, следует знать об этом.
Добавляем именованные контейнеры данных
Конечно вы заметили, что у нас все еще нет именованного доступа к примитивным типам, хотя мне и кажется, что этот подход правильнее и лаконичней для экосистемы раста и не вызывает никаких проблем, компилятор легко находит трейт отвечающий за тип. Но для чернокнижников приготовлен следующий раздел с щепоткой 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}"); }
С таким подходом у нас появляется возможность именовать возвращаемые типы, хоть и не без последствий. Теперь при наличии множества сквозных полей одного типа, требуется явно указывать используемую реализацию. Rust, как-никак, ориентирован на безопасную работу с данными, поэтому и рекомендую использовать первый вариант.
Мы также можно убрать
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-макрос для написания рутины, оставив программистам декларативную часть ООП, зависит только от вашей поддержки!
