TGViewer
StepOne | Степан Минин StepOne | Степан Минин @steponeit · 3.27K subscribers
Post #236 2.35K
StepOne | Степан Минин Правильность архитектуры проверяется при внедрении нового функционала Кажется, написал что-то очевидное, но столкнулся с этим лично на своём pet project. В рамках него я пишу интерпретатор TypeScript подобного языка. Этот проект родился, как дипломная работа…
Visitor NET. Уникальный проект

Когда я начал рефакторинг своего интерпретатора, передо мной встал вопрос, как реализовывать паттерн Visitor.

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

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

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

Так было принято решение выделить контракты для реализации паттерна в отдельную сборку, которая потом подключалась через NuGet.

Изначально, хотелось сделать ациклический вариант шаблона - он был гибким в том плане, что конечный посетитель строился из "кубиков" - возможностей посещать какие-либо элементы:

public interface IVisitor<in TVisitable, out T> : IVisitor
where TVisitable : IVisitable
{
T Visit(TVisitable visitable);
}


Соответственно сам визитор выглядел примерно так:

public class TypeSystemLoader :
IVisitor<ScriptBody>,
IVisitor<AbstractSyntaxTreeNode>,
IVisitor<TypeDeclaration>
{
// ...
}


Сам посещаемый элемент должен уметь вызвать visitor.Visit(this) и для этого ему нужно знать, что у типа есть правильная перегрузка метода.

Классический ациклический вариант предлагает следующую реализацию метода Accept:

public class ScriptBody : AbstractSyntaxTreeNode
{
public override void Accept(IVisitor visitor)
{
if (visitor is IVisitor<ScriptBody> typed)
typed.Visit(this);
}
}


Однако, прибегать к явному даункастингу в такой ситуации очень не хочется, потому что это лишает код полиморфности.

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

public interface IVisitable<in TVisitor, out T> : IVisitable
where TVisitor : IVisitor
{
T Accept(TVisitor visitor);
}


Такая реализация нарушала OCP и LSP, поскольку в посещаемом элементе отсутствовала единая точка входа для любого посетителя, независимая от результата обработки.

Получается нужно было обобщить сам метод Accept, чтобы склеить несколько перегрузок в одну, за счёт изменения применяемого вида полиморфизма.

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

Всего этого удалось добиться с помощью контравариантности и паттерна CRTP:

public interface IVisitor<in TVisitable, out TReturn>
where TVisitable : IVisitable<TVisitable>
{
TReturn Visit(TVisitable visitable);
}

public interface IVisitable<out TVisitable>
where TVisitable : IVisitable<TVisitable>
{
TReturn Accept<TReturn>(IVisitor<TVisitable, TReturn> visitor);
}


Тогда, в самом посещаемом элементе приходится писать два метода Accept: первый для корня иерархии, который будет вызывать посетитель; а второй уже для того, чтобы под капотом вызвался нужный Visit:

public record Operation(
char Symbol,
BinaryTreeNode Left,
BinaryTreeNode Right) : BinaryTreeNode, IVisitable<Operation>
{
public override TReturn Accept<TReturn>(
IVisitor<BinaryTreeNode, TReturn> visitor) =>
Accept(visitor);

public TReturn Accept<TReturn>(
IVisitor<Operation, TReturn> visitor) =>
visitor.Visit(this);
}


С написанием этих двух методов можно не заморачиваться, так как я создал incremental source generator, который автоматизирует написание кода.

Таким образом, удалось решить нетривиальную задачу по созданию ациклической, универсальной, расширяемой и типобезопасной реализации шаблона Visitor без применения явного даункастинга.

При этом, подобного решения не найти на просторах интернета - осмелюсь заявить, что вы видите подобное впервые.

Поэтому, давайте поддержим мой проект звездой на гитхаб ⭐️
https://github.com/Stepami/visitor-net

Нейросеть так не сможет.
GitHub GitHub - Stepami/visitor-net: First-ever acyclic generic extensible typesafe implementation of Visitor pattern for .NET without… First-ever acyclic generic extensible typesafe implementation of Visitor pattern for .NET without any usage of dynamic cast. Implementation powered by Source Generators - Stepami/visitor-net
  • 🔥 16
  • 👍 7
  • ❤ 2
  • 🕊 2
More from @steponeit
  1. Sep 29, 2026Рассказал как автоматизировал сборку лексера компилятором для своего языка программировани…
  2. Sep 28, 2026Пошутил про джавистов в прямом эфире 😅 Шарпизм победил окончательно и бесповоротно!
  3. Sep 27, 2026Post #860
  4. Sep 25, 2026Рассказал как я добавил в HydraScript объекты и методы без классов и сделал протоколы. Стр…
  5. Sep 22, 2026Post #858
  6. Sep 21, 2026Post #857
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →