Ржавое наследование 2. Славянский проброс Get/Set
Вторая статья по теме, развивающая мои теоретические выкладки про наследование реализаций из первой части. В этой части пойдет речь о доступе к данным через цепочку вложенных структур. Все также никакой лишней нагрузки в виде: Rc
, RefCell
, и тд, только no_std
и немного nightly в конце. Но чтобы понимать происходящее требуется изучить первую статью: ссыль, хотя и будет предоставлена минимальная вводная.
Историческая справка
В Rust Book, в 17 главе приводят: «Если язык должен иметь наследование, чтобы быть объектно-ориентированным, то Rust таким не является. Здесь нет способа определить структуру, наследующую поля и реализации методов родительской структуры, без использования макроса» . Наследовать поля и правда невозможно, чего не скажешь о реализациях, и используя знания из первой части мы можем построить Get/Set для доступа к этим самым полям, хоть и не без проблем вытекающих из ООП.
Белая магия
Начнем с маленького ядра нашей логики, объявляем очень простой трейт, наверное, в большей его части знакомый всем Rust-программистам:
trait GetSet {
type Source: GetSet;
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 {
type Source: GetSet;
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 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 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) {
println!("{:?}", container.get());
}
// Строгий тест, принимает только тех, у кого источником выступает ExternalBridge
fn strong_polymorph_test>(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 {
type Source: Throughfield;
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 {
type Source: Throughfield;
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>(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-макрос для написания рутины, оставив программистам декларативную часть ООП, зависит только от вашей поддержки!