Visitor позволяет добавлять операции к иерархии объектов без изменения их классов. Классический пример — обход AST, дерева документа или иерархии фигур. Вы создаёте отдельный объект-посетитель, который «обходит» структуру и выполняет нужное действие для каждого типа.
Долгое время это был единственный нормальный способ реализовать double dispatch в C#. Но начиная с C# 8 появился
switch по типам, а в C# 12 эта возможность стала ещё мощнее. И теперь Visitor выглядит как лишняя обвязка.Как выглядел Visitor
Допустим, есть иерархия фигур и нужно посчитать площадь.
public interface IShape
{
void Accept(IShapeVisitor visitor);
}
public class Circle : IShape
{
public double Radius { get; init; }
public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}
public class Rectangle : IShape
{
public double Width { get; init; }
public double Height { get; init; }
public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}
public interface IShapeVisitor
{
void Visit(Circle circle);
void Visit(Rectangle rectangle);
}
public class AreaCalculator : IShapeVisitor
{
public double Result { get; private set; }
public void Visit(Circle circle) =>
Result = Math.PI * circle.Radius * circle.Radius;
public void Visit(Rectangle rectangle) =>
Result = rectangle.Width * rectangle.Height;
}
Два интерфейса, метод
Accept в каждом классе, отдельный класс на каждую операцию. Для двух фигур это терпимо. Для десяти — уже много шаблонного кода.Как это делается через pattern matching
Тот же расчёт площади в одном
switch выражении.public static double CalculateArea(IShape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
_ => throw new ArgumentException($"Unknown shape: {shape.GetType().Name}")
};
Нет интерфейса
IShapeVisitor. Нет метода Accept. Нет отдельного класса калькулятора. Логика читается сверху вниз, в одном месте.Что происходит, когда добавляется новый тип
Это ключевое отличие. Допустим, вы добавили
Triangle.В Visitor нужно добавить метод
Visit(Triangle t) в интерфейс IShapeVisitor, потом реализовать его в каждом посетителе. Если забыли — получите ошибку в рантайме. Или хуже — молчаливое некорректное поведение, если базовый класс имеет реализацию по умолчанию.В
switch выражении компилятор выдаст предупреждение, что Triangle не обработан. Вы узнаете об этом до запуска, на этапе сборки.// CS8509: The switch expression does not handle all possible values
public static double CalculateArea(IShape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height
// Triangle не обработан — компилятор предупредит
};
Когда Visitor всё ещё уместен
Если иерархия типов находится в чужой библиотеке и вы не можете на неё влиять, а
switch по типам становится слишком длинным и разбросанным по кодовой базе — Visitor помогает собрать логику в одном месте. Также он полезен, когда операция требует сложного состояния между вызовами, которое неудобно протаскивать через отдельные методы.Но для большинства задач, где вы контролируете код и типы,
switch выражения проще, короче и безопаснее. Компилятор работает на вашей стороне, а не против вас.📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор