Обновить

Комментарии 23

Rust вызывает смешанные чувства. Его авторы, кажется, придумали его не для решения практических задач, а чтобы опробовать несколько абстрактно-научных концепций. Он не выглядит ориентированным на практические нужды программистов, поэтому в большинстве случаев код получается многократно длиннее и сложнее для написания и чтения. Программа выглядит как мешанина из вложенных Rc, RefCell, Weak, upgrade, borrow, drop, unwrap, map, match, as_deref_mut.

Сейчас набегут любители Rust и зададут вам по первое число, что мало не покажется, не смотря на то, что вы совершенно правы. ;-)

Не то чтобы я люблю Rust, но это слегка выглядит как попытка пиара своего решения в виде Argentum. Так что наоборот жду любителей Rust, тогда может в дискуссии с ними выяснится что.

Это безусловно пиар своего языка Argentum, в котором автор немного перемудрил со ссылочными типами данных

Я не могу пиарить Аргентум, он еще не готов. Я просто оцениваю другие решения, сравнивая со своим. И если где-то что-то будет лучше, это будет украдено я буду только рад.

Я не могу пиарить Аргентум...

Ну и зря. Как по мне, это наоборот нужно делать заранее, еще до выхода , чтобы было время переделать, если в базовой идее обнаружатся какие либо противоречия. Тогда в этом случае можно будет переделать и подправить не ломаю обратную совместимость (как это иногда происходило в Python или Rust). Ведь решение еще не выпущено :-)

Мне, как человеку, у которого есть опыт только с сишным синтаксисом (C++ и преимущественно C#), Раст бьёт по глазам очень сильно.

Но это далеко не так больно, как факт того, что в Расте до сих пор нет стабильного ABI. Ладно стабильного, компилятор Раста даже не способен выдать после компиляции инфу в адекватном виде по макетам типов, что решило бы множество проблем взаимодействия. 25 год подходит к концу, на секундочку.

Хорошо сделанную либу, написанную на Расте, просто невозможно вызвать из, например Шарпа, не написав кучу клея с оверхедом.

То есть язык, с которым невозможно общаться без С ABI (повышая цену вызова), позиционируется как замена С.

А зачем еще один стабильный ABI, когда есть C ABI? Тут была статья о том, как компилятор оптимизирует вложенные друг в друга перечисления, типа Option<Result<Option<...>>>. Как вы представляете себе это со стабильным ABI, если теги каждого фактического варианта поползут от изменения любого типа?

Чтобы добиться того же результата, нам понадобятся Rc и RefCell, плюс немного акробатики:

Не совсем понятно, зачем тут Rc и RefCell? Чем обычный &dyn CardItem не угодил? Даже гугловский ИИ в поиске уже дает готовое решение:

use std::cell::RefCell;
use std::rc::Rc;

trait DomNode {
  fn as_card_item(&self) -> Option<&dyn CardItem> {
    None
  }
}

trait CardItem {// не обязательно даже требовать : DomNode
    fn echo(&self);
}

struct TextItem;

impl DomNode for TextItem {
    fn as_card_item(&self) -> Option<&dyn CardItem> {
        Some(self)
    }
}

impl CardItem for TextItem {
    fn echo(&self) {
        println!("Hello from Text")
    }
}

#[test]
fn test() {
    {
        let text: Box<dyn DomNode> = Box::new(TextItem);
        if let Some(card) = text.as_card_item() {
            card.echo();
        }
    }

    {
        let text: Rc<dyn DomNode> = Rc::new(TextItem);
        if let Some(card) = text.as_card_item() {
            card.echo();
        }
    }

    let text: Rc<RefCell<dyn DomNode>> = Rc::new(RefCell::new(TextItem));
    if let Some(card) = text.borrow().as_card_item() {
        card.echo();
    }
    // Вывод перед падением теста:
    // Hello from Text
    // Hello from Text
    // Hello from Text
    assert!(false);
}

Похоже вы по привычке из других языков решили, что вам не обойтись без возврата умных указателей из функций, поскольку в этих других языках без этого легко можно получить висячий указатель. А Rust-е нельзя, так и зачем себе делать больно?

Я это объяснил в параграфе "Зачем такое может понадобиться?"

Ну допустим, чтобы можно было взять из коллекции умный указатель, копирнуть указатель со сменой типа и новый указатель кому-то отдать. При этом хранить в коллекциях вы же собрались Rc<RefCell<dyn DomNode>>. Если и в поле вы затем решите сохранить Rc<RefCell<dyn CardItem>>, то как я ниже написал, зачем вам тогда реализовывать типаж над TextItem? Реализуйте сразу над Rc<RefCell<TextItem>>. Вы фактически это и делаете, только зачем-то как-то через задницу.

Еще отмечу, что вы фактически реализуете C++-ный вариант наследования от enable_shared_from_this, но так как в Rust-е нет наследования, а только агрегация, то пришлось всю машинерию писать руками (хотя и тут ее можно было бы упрятать за макрос). Тут вы копируете свой опыт с C++, так как там по другому не сделать (точнее можно, но кажется многословнее и в сущности мало чем отличается от наследования от enable_shared_from_this -- если упрятать shared_ptr внутрь своей структуры и интерфейс реализовать для нее. Вообще это pimpl идиома получается).

Если реализовать трейт сразу над Rc<RefCell<TextItem>> то я не смогу хранить объект ни в чем другом, потому что тогда и все прочие реализации трейтов и собственных методов должны будут получать self не как ссылку на структуру, а как ссылку на Rc-обертку, чтобы их можно было вызывать друг из друга.

Кроме того каждый метод будет начинаться с let realSelf = self.borrow() а мутабельный метод - с let mut inner = self.borrow_mut(); а любой вызов другого метода будет превращаться в последовательность drop-call-borrow_mut. Это выглядит как ручное управление памятью.

Непонятно, откуда это следует. Один раз получите ссылку realSelf и все методы этого типа на ней вызываете, внутри них никаких self.borrow() не потребуется, они-то ведь будут реализованы на типе, а не на Rc-обертке.

Предположим, что у меня есть класс с десятком методов, все они пользуются данными класса, 3 из 10 являются реализацией интерфейса, а остальные - или методами других интерфейсов или методы непосредственно класса. У предлагаемого решения есть три пути реализации:

  1. Пусть три метода принимают self как &[mut] Rc<RefCell<Self>> а остальные как &[mut] Self. В этом случае только три дожны делать borrow[_mut] и делать drop-borrow, вызывая друг друга. При этом они могут свободно вызывать остальные 7, но эти 7 никогда не смогут вызвать эти 3. Звучит не очень удобно.

  2. Пусть все методы принимают self как &[mut] Rc<RefCell<Self>> мучаются с borrow/drop/borrow. Зато тогда не будет проблем с невозможностью вызова некоторых методов из других методов.

  3. Пусть все методы принимают self как &[mut] Self но для трех методов будут написаны отдельные методы-обертки, принимающие self как &[mut] Rc<RefCell<Self>>, делающие borrow[_mut] и перевызывающие обычные методы. Но тогда мы не сможем передавать self наружу, например в виде Weak (это иногда справедливо и для 1 кстати).

Итого мы имеем или странные на ровном месте ограничения, или тонну рукописной работы по менеджменту времени жизни заимствований, которая очень похожа на ручное управление памятью на максималках или удвоение количества интерфейсных методов и невозможность подписывать свой объект на внешние события через Weak.

По сравнению с перечисленными проблемами хранение в объекте self-weak не такая большая плата.

Если прям так нужен Rc<dyn CardItem> и вы методы кастования только на Rc<TextItem> собрались вызывать -- так и реализуйте типаж на Rc<TextItem>, а не на TextItem:

trait RcDomNode {
    fn as_card_item(&self) -> Option<Rc<dyn CardItem>> {
        None
    }
}
impl RcDomNode for Rc<TextItem> {
    fn as_card_item(&self) -> Option<Rc<dyn CardItem>> {
        Some(self.clone())
    }
}

"Очень часто элементы из этих коллекций нужно возвращать в поля объектов..." Объекты приложения должны быть мутабельными, и еще таргетами для Weak. Буду рад, если укажете как это сделать без Rc<RefCell<T>>.

Ну так сделайте аналогичный даункаст с Rc<RefCell<T>>. Суть в том, что если Вам нужен доступ к специфичному контейнеру, то просто реализуйте интерфейс для него.

В целом можно более аккуратно покрутить трейты, чтобы пересобрать толстый указатель с другой метадатой. Хотя не уверен, что это без CoerceUnsize выйдет сделать.

Но в расте в целом не очень приветствуют даункасты, поэтому инфраструктура под них плохо развита

Этот подход как раз обсуждается двумя комментами выше (он имеет большие проблемы).

В расте нет классического наследования, вполне ожидаемо, что попытки прывычным образом его сюда натянуть выглядят так себе 😀 Для моделирования ациклических графовых структур напрашиваются типы-суммы и паттерн-матчинг.

Наивное решение через типы-суммы
enum DomNode {
    Card(CardType),
    Wrapper(Vec<DomNode>),
}

enum CardType {
    Text(String),
    NoText,
}

impl CardType {
    fn echo(&self) {
        match self {
            CardType::Text(content) => println!("A TextItem's content: {content}"),
            _ => {}
        }
    }
}

fn main() {
    let dom = DomNode::Wrapper(vec![
        DomNode::Card(CardType::NoText),
        DomNode::Card(CardType::Text("Hello World".to_string())),
        DomNode::Wrapper(vec![
            DomNode::Card(CardType::Text("Hello World".to_string())),
            DomNode::Card(CardType::NoText),
        ]),
        DomNode::Card(CardType::NoText),
    ]);

    visit_node(&dom);
}

fn visit_node(node: &DomNode) {
    match node {
        DomNode::Card(card) => card.echo(),
        DomNode::Wrapper(inner) => {
            for node in inner {
                visit_node(&node);
            }
        }
    }
}

прейграунд

Спасибо, я уже второй раз сталкиваюсь с предложением забить на dyn полиморфизм и реализовать Card DOM на enums. Похоже это хороший путь. Пойду делать CardDOM 👍

Для более сложных случаев (много вариантов и нужна вся производительность) может помочь библиотека enum_dispatch.

Мне показалось, что на аргентуме класс реализует только один интерфейс, а на расте два пересекающихся по методам?

Поскольку интерфейс CardItem продекларирован как расширение DomNode, все классы реализующие CardItem автоматически реализуют и DomNode, это не нужно повторять в каждом классе.

Ваша статья – великолепный пример сравнения ежа с ужом методом натягивания совы на глобус

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации