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在每个地方都得出现一次,产生了一些重复的代码。这是个取舍的问题,我个人不觉得这是很大的负担。
Last updated