Я пытаюсь изучить Rust, и выбор реализации черт по отдельности не имеет для меня смысла. Якобы это полезно, поскольку позволяет вам добавить поведение к типу, которым вы не владеете. Но никакой государственности!
Например, представьте, что у меня есть структура Water
в каком-то ящике, которым я не владею. И я хочу реализовать поведение для своей черты Matter
, чтобы воплощать поведение состояний материи.
ящик1
struct Water {
num_moles: u128,
// suppose there's some stuff here about molecular structure
...
}
ящик2
trait Matter {
fn temperature(&self) -> i32;
fn melt(&mut self);
fn freeze(&mut self);
fn evaporate(&mut self);
fn condense(&mut self);
fn get_matter_state(&self) -> str;
}
Теперь я хотел бы реализовать это, чтобы сделать Water
типом Matter
, но для этого мне нужны как переменные состояния, так и постоянные переменные. Но в блоке impl Matter for Water
я этого сделать не могу. Тем не менее, я не могу редактировать структуру Water
, поскольку у меня нет кода. Как мне тогда сохранить текущую температуру?
Связано: stackoverflow.com/a/73163713/5397009
Но никакой государственности!
просто неправильно, хотя вы не можете реализовать эту конкретную черту для любого типа, и crate1::Water
просто не является одним из типов, для которых вы можете ее реализовать.
Вы наверняка могли бы реализовать это с помощью этой альтернативы Water
:
pub struct Water {
pub temperature: i32,
pub state: String,
}
Поэтому исходное утверждение следует перефразировать так:
Не существует состояния, которое не предоставляет внешний тип.
Альтернативно, вы всегда можете добавить произвольное состояние, реализовав Matter
для пользовательского типа оболочки вместо внешнего типа напрямую:
struct MatterialWater {
water: crate1::Water,
temperature: i32,
state: String,
}
В этом ответе рассматривается более общий вопрос о том, как состояние и характеристики могут быть связаны в иерархических отношениях.
Что касается вопроса ОП, шаблон декоратора можно применять к объектам из внешних ящиков, которые имеют функции, которые можно считать полезными, но которые необходимо расширить. Можно создать свою собственную структуру Water
, в которой есть поле, содержащее поля другого ящика Water
, и использовать ее методы в своей собственной impl
.
Остальная часть этого подхода не касается конкретно применения шаблона декоратора к предметам из других ящиков. В более общем плане речь идет о том, как применять шаблон декоратора и использовать возможности языка для достижения иерархии объектов.
Вы можете добиться сохранения состояния в иерархии типов объектов посредством комбинации свойств и структур. Это действительно требует немало накладных расходов; разделение большого проекта на файлы для каждого типа может помочь в управлении им.
Ниже в общих чертах применяется основная идея Шаблона декоратора для достижения целей объектно-ориентированного проектирования, когда «подклассы» наследуют поведение, а также косвенно определяют его.
Идея состоит в том, чтобы иметь структуру, тесно связанную с признаком. Признак определяет нереализованный метод, который возвращает ссылку на связанную с ним структуру состояния, которая реализована «подклассами».
В признаке поведение или методы могут иметь реализации, которые будут автоматически наследоваться «подклассами».
ссылка на игровую площадку ржавчины
// --- canine.rs ---
pub struct CanineState {
is_sleeping: bool,
}
impl CanineState {
pub fn new() -> Self {
Self { is_sleeping: false }
}
}
pub trait Canine {
fn canine_mut(&mut self) -> &mut CanineState;
// Predefined behavior all subclasses can use.
fn sleep(&mut self, b: bool) {
println!("CanineState.is_sleeping = {b}");
self.canine_mut().is_sleeping = b;
}
}
// --- dog.rs ---
pub struct DogState {
// Canine characteristics.
canine_state: CanineState,
// New dog characteristics.
is_barking: bool,
}
impl DogState {
pub fn new() -> Self {
Self { canine_state: CanineState::new(), is_barking: false }
}
// Give subclasses a way to get the superclass' state.
fn canine_mut(&mut self) -> &mut CanineState {
&mut self.canine_state
}
}
// Note the semantics here `Dog: Canine`. This declares that a Dog
// is a Canine.
pub trait Dog: Canine {
// Unimplemented in virtual method.
fn dog_mut(&mut self) -> &mut DogState;
// Virtual behavior with implementation.
fn bark(&mut self, b: bool) {
println!("DogState.is_barking = {b}");
self.dog_mut().is_barking = b;
}
}
// --- pug.rs ---
// A Pug is a Dog which is a Canine.
//
pub struct Pug {
dog_state: DogState,
is_snorting: bool,
}
impl Pug {
pub fn new() -> Self {
Self { dog_state: DogState::new(), is_snorting: false }
}
// Pug has no subclasses, so we can implement its behaviors as
// struct funcs.
pub fn snort(&mut self, b: bool) {
println!("Pug.is_snorting = {b}");
self.is_snorting = b
}
}
// Implement the state accessor and automatically get all predefined
// Dog behaviors.
//
impl Dog for Pug {
// Only need to implement this.
fn dog_mut(&mut self) -> &mut DogState {
&mut self.dog_state
}
}
// Implement the state accessor and automatically get all predefined
// Canine behaviors.
//
impl Canine for Pug {
// Only need to implement this.
fn canine_mut(&mut self) -> &mut CanineState {
self.dog_state.canine_mut()
}
}
fn main() {
let mut pug = Pug::new();
pug.snort(true);
pug.bark(true);
pug.bark(false);
pug.snort(false);
pug.sleep(true);
}
Pug.is_snorting = true
DogState.is_barking = true
DogState.is_barking = false
Pug.is_snorting = false
CanineState.is_sleeping = true
Современная тенденция в ООП заключается в отказе от глубоких и/или сложных иерархий. В прошлом многие объектно-ориентированные проекты страдали от архитектурных недостатков, таких как хрупкий базовый класс. Отношения «использование» являются предпочтительными, если нет веской причины для реализации иерархии. Шаблон «Декоратор» — это тип отношений использования, но он также может стать громоздким, если им злоупотреблять. Хорошая идея — сохранять иерархические отношения поверхностными и простыми.
Каждому, кто утверждает, что Rust не является объектно-ориентированным, я бы возразил, что стандартные библиотеки и популярные пакеты Rust на самом деле являются объектно-ориентированными. Хотя наследование состояний не поддерживается языком напрямую через структуры, вы видите множество примеров наследования поведения через признаки. Сам факт реализации пользовательского итератора с использованием типажа Iterator
дает разработчику множество новых вариантов поведения, унаследованных от интерфейса. В язык встроено множество других объектно-ориентированных функций, помимо простого наследования поведения (полиморфизм/динамическая диспетчеризация).
Является ли Rust «ОО-языком» или нет, может зависеть от того, как человек лично определяет, что такое ОО-язык. Rust не поддерживает напрямую наследование состояний, но в остальном поддерживает основные функции объектно-ориентированного языка. Шаблоны проектирования GoF и другие объектно-ориентированные стратегии можно легко реализовать с помощью Rust.
Если вы новичок в Rust, не отказывайтесь от всех своих стремлений применить знакомые концепции объектно-ориентированного подхода к своим собственным проектам или вообще отказывайтесь от Rust только потому, что кто-то на раннем этапе сказал вам: «Rust — это не объектно-ориентированный подход». . С учетом вышесказанного, Rust — это уникальный и несколько сложный язык, в котором нельзя слепо пытаться привести его в соответствие со своими представлениями о том, как можно применять объектно-ориентированный подход. Существует множество идиоматических способов реализации функций Rust, с которыми следует ознакомиться, прежде чем принимать решения по проектированию.
Я бы настоятельно не советовал никому пытаться применять шаблоны ООП-проектирования к Rust. Эта дорога заканчивается болью. Rust не является объектно-ориентированным языком.
@cdhowie Я категорически с тобой не согласен. Объектно-ориентированное программирование — это проверенный и надежный подход к сложным проектам, и Rust его поддерживает. И Rust ЯВЛЯЕТСЯ объектно-ориентированным языком в такой же степени, как и C++ — оба являются мультипарадигмальными.
Я не понимаю, как решается вопрос ОП... Он реализует признак для типа, который находится в пакете, который им не принадлежит - в этом случае impl for
запрещено
@Todd Rust Book говорит: «Многие конкурирующие определения описывают, что такое ООП, и по некоторым из этих определений Rust является объектно-ориентированным, а по другим - нет». Далее объясняется, что наследование может быть плохим, и как реализовать некоторые шаблоны ООП способом Rust... в целом я бы не согласился с тем, что это объектно-ориентированный язык, но я думаю, он открыт для интерпретации.
@Todd Rust вообще не поддерживает наследование типов значений, что является одним из основных элементов объектно-ориентированной парадигмы. Если Rust — это объектно-ориентированный подход, то в нем отсутствует одна из основных функций объектно-ориентированного программирования.
@cdhowie объектно-ориентирован на Gtk? Определенно это так. Основным языком реализации является C. Если применение Rust к объектно-ориентированному проекту может закончиться болезненно, то Gtk будет крайне агонизирующим.
@Todd GObject вроде... это агония во многих отношениях. В любом случае, лучший способ описать Gtk — это то, что Gtk построен на GObject и C. GObject — это то, что реализует парадигму объектно-ориентированного программирования поверх CC. C не является объектно-ориентированным.
@cdhowie Rust обладает всеми функциями объектно-ориентированного языка, за исключением наследования состояний. В Rust есть виртуальные таблицы, виртуальные методы, реализация которых может наследоваться без необходимости писать дополнительный код. В Java есть интерфейсы, которые являются близким аналогом особенностей Rust. У Rust достаточно возможностей для поддержки мультипарадигмальной разработки.
@Todd Если вы хотите попробовать объектно-ориентированную разработку в Rust, добро пожаловать. В конце концов вы упретесь в стену, где будете работать настолько против течения, что загоните себя в угол. На форумах Rust есть много историй о людях, пытающихся внедрить объектно-ориентированный подход в Rust. Это просто не работает. Это мой совет как человека, который работает с Rust уже много лет. Возьми это или оставь.
Давайте продолжим обсуждение в чате.
Кстати, у меня 7 лет опыта работы с Rust. В любом случае, реализация собственного итератора для типа impl Iterator for MyType
по своей природе является объектно-ориентированной. Я бы сказал, что это «ОО-разработка». Таким образом, ваш тип автоматически наследует множество моделей поведения. Тот факт, что состояние не передается, не делает процесс реализации итератора не-OO.
@Todd, Спасибо за подробный ответ. Похоже, вариант использования, который я собираюсь использовать, не очень идиоматически ржавый? Будет ли это антипаттерном?
@bluesquare Rust, как и большинство объектно-ориентированных языков, строго обеспечивает инкапсуляцию и конфиденциальность членов данных (если только они не раскрываются намеренно). И то, что описывал ваш вопрос, звучало так, будто вам нужна черта, которая могла бы получить доступ к личным членам данных внешнего типа. В таких языках, как C++ или Java, вы не можете сделать внешний класс подклассом одного из ваших собственных интерфейсов или классов без непосредственного изменения исходного кода. Rust немного более гибок: вы можете добавлять свойства к внешним типам.
@Тодд да, но во многих других языках (как C++, так и Java) вы можете создать подкласс оригинала, а затем создать свой собственный подкласс с нужным вам состоянием, а также реализовать третий «интерфейс», если это необходимо, но по-прежнему сохраняйте ссылку/указатель/и т. д. на ваш новый класс в коллекции указателей на типы базовых классов. Ржавчина, ты не можешь этого сделать, это инкапсуляция. Есть причины, по которым вам не следует этого делать, но это разница между «можно» и «нельзя».
@KevinAnderson, верно, вы не можете создавать подклассы для структуры. Это и есть причина моего поста, показывающего, как применять шаблон декоратора для достижения целей расширения внешнего типа. WRT-агрегаты/коллекции базового типа, вы на самом деле можете сделать это в Rust. Однако есть некоторые вещи, которые до сих пор не поддерживаются, например преобразование черты в суперчерту (в какой-то степени это есть в Nightly).
@Тодд Спасибо. Раньше я знал, что это можно сделать с помощью общей черты, но не знал, что в Rust есть множественное наследование трейтов (не только Java, которую я знаю) в стиле Java (кашель-интерфейсы). Меня это устраивает, и мне тоже нравится ваш пример, но для меня это новость. Видимо, я пропустил главу «Продвинутые черты» в «Книге».
Придирка:
-> ()
избыточен.