Комментарии 11
Получилось сложно, я бы даже сказал очень сложно, в ExpressionVisitor я просто наследуюсь от него и сразу вижу все возможные типы выражений которые он посещает, а у вас всё разширяемо и кастомизируемо, но абсолютно не наглядно
Эх, когда уже довезут discriminated union до шарпа...
Пользуясь случаем, поделюсь своей статьёй ( надеюсь, уместно - тоже пол визитор https://habr.com/p/332042/)
В Rust-е уже есть, более того, каждый его variant может принимать произвольные данные (будь то кортеж, структура или другой enum, да что угодно).
Возможно, я даже читал вашу статью, находясь в поисках вдохновения для своей библиотеки!
Очень классно, что вы тоже пришли к дженериковому Accept
Правда, описанные недостатки и приведённые сведения о паттерне также есть и в моей статье
Я использую пакет OneOf, попробуйте, вдруг устроит?
Чтобы добавить НОВУЮ операцию не трогая классы, придумали Visitor.
Что есть «Visitor» в функциональном стиле? В ФП «суммовой» тип (ADT) встроен в язык, а pattern-matching даёт эквивалент двойной диспетчеризации за одну строку.
Дело в том, что обработчик данного элемента неприводим к типу 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, тут другие трейдоффы, но как будто бы больше контроля за тем, что не можем менять, а что можем отжать на откуп вызывающему коду. Интересно, когда какой подход логичнее использовать.
Такого «Посетителя» вы ещё не видели — Visitor.NET