Ржавое наследование 2. Славянский проброс Get/Set

от автора

Вторая статья по теме, развивающая мои теоретические выкладки про наследование реализаций из первой части. В этой части пойдет речь о доступе к данным через цепочку вложенных структур. Все также никакой лишней нагрузки в виде: 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-макрос для написания рутины, оставив программистам декларативную часть ООП, зависит только от вашей поддержки!


ссылка на оригинал статьи https://habr.com/ru/articles/767416/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *