В последнее время я часто слышу, что Rust — это такой удивительный новый язык программирования, который решает все проблемы индустрии одним махом. Ну что ж, давайте попробуем его в деле.
Мы пока не будем строить полноценный CardDOM — начнём с небольшого упражнения.
Задача
Представим простую иерархию типов: кусочек модели документа, которая состоит всего из трех типов.
базовый тип
DomNode,производный интерфейс
CardItem,один конкретный класс
TextItem, который расширяетCardItemиDomNode.
Наша задача проста:
Создать указатель на
DomNode,который на самом деле ссылается на
TextItem,затем привести его вниз к
CardItemи вызвать какой-нибудь метод.
Зачем такое может понадобиться?
У нас в приложении есть множество разнотипных классов. И есть инфраструктурные механизмы - копирования логирования, сериализации и т.д., которые будут хранить эти разнотипные объекты в коллекциях, вида map(оригинал->копия) или vector(id->объект) или set(visited). И ссылки в этих коллекциях будут иметь какой-то универсальный тип, например (С++) weak_ptr<DomNode>. Очень часто элементы из этих коллекций нужно возвращать в поля объектов, при этом поля могут быть указателями на конкретные типы (sized в терминологии Rust, и тогда можно применить Any) но гораздо чаще они - полиморфные dyn Traits. Сегодняшний пример исследует именно этот сценарий приведения типов. Берем указатель на базовый тип, приводим его к производному трейту (с проверкой, конечно) и вызываем метод этого трейта.
Версия на Argentum
В Argentum это занимает всего несколько строк (я привожу этот пример только как демонстрацию простого решения на ультра-безопасном языке с нативной производительно, так сказать, точка отсчета, чтобы было с чем сравнивать):
interface DomNode {}
interface CardItem {
+DomNode;
echo();
}
class TextItem {
+CardItem {
echo() { sys_log("Hello from Text") }
}
}
v = TextItem~DomNode;
v~CardItem?_.echo();
Давайте разберём, что здесь происходит (Ссылка на Playground):
Мы определяем два интерфейса — DomNode и CardItem, где CardItem расширяет DomNode.
Затем определяем конкретный класс TextItem, реализующий CardItem, и добавляем ему метод echo(), который логгирует строчку.
Далее мы создаем экземпляр TextItem, приводим его (upcast) к DomNode и сохраняем в переменной v
А затем пробуем привести (downcast) к CardItem. Оператор ? проверяет результат нисходящего приведения (распаковывает optional) и если всё успешно — вызывает echo().
И всё. 12 простых строк кода. Без синтаксического мусора и танцев с бубном.
Версия на Rust
Теперь попробуем то же самое в Rust (Ссылка на Playground)
Чтобы добиться того же результата, нам понадобятся Rc и RefCell, плюс немного акробатики:
use std::cell::{RefCell};
use std::rc::{Rc, Weak};
trait DomNode {
fn as_card_item(&self) -> Option<Rc<RefCell<dyn CardItem>>> { // 2
None
}
}
trait CardItem: DomNode {
fn echo(&self);
}
struct TextItem {
me: Weak<RefCell<Self>>, // 4
}
impl TextItem {
fn new() -> Rc<RefCell<Self>> {
let node = Rc::new(RefCell::new(Self { me: Weak::new() })); // 5
node.borrow_mut().me = Rc::downgrade(&node);
node
}
}
impl DomNode for TextItem {
fn as_card_item(&self) -> Option<Rc<RefCell<dyn CardItem>>> { // 3
self.me
.upgrade()
.map(|rc| rc as Rc<RefCell<dyn CardItem>>) // 6
}
}
impl CardItem for TextItem {
fn echo(&self) {
print!("Hello from Text")
}
}
fn main() {
let text_as_dom_node = TextItem::new() as Rc<RefCell<dyn DomNode>>; // 1
if let Some(text_as_card_item) = text_as_dom_node.borrow().as_card_item() { // 7
text_as_card_item.borrow().echo();
}
}
Выглядит немного избыточно? Давайте разберёмся, почему так.
Rc и RefCell используются для создания разделяемых и изменяемых объектов. Это не встроено в язык по умолчанию, поэтому подключаем из стандартной библиотеки.
Мы определяем трейты (интерфейсы) DomNode и CardItem.
Как и в Argentum, мы приводим возвращаемое значение Rc<RefCell<Self>> к типу Rc<RefCell<dyn DomNode>> [1].
Ключевое слово dyn делает этот указатель fat: он состоит из двух указателей — один на саму структуру данных, другой на таблицу методов интерфейса. Это и есть механизм полиморфизма в Rust.
CardItem является подтипом DomNode. Поэтому upcast-преобразование [1] работает. Но Rust не умеет делать обратное — downcast, то есть приводить указатель к базовому трейту. Эту функциональность нужно реализовать вручную.
Добавим в DomNode новый метод as_card_item [2]. Он возвращает опциональное значение Option<Rc<RefCell<dyn CardItem>>>. Для всех DOM-узлов по умолчанию он возвращает Option::None, а для тех, которые действительно можно привести к CardItem, мы реализуем собственную логику преобразования [3]. Эту реализацию нужно повторить во всех конкретных типах, реализующих интерфейс CardItem.
Возникает вопрос: как вернуть Rc<RefCell<Self>>, если у нас есть только ссылка на внутреннюю структуру, которая вложена в несколько оберток? Простого способа нет. Нужно хранить в каждом объекте ссылку на внешний контейнер Rc<RefCell<Self>>. Чтобы избежать утечек памяти, эта ссылка должна быть Weak.
Так что добавим в TextItem поле [4] (и аналогично — в каждый тип, реализующий CardItem). Это будет слабый указатель на RefCell<Self>. Так как поле объявлено, его нужно инициализировать при создании объекта. Сделать это сразу нельзя — структура еще не обернута в Rc и RefCell, а слабая ссылка должна указывать именно на этот Rc. Поэтому сначала мы инициализируем поле пустым Weak [5], затем сохраняем созданный узел в переменную, берём его через borrow_mut() и присваиваем полю ссылку на внешний Rc через Rc::downgrade. После этого возвращаем этот узел как результат new().
Теперь, когда поле объявлено и инициализировано, можно завершить операцию приведения типов. Мы берём слабый указатель [6], пытаемся восстановить его в Rc (через upgrade()), проверяем, не равен ли он None, и если всё хорошо — приводим его к интерфейсу CardItem.
Так выполняется downcast из одного интерфейса в другой: сначала через виртуальный вызов получаем Rc<RefCell<Self>>, а потом снова upcast к нужному интерфейсу.
В функции main мы берем указатель на интерфейс DomNode. Вызываем метод as_card_item [7], который мы только что добавили; он возвращает Option<T>, как и в Argentum. Поэтому нужно проверить, не пуста ли ссылка, и вызвать метод только если есть значение. В отличие от Argentum, где есть оператор ?, — в Rust это требует более громоздкого синтаксиса: нужно сделать ...map(lambda) или матчинг if let Some(value) = ...,
После всего сделанного downcast заработал, программа компилируется и не падает.
Субъективные ощущения
Rust вызывает смешанные чувства. Его авторы, кажется, придумали его не для решения практических задач, а чтобы опробовать несколько абстрактно-научных концепций. Он не выглядит ориентированным на практические нужды программистов, поэтому в большинстве случаев код получается многократно длиннее и сложнее для написания и чтения. Программа выглядит как мешанина из вложенных Rc, RefCell, Weak, upgrade, borrow, drop, unwrap, map, match, as_deref_mut.
Чтобы сделать тривиальное приведение типов, приходится вручную:
добавлять дополнительные поля,
реализовывать методы преобразования,
дублировать логику интерфейсов в каждом конкретном классе.
И всё это — ради того, что в других безопасных языках происходит автоматически.
Заключение
Rust (его safe-подмножество) действительно дает безопасность памяти (если считать панику безопасностью), но ценой многословности и перегруженности. Вы тратите больше времени на борьбу с компилятором, чем на выражение своих идей. Давайте кратко перечислим категории, которые нужно держать в голове, и явно и обслуживать в коде раз за разом: (Владение и заимствование) × (отдельно для указателей и для указуемых контейнеров) × (отдельно для структур и их Cell-фрагментов) × (отдельно в mutable и immutable форме) + времена жизни этих владений и заимствований. Некоторые вещи проверяются компилятором, для всего остального вставляются рантайм-проверки. Причем рантайм автоматически ведет учет этих владений и заимствований, но не для того, чтобы помочь программисту, а для того, чтобы убить приложение паникой, если посчитает, что программист ошибся.
Может быть для полиморфных структур данных Раст предлагает другой метод приведения типов? Поделитесь в комментариях, если вы знаете каст попроще. Он нужен для реализации Card DOM.
Комментарии (14)

Mingun
27.10.2025 14:57Чтобы добиться того же результата, нам понадобятся 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-е нельзя, так и зачем себе делать больно?

kotan-11 Автор
27.10.2025 14:57Я это объяснил в параграфе "Зачем такое может понадобиться?"

Mingun
27.10.2025 14:57Ну допустим, чтобы можно было взять из коллекции умный указатель, копирнуть указатель со сменой типа и новый указатель кому-то отдать. При этом хранить в коллекциях вы же собрались
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 идиома получается).
kotan-11 Автор
27.10.2025 14:57Если реализовать трейт сразу над
Rc<RefCell<TextItem>>то я не смогу хранить объект ни в чем другом, потому что тогда и все прочие реализации трейтов и собственных методов должны будут получать self не как ссылку на структуру, а как ссылку на Rc-обертку, чтобы их можно было вызывать друг из друга.Кроме того каждый метод будет начинаться с
let realSelf = self.borrow()а мутабельный метод - сlet mut inner = self.borrow_mut();а любой вызов другого метода будет превращаться в последовательность drop-call-borrow_mut. Это выглядит как ручное управление памятью.

Mingun
27.10.2025 14:57Если прям так нужен
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()) } }
kotan-11 Автор
27.10.2025 14:57"Очень часто элементы из этих коллекций нужно возвращать в поля объектов..." Объекты приложения должны быть мутабельными, и еще таргетами для Weak. Буду рад, если укажете как это сделать без Rc<RefCell<T>>.

cpud47
27.10.2025 14:57Ну так сделайте аналогичный даункаст с Rc<RefCell<T>>. Суть в том, что если Вам нужен доступ к специфичному контейнеру, то просто реализуйте интерфейс для него.
В целом можно более аккуратно покрутить трейты, чтобы пересобрать толстый указатель с другой метадатой. Хотя не уверен, что это без CoerceUnsize выйдет сделать.
Но в расте в целом не очень приветствуют даункасты, поэтому инфраструктура под них плохо развита
rsashka
Сейчас набегут любители Rust и зададут вам по первое число, что мало не покажется, не смотря на то, что вы совершенно правы. ;-)
smt_one
Не то чтобы я люблю Rust, но это слегка выглядит как попытка пиара своего решения в виде Argentum. Так что наоборот жду любителей Rust, тогда может в дискуссии с ними выяснится что.
rsashka
Это безусловно пиар своего языка Argentum, в котором автор немного перемудрил со ссылочными типами данных
kotan-11 Автор
Я не могу пиарить Аргентум, он еще не готов. Я просто оцениваю другие решения, сравнивая со своим. И если где-то что-то будет лучше,
это будет украденоя буду только рад.rsashka
Ну и зря. Как по мне, это наоборот нужно делать заранее, еще до выхода , чтобы было время переделать, если в базовой идее обнаружатся какие либо противоречия. Тогда в этом случае можно будет переделать и подправить не ломаю обратную совместимость (как это иногда происходило в Python или Rust). Ведь решение еще не выпущено :-)