Ruby traps & pitfalls

用Ruby有一段时间,把发现的gotchas总结一下:

1.不要用继承

本来继承就不是什么好鸟,早有人说过,Java的“extends is evil” 。而除了上述理由,Ruby还有自己的特性,使继承更加糟糕。第一个就是Ruby的instance variable不是提前声明在class definition里的,而是在instance methods执行过程中动态创建的,所以为了避免名字冲突这种毫无征兆、近乎“人品问题”的极难查找的bug,不要继承。即使继承一个自己写的、不暴露的类,也要付出代价--修改基类的时候必须先把所有子类的代码读一遍。第二个理由是Ruby程序不能把method限定成final(as Java)或sealed(as .NET)。继承的时候不小心override掉基类的私有方法就死菜了,而怎么知道自己的自类的方法名和基类的私有方法名不冲突呢?根本不可能知道,因为Ruby的类定义不和Java、C#一样,所有的方法都在一个class{...}里写着,Ruby可以在运行的时候加instance method,所以除非是特别喜欢debug的程序员,不要继承。说到这里,有人是不是觉得Ruby的动态特性很碍手碍脚?我认为不是的,Ruby不鼓励继承,就像structured programming不鼓励goto,但是它提供了if...then...、for、while、switch这些比goto还好的东西。而Ruby给我们提供的,就是meta-programming能力。如果实在想实现继承的效果,比如把class A的接口和实现导入class B,但是B保留大部分,而override小部分实现,怎么办?至少有一种方法:让class B包含一个class A的instance,然后用class B的method_missing把直接使用的A的方法重定向到这个A的instance就可以了。

2.调用自身的setter method,要写self.attr = ...不能写attr = ...。这一点不能不说是Ruby的一个缺陷,因为它绝对是一个bug source。 站在编程语言设计的角度想这个问题,为什么有的语言没有这个问题呢?这应该是Ruby追求programming should be fun付出的小小的代价吧。在其它语言中local variable需要声明,即要有个type var_name,或者至少是let var_name = ...。而在Ruby中,第一次出现就算声明了,大部分时候这个特性的确让程序更简洁了,但在这里却带来了一点麻烦。顺便说一句,JavaScript也有类似的问题:在“var_name = ...”里,JavaScript有可能创建一个var_name变量,也有可能把var_name绑定到一个closure variable上。所以在JavaScript里,“var”关键词一个都不要省!在这里顺便比较一下各种语言的设计。C++/C#/Java类的设计思想是先申明再使用——所以的class data member都要在class定义中列出,函数的本地变量也要申明,因此不存在歧义。Python的数据成员是动态的,但是必须通过“self.”前缀来访问,而self则必须显示的在函数定义中声明。F#混合了以上两种特性——数据成员要先声明,且“this” identifier必须显示写出。总的来说,Ruby追求的是简洁,less typing(双关语),而其它语言追求的是“安全”,即不易出错。关于这一点还有一个淫荡的Python例子:
def counter(start_at=0):
 count=[start_at]
 def incr():
  count[0]+=1
  return count[0]
 return incr

def foo():
 m=4
 def bar():
  m+=1
  return m
 return bar
为何两个代码段看上去如此相似,运行结果卻大相径庭?原因就在于python认为bar函数有一个局部变量m,所以m+=1,或者m=m+1这一句中两个m并没有bind到foo中的m,而是bind到bar的本地变量m。Python之所以认为bar中定义了局部变量m是因为出现了m=...这种m为左值的赋值语句。而在incr中虽然有count[0]=count[0]+1,这个count[0]=...中的count虽然在等于号左边但并不是左值,而是index operation的右值,因此不会引起局部变量的定义;在找不到局部变量count的情况下,Python就只好把count绑定到上面的count= [start_at]语句“引发”的count局部变量。Python的这种“赋值‘引起’局部变量”的设计违法了两个原则:1. 看上去相似的程序语言就应该相似,或者说,程序的微小变化不应该引起语义的巨大变化,这应该是“no surprise rule”精神的体现;2.程序语言的一个结构应该只有一个含义,不应该有“多功能”结构。Python中“x = ...”语句就有两个含义:编译时:“引起”局部变量x,运行时:赋值,这是不好的。由于Python的以上特性,Python的closure变量只能读不能写(除非用counter/incr中的数组hack),为了修正,Python 3引入了“nonlocal”变量声明。扯远了。

3.在static method(其实是class singleton methods)里,self可能是一个继承类,而不是这个static method所属的类(self所指涉类的基类)。

4.mix-in module最好不要有instance variable,和1同样的原因,防止不知情的名字冲突。

5.使用closure的时候要注意,closure不是把变量在形成closure时的值记住了,而是把这个变量记住了,所以如果在这个closure被调用之前变量的值发生了变化,closure在调用时“看到”的是改变后的值。把循环体里的变量装入closure的时候要特别注意。举个例子:

1  arr  =  []
2  for  n  in  ( 1 .. 3 ) do
3    arr.push  lambda { puts n}
4  end
5  arr.map { | l |  l.call }

这个程序输出什么?1 2 3?不是,是3 3 3。原因是3个lambda中的n都是第二行的n。如果要1 2 3,可以这样改一下:

1  arr  =  []
2  for  n  in  ( 1 .. 3 ) do
3    arr.push( lambda { | x |   lambda { puts x}}.call n)
4  end
5  arr.map { | l |  l.call }

同样支持closure的JavaScript也有这个问题,相信不少JavaScript程序员被这种bug咬过。我觉得根本上这反映出一个问题,就是closure更适宜在函数式编程语言里使用。在状态变化的程序里,closure用得好,当然有事半功倍的效果,但较容易“ 触雷”。

6.If a block assigns a value to a variable that is already defined outsided of the block, this does not create a new block-local variable but instead assigns a new value to the already-existing variable(TRPL p142). In Ruby 1.8, if a block parameter shares the name of an existing variable, then invocation of the block simply assign a value to that existing variable rather than creating a new block-local variable(TRPL p143),这一点非常反直觉,所以要特别注意,好在Ruby 1.9修正了这种行为。另外在Ruby 1.9或更高的版本里,一定要声明block local variables。理由和2类似,防止无意中与外面的变量名字冲突。 

7.在方法调用中什么时候要用括号,什么时候不用括号,以及方法名和括号之间不能有空格等等,这些都是小问题,一般不会造成隐藏的bug,教科书写得很明白,不用我多嘴了。

8.The way ruby binds variables in a closure。两点规则:1.Ruby是lexical scoping的,2.能在当前scope中解析到的变量即被绑定,不能解析到的也不会引起错误,而是被当作receiver为self的方法调用,在closure被调用的时候动态解析,因为在Ruby中,变量引用和函数调用的格式恰好是一样的。所以说Ruby closure是lexical scoped when possible, dynamic scoped when needed!举个例子:

 1  v1  =   " outer scope v1 "
 2  =   lambda  { puts  " v1=#{v1} v2=#{v2} " }
 3  def  m
 4     def  v2
 5       " method v2 "
 6    end
 7    v1  =   " inner scope v1 "
 8    v2  =   " inner scope v2 "
 9     yield
10  end
11  & f

 运行的结果是v1=outer scope v1 v2=method v2。显然第二行中的v1通过lexical scoping绑定到了第一行的v1,而v2通过dynamic scoping绑定到了第4-6行定义的方法v2。

9.还是关于closure的,非常的容易让人上当。注意,instance variable,即以@开头的变量,是不会形成closure的,看这截代码:

 1  class  X
 2     def  initialize
 3      @var  =   " counter-intuition "
 4    end
 5    
 6     def  do_this( & block)
 7      instance_eval( & block)
 8    end
 9  end
10 
11  @var  =   " intuition "
12  X.new.do_this { puts @var}

如果只看11、12两行,很容易认为结果是"intuition",但是结果是"counter-intuition"。

好了,先写到这里了。以后有心得再补充。谢谢观看。


你可能感兴趣的:(Ruby)