Pull to refresh

Comments 19

А разве в Java нельзя переопределить оператор сравнения двух объектов Object. equals?

Можно, но вопрос не в том. Вопрос в подгонке произвольного объекта под произвольный интерфейс. В Java это можно сделать анонимной имплементацией интерфейса, которая враппит объект (так в долямбдную эру реализовывались динамические коллбеки, например).

А что если у вас есть бизнес-логика, завязанная на текущую реализацию Object.equals? Или у вас просто нет доступа к исходным данным оборачиваемого объекта?
А что если у вас есть бизнес-логика, завязанная на текущую реализацию Object.equals?

Это сразу «вон из профессии». Кроме шуток.
То есть реализовывать Objct.equals для каких-нибудь строк уже считается некультурным?

Единственный допустимый кейс — сравнивание экземпляра класса с себе подобными относительно определенных для сравнения внутренностей. Никакой бизнес логики быть не должно. equals() неявно используется в куче мест языка.

Проблема с обёртками — нарушение ссылочной идентичности. Т.е у тебя может быть две обёртки, а за ними — один и тот же объект.


Почему это плохо? Бывает код, который неявно полагается на то, что если "сущность" одна и та же, то и ссылка должна быть та же.


  1. Например, если код делает synchronized(adapted) (но синхронизироваться на левых объектах — плохая идея).
  2. Сравнивает по ссылке. Например, какой-то внутренний кеш. Мы обходим граф объектов и проверяем, видели мы какой-то объект или нет. Если каждый узел графа будет обёрткой, то можем оказаться в ситуации, когда мы объект видим второй раз, а ссылка на него другая (так как это обёртка)
  3. Просто с точки зрения памяти, хотя это не сильно принципиально. С коллекциями беда — их то ли копировать (память), то ли заворачивать (морока).

В случае Rust, эта самая идентичность ещё более критична если речь идёт о изменяемом заимствовании (&mut). Правда, тут получается, что как бы сами "создали" себе проблему (выбором Rust).


Насколько я помню, в Java такое адаптирование в случае изменяемых объектов тоже какие-то нехорошие эффекты имело, но не помню деталей. Может, с коллекциями это было связано.

У нас есть родной ночной TraitObject. Правда все равно от него польза в таком смысле сомнительна. Безопасней создать структуру, адаптирующую пришедшее к нужному типажу. Типажи известны, на каждый можно по структуре определить, внутрь структуры распарс пришедшего согласно нуждам типажа. Ведь мы же только данными оперируем, правда? Мы же не имеем права определить пришедшие данные как исполняемый код?

Этот TraitObject — то же самое по сути, но требует включение нужной фичи (raw). Мой вариант будет работать и на стабильном Rust. Понятно, что если просто убрать этикетку (#![feature(raw)]), сущности это не поменяет и код будет ровно такой же хрупкий :D


Не понял второй части. Можно через обёртки, да, собственно варианты #1 и #2 про это и были, но с ними, например, изменяемый интерфейс уже сложнее сделать (т.к &mut self может быть только один).


Можно, но если дальше обобщать (например то, что сама "система типов" — это типаж с функцией fn adapt<'a>(&'a self, data: &'a Data, meta: &MetaInfo) -> Self::Adaptor<'a>, то уже GAT-ы нужны.


На верхнем уровне, задача такая:


  1. Взять, например, serde_json::Value. Взять JSON схему (с диска прям прочитать). Скрестить данные и схему и заставить &Value вести себя как "типизированный объект", &dyn Object (т.е отвечать на вопросы "какие у тебя есть поля?", и.т.д.).
  2. Взять структуры. Поступить аналогично (этот вариант тривиален — просто реализуем типажи для структуры).
  3. Обобщённый код (например, валидация) может работать с обоими ^^ через обобщённый интерфейс типаж-объекта Object).

Другой вариант — это всегда держать пару (&Data, &Meta) и при обходе данных обновлять параллельно. Т.е let (child, child_meta) = (data.get_field("field"), meta.get_field("field")), но там тоже свои проблемы.

Если нам приходят всегда только данные (а не исполняемый код, который безусловно подсовывать в таблицу), то каждый раз динамически генерировать vtable слегка оверкилл. Безусловно, там будет либо копирование, либо один владелец-враппер, но этого достаточно для организации цепочки ответственных, просто на каждый типаж по врапперу.

Ээээ… В 99% случаев это решается проще.
Раст позволяет просто имплементировать произвольный трейт для произвольного типа, единственное условие — либо тип, либо трейт должны быть определены в вашем модуле. Положим, указатель вам кто-то отдал, но интерфейс-то вы свой используете, вот и имплементируйте этот интерфейс для типа, а потом приводите указатель в trait object сколько влезет.

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


#[test]
fn test() {
  use serde_json::Value;
  let x: Value = Value::String("hello".into());
  let y: Value = Value::String("hello".into());

  let x_obj: &dyn Object = adapt(&x, "id");
  let y_obj: &dyn Object = adapt(&y, "string");

  assert_eq!(x_obj.type_name(), "id");
  assert_eq!(y_obj.type_name(), "string");
}

Наверное, я немного запутал тем, что использовал термины "система типов" и "имя типа" (fn type_name). Это просто некоторая абстрактная мета-информация, которую я хочу приписать к данным. Идея здесь в том, что эта информация не всегда доступна во время компиляции.


Я немного тут расписал: https://habr.com/post/432202/#comment_19459492


P.S. Про 99% — совершенно верно. Я думаю, те, кому этот приём может пригодиться и сами могут его реализовать. А те, кто не может — им это и не нужно.

Прикольная черная магия, но вообще мне кажется, что когда такое вот становится нужным в бизнес логики, то нужно задать себе вопрос, может стоит переписать эти участки на языке с динамической типизацией?

Это правильный вопрос, но тут много переменных чтобы так просто ответить. Если по-честному, то мы вот это вот не используем. Это был так, эксперимент, чтобы джокера к рукав засунуть, на случай чего.


На языке с динамической типизацией были бы другие проблемы.


Энтерпрайз (особенно, когда всё делается практически с чистого листа, как у нас :D) — это как водяной матрас. Сложность никуда не девается, её можно только в разные части передавливать.

Почему бы просто не использовать Rc?


fn annotate<'a>(input: &'a String, type_name: &str) -> Rc<dyn Object + 'a> {
    Rc::new(Wrapper {
        value: input,
        type_name: type_name.into(),
    })
}

"Но при этом сохранив сигнатуру annotate как она есть. То есть вернуть ссылку с подсчётом ссылок (например, Rc<Wrapper>) — не подходит."


В каких случаях это может быть необходимо? Когда мы можем использовать Box, но не можем Rc потому что… не хотим менять сигнатуру метода? Да ладно? )

Цель была не использовать обёртки. Прелесть конверсии &mut Data -> &mut Object в том, что с заимствованием хорошо стыкуется.


С Rc изменяемости не получится (&mut self).


Плюс, Rc — заразны. В нашем случае API у Object более сложный, и весь API пришлось бы перетряхивать, чтобы возвращать Rc (или Box).

Sign up to leave a comment.

Articles