Как стать автором
Обновить

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

Получилось сложно, я бы даже сказал очень сложно, в ExpressionVisitor я просто наследуюсь от него и сразу вижу все возможные типы выражений которые он посещает, а у вас всё разширяемо и кастомизируемо, но абсолютно не наглядно

Эх, когда уже довезут discriminated union до шарпа...

Пользуясь случаем, поделюсь своей статьёй ( надеюсь, уместно - тоже пол визитор https://habr.com/p/332042/)

В Rust-е уже есть, более того, каждый его variant может принимать произвольные данные (будь то кортеж, структура или другой enum, да что угодно).

Возможно, я даже читал вашу статью, находясь в поисках вдохновения для своей библиотеки!

Очень классно, что вы тоже пришли к дженериковому Accept

Правда, описанные недостатки и приведённые сведения о паттерне также есть и в моей статье

Я использую пакет OneOf, попробуйте, вдруг устроит?

Чтобы добавить НОВУЮ операцию не трогая классы, придумали Visitor.

Что есть «Visitor» в функциональном стиле? В ФП «суммовой» тип (ADT) встроен в язык, а pattern-matching даёт эквивалент двойной диспетчеризации за одну строку.


    type Expr =
      | Lit of int
      | Add of Expr * Expr
      | Mul of Expr * Expr

    let rec eval = function
      | Lit  n     -> n
      | Add (a, b) -> eval a + eval b
      | Mul (a, b) -> eval a * eval b

Вот тот же Visitor, только компактно и без шаблонного кода

В общем, в ООП визитор выглядит как костыли

Дело в том, что обработчик данного элемента неприводим к типу IVisitor, а сам IVisitor является маркерным интерфейсом без каких-либо членов.

Если у вас это - единственная проблема, то унаследуйте IVisitor<T> от IVisitor.
Но проблему проверки типов при компиляции это действительно не решает.

А вообще Visitor всегда производил у меня впечатление костыля - хорошего, крепкого, но костыля. По хорошему, для решения таких задач нужна двумерная таблица виртуальных методов, но ее нам не завезли. Но можно сделать самому. В C++ подобные задачи, помнится, было удобно решать с помощью явных специализаций template, но в C# этого AFAIK нет до сих пор.
В C# мне подобные задачи не попадались, но если попадутся, первым кандидатом в средства решения будут обобщенные (generic) методы - буду, если чо, пытаться впихнуть их по месту.
И последнее: dynamic - это, чаще всего, про отражение, а потому - небыстро. Впрочем, какие-то средства для ускорения в виде Dynamic Language Runtime были во Framework и возможно (не проверял) остались и в Core (он же - просто .NET).

PS Ну а рутинной работы с Expression Trees (которые MS объявила нерасширяемыми) есть встроенный ExpressionVisitor.

PPS Я долго пытался понять, что делает первый пример, пока не сообразил, что тамошний единственный метод занимается кодогенерацией, а start - это счетчик команд для начала фрагмента кода. Короче, неплохо было бы пояснить. И ещё, я для себя решил, что речь идет про классы Expression Trees (из System.Linq.Expressions) - потому что об эти деревья я сейчас частенько бьюсь головой и потому названия все знакомые, а вообще было бы неплохо это разъяснить прямо в статье. И последнее - почему бы не давать примеры кода не картинками а вставками кода: а то хочется зацитировать, а неудобно.

Ну, а в целом оценка статьи позитивная, читать только трудновато.

У меня в моём компиляторе (написанном на C++) тоже используется нечто вроде visitor, но на основе std::variant, а не иерархии наследования. Абстрактные типы синтаксических элементов объявляются как variant от конкретных типов. Посещение осуществляется через std::visit, которой я передаю шаблонную лямбду, которая вызывает функцию/метод с именем вроде VisitImpl. Так вот эта функция имеет несколько перегрузок - под каждый возможный тип. При добавлении нового типа компиляция ломается, пока не будет реализована функция VisitImpl под этот тип.

Достоинство подхода - передача потока управления осуществляется без вызова по указателю, как происходило бы в случае виртуальных функций, что даёт возможность компилятору ускорять код за счёт встраивания. По сути std::visit разворачивается внутри в нечто вроде switch-case, который хорошо оптимизируется. К тому же нету необходимости посещаемые типы делать частью какой-либо иерархии наследования.

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

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