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