> For the complete documentation index, see [llms.txt](https://mashplant.gitbook.io/decaf-doc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mashplant.gitbook.io/decaf-doc/pa2/visitor-mo-shi.md).

# visitor模式

在找一个类似于`Visitor`之类的trait吗?没有的，别想了。

rust中当然也可以有visitor模式这样的东西，比如<https://github.com/rust-unofficial/patterns/blob/master/patterns/visitor.md> 中给出的例子，不过我认为把`Visitor`像这个文章里一样作为一个明确的trait写出来，一是没有必要，二是限制了灵活性。

先说为什么没有必要。对于java或者是c++的面向对象子集那样的面向对象的语言，ast节点往往表示成继承的关系，例如`Call`继承自`Expr`之类的。如果不写类似于`Visitor`的interface或者是抽象类的话，要想判断并处理不同类型的ast节点，也许只能使用动态的类型判断/转换，也就是java中的`instanceof`+强制类型转换，c++中的`dynamic_cast`。以前的文档(不确定现在还有没有)称这是"一种比较龌龊但确实可行的方法"，其实我个人觉得这并没有什么道理，这并不比visitor模式更不优雅一些，尤其是考虑到类似scala等基于jvm但支pattern matching的语言，本质上pattern matching也是被编译器翻译成`instanceof`+强制类型转换。

总之，对于rust的enum来说，`match`是它最自然也最直接的使用方式，不必有任何心理负担，毕竟即使写了个visitor，里面实现的时候不也只能用`match`吗。

大家会发现代码中有一些非常巨大的函数，例如把所有对于`Expr`的处理全部放在了一个函数里面。我认为这并不会造成什么困扰，因为各个`match`的分支之间是独立的，把它们看成不同的函数也可以，但是我并不认为如果把它真的拆分成不同的函数，在可读性上比现在会有任何的优越性。

再说灵活性。一个trait/interface/抽象类都限定了函数的类型，这对我们来说是完全是没有意义的约束，会带来很多麻烦。例如函数的返回值可能必须为空，那么为了表示visitor从这个节点中获取的信息，就必须把信息存在节点里面，访问完后再取出来；例如函数只能接受节点作为输入参数，那么为了传递一些临时的状态，就必须把这个状态作为struct/class的一个成员。

有人可能认为新增了一种ast节点之后就会出现很多编译错误，这是不灵活的表现，而如果用visitor的话只要在trait里加几个默认的空函数即可。对此我的看法是，编译错误本来就不是坏事，它直接就可以提醒你哪些地方需要修改，这并不比默默的编译通过了但是结果不对要差。如果修改某个地方的工作量的确比较大，又想尽快测试已经修改好的部分，那么填上几个`unimplemented!()`即可。

不写visitor也的确有一些劣势，例如为了判断节点类型，match在每个地方都得出现一次，产生了一些重复的代码。这是个取舍的问题，我个人不觉得这是很大的负担。
