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。java版本的文档称这是"一种比较龌龊但确实可行的方法",这是比较中肯的说法。但是对于rust的enum来说,match是它最自然也最直接的使用方式,不必有这种心理负担,毕竟即使写了个visitor,里面实现的时候不也只能用match吗。

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

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

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

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

Last updated