访问者模式的核心是分离操作与数据结构,通过IVisitor和IElement接口实现双分派,在不修改元素类的前提下新增操作;要求每个元素子类显式实现Accept调用visitor.Visit(this),确保类型精确分发。

访问者模式的核心是分离「操作」与「数据结构」
它不是为了解耦类与类,而是为了在不修改原有类的前提下,给对象结构里的元素增加新操作。典型场景是:你有一组类型固定的类(比如 Expression 的各种子类),但后续会不断新增不同类型的处理逻辑(求值、打印、优化、序列化),这时硬编码每种操作到每个类里会导致大量重复和维护困难。
必须实现的两个关键接口:IVisitor 和 IElement
所有被访问的元素都要实现 IElement,它只定义一个 Accept(IVisitor visitor) 方法;所有访问者实现 IVisitor,里面按需重载多个 Visit(XxxElement element) 方法:
public interface IElement
{
void Accept(IVisitor visitor);
}
<p>public interface IVisitor
{
void Visit(NumberElement element);
void Visit(AddElement element);
void Visit(MultiplyElement element);
}注意:C# 没有方法重载的运行时分发(不像 Java 的 double dispatch 机制),所以 Accept 方法内部必须显式调用 visitor.Visit(this),让 this 的实际类型参与分派。
- 如果漏写
Accept中的Visit(this),访问者永远收不到回调 - 如果
IVisitor接口里漏了某个子类的Visit方法,编译期就报错,这是类型安全的关键 - 不要试图用
dynamic绕过接口定义——会丢失编译检查,也违背模式本意
双分派靠 Accept + Visit 配合完成
例如 AddElement 的 Accept 实现必须是:
public void Accept(IVisitor visitor)
{
visitor.Visit(this); // this 是 AddElement 类型,触发 Visit(AddElement)
}而不能写成 visitor.Visit((IElement)this) 或其他转型——那样只会调用 Visit(IElement)(如果存在),失去类型精度。
- C# 编译器根据
this的**静态类型**选择Visit重载,所以Accept必须在每个子类中单独实现 - 无法用基类统一实现
Accept(除非用反射或dynamic,但代价高且不可靠) - 如果元素类型太多,手写每个
Accept容易遗漏,建议配合 Roslyn 分析器或源生成器自动补全
访问者不适合频繁增删元素类型
每加一个新元素类,就要改 IVisitor 接口、所有已实现的访问者类、以及所有已有 Accept 方法。这违反开闭原则的“对扩展开放”部分——它适合操作频繁变、结构相对稳定的情况。
- 如果你的
Expression子类经常新增(比如每周加一种节点),访问者模式反而增加负担 - 此时更推荐用
switch表达式 +pattern matching(C# 8+),或者策略字典 +Type映射 - 访问者真正的优势在于:当你要一次性引入 5 种新操作(比如导出 JSON、生成 SQL、做类型推导、计算复杂度、做 AST 可视化),而元素类型只有 4–5 个且基本不变时
最常被忽略的一点是:访问者本身的状态管理。多个 Visit 调用之间往往需要共享上下文(比如当前缩进、作用域栈、错误列表),这些不能放在 IVisitor 接口里,得由具体访问者类自己维护——别忘了初始化和复用边界。










