Состояние реализации трейтов в Rust

Я пытаюсь изучить 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, поскольку у меня нет кода. Как мне тогда сохранить текущую температуру?

Придирка: -> () избыточен.

Chayim Friedman 16.04.2024 22:09

Связано: stackoverflow.com/a/73163713/5397009

Jmb 17.04.2024 08:48
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
0
2
199
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Но никакой государственности!

просто неправильно, хотя вы не можете реализовать эту конкретную черту для любого типа, и 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 17.04.2024 00:11

@cdhowie Я категорически с тобой не согласен. Объектно-ориентированное программирование — это проверенный и надежный подход к сложным проектам, и Rust его поддерживает. И Rust ЯВЛЯЕТСЯ объектно-ориентированным языком в такой же степени, как и C++ — оба являются мультипарадигмальными.

Todd 17.04.2024 00:13

Я не понимаю, как решается вопрос ОП... Он реализует признак для типа, который находится в пакете, который им не принадлежит - в этом случае impl for запрещено

Codebling 17.04.2024 00:17

@Todd Rust Book говорит: «Многие конкурирующие определения описывают, что такое ООП, и по некоторым из этих определений Rust является объектно-ориентированным, а по другим - нет». Далее объясняется, что наследование может быть плохим, и как реализовать некоторые шаблоны ООП способом Rust... в целом я бы не согласился с тем, что это объектно-ориентированный язык, но я думаю, он открыт для интерпретации.

Codebling 17.04.2024 00:27

@Todd Rust вообще не поддерживает наследование типов значений, что является одним из основных элементов объектно-ориентированной парадигмы. Если Rust — это объектно-ориентированный подход, то в нем отсутствует одна из основных функций объектно-ориентированного программирования.

cdhowie 17.04.2024 00:43

@cdhowie объектно-ориентирован на Gtk? Определенно это так. Основным языком реализации является C. Если применение Rust к объектно-ориентированному проекту может закончиться болезненно, то Gtk будет крайне агонизирующим.

Todd 17.04.2024 00:45

@Todd GObject вроде... это агония во многих отношениях. В любом случае, лучший способ описать Gtk — это то, что Gtk построен на GObject и C. GObject — это то, что реализует парадигму объектно-ориентированного программирования поверх CC. C не является объектно-ориентированным.

cdhowie 17.04.2024 00:46

@cdhowie Rust обладает всеми функциями объектно-ориентированного языка, за исключением наследования состояний. В Rust есть виртуальные таблицы, виртуальные методы, реализация которых может наследоваться без необходимости писать дополнительный код. В Java есть интерфейсы, которые являются близким аналогом особенностей Rust. У Rust достаточно возможностей для поддержки мультипарадигмальной разработки.

Todd 17.04.2024 00:50

@Todd Если вы хотите попробовать объектно-ориентированную разработку в Rust, добро пожаловать. В конце концов вы упретесь в стену, где будете работать настолько против течения, что загоните себя в угол. На форумах Rust есть много историй о людях, пытающихся внедрить объектно-ориентированный подход в Rust. Это просто не работает. Это мой совет как человека, который работает с Rust уже много лет. Возьми это или оставь.

cdhowie 17.04.2024 00:55

Давайте продолжим обсуждение в чате.

Todd 17.04.2024 00:59

Кстати, у меня 7 лет опыта работы с Rust. В любом случае, реализация собственного итератора для типа impl Iterator for MyType по своей природе является объектно-ориентированной. Я бы сказал, что это «ОО-разработка». Таким образом, ваш тип автоматически наследует множество моделей поведения. Тот факт, что состояние не передается, не делает процесс реализации итератора не-OO.

Todd 17.04.2024 01:57

@Todd, Спасибо за подробный ответ. Похоже, вариант использования, который я собираюсь использовать, не очень идиоматически ржавый? Будет ли это антипаттерном?

bluesquare 17.04.2024 17:35

@bluesquare Rust, как и большинство объектно-ориентированных языков, строго обеспечивает инкапсуляцию и конфиденциальность членов данных (если только они не раскрываются намеренно). И то, что описывал ваш вопрос, звучало так, будто вам нужна черта, которая могла бы получить доступ к личным членам данных внешнего типа. В таких языках, как C++ или Java, вы не можете сделать внешний класс подклассом одного из ваших собственных интерфейсов или классов без непосредственного изменения исходного кода. Rust немного более гибок: вы можете добавлять свойства к внешним типам.

Todd 17.04.2024 19:31

@Тодд да, но во многих других языках (как C++, так и Java) вы можете создать подкласс оригинала, а затем создать свой собственный подкласс с нужным вам состоянием, а также реализовать третий «интерфейс», если это необходимо, но по-прежнему сохраняйте ссылку/указатель/и т. д. на ваш новый класс в коллекции указателей на типы базовых классов. Ржавчина, ты не можешь этого сделать, это инкапсуляция. Есть причины, по которым вам не следует этого делать, но это разница между «можно» и «нельзя».

Kevin Anderson 17.04.2024 22:14

@KevinAnderson, верно, вы не можете создавать подклассы для структуры. Это и есть причина моего поста, показывающего, как применять шаблон декоратора для достижения целей расширения внешнего типа. WRT-агрегаты/коллекции базового типа, вы на самом деле можете сделать это в Rust. Однако есть некоторые вещи, которые до сих пор не поддерживаются, например преобразование черты в суперчерту (в какой-то степени это есть в Nightly).

Todd 17.04.2024 23:45

@Тодд Спасибо. Раньше я знал, что это можно сделать с помощью общей черты, но не знал, что в Rust есть множественное наследование трейтов (не только Java, которую я знаю) в стиле Java (кашель-интерфейсы). Меня это устраивает, и мне тоже нравится ваш пример, но для меня это новость. Видимо, я пропустил главу «Продвинутые черты» в «Книге».

Kevin Anderson 18.04.2024 18:35

Другие вопросы по теме