首发:http://zhaowen.me/blog/2014/03/04/ruby-gotchas/ 原文:Ruby Gotchas that will come back to haunt you
大多数 Ruby on Rails 的初学者们都会为这个出色的框架着迷,在缺乏 Ruby 语言知识的情况下就开始开发应用程序。这也无可厚非。至少,除非这些初学者们坚持了下来,然后摇身一变,成了没有 Ruby 基础知识的「senior」开发者。
不论如何,不管初学者还是有经验的程序员,迟早都会遇到传说中的「Ruby 的坑」。这些平时埋伏很深的语言上的细微之处将会耗费我们数个小时的死命调试( puts "1" ),查明真相后我们会惊呼「怎么会这样??!」,「好吧,我发誓这一次我会去看那本镐头封面的书!」,又或者,我们喊了声「操!」然后就去睡觉了。
我在这篇文章中列举了开发者们需要警惕的 Ruby 中常见的坑。我在每个条目中都给出了示例代码,包括了让人迷惑的或容易出错的代码。
另外,我也给出了最佳实践来简化你(和维护你代码的人)的生活。如果你对「最佳实践」不感冒,你也可以选择阅读详细的解释来了解为什么这个坑会引发 bug(多数情况下是因为它和你所想的不一样)。
and/or
不同于 &&/||
surprise = true and false # => surprise 的值为 true
surprise = true && false # => surprise 的值为 false
只使用 && / ||
运算符。
and / or
运算符的优先级比 && / ||
低and / or
的优先级比 =
低,而 && / ||
的优先级比 =
高and
和 or
的优先级相同,而 &&
的优先级比 ||
高我们来给上述示例代码加上括号,这样就可以明显地看出 and
和 &&
在用法上的不同之处了。
(surprise = true) and false # => surprise is true
surprise = (true && false) # => surprise is false
也有这样的说法:and / or
用于流程控制,而 && / ||
用于布尔型运算。但我认为:不要使用这些运算符的关键字版本(and / or / not
),而使用更为清晰的 if
和 unless
等。更加明确,更少困惑,更少 bugs。
延伸阅读:Difference between “or” and || in Ruby?
eql?
不同于 ==
(也不同于 equal?
或 ===
)1 == 1.0 # => true
1.eql? 1.0 # => false
只使用 ==
运算符。
==
、===
、eql?
和 equal?
都是互不相同的运算符,各自有不同的用法,分别用于不同的场合。当你要进行比较时,总是使用 ==
,除非你有特殊的需求(比如你 真的 需要区分 1.0
和 1
)或者出于某些原因重写(override)了某个运算符。
没错,eql?
可能看起来要比平凡的 ==
更为 聪明 ,但是你真的需要这样吗,去 区分两个相同的东西 ?
延伸阅读:What’s the difference between equal?, eql?, ===, and ==?
super
不同于 super()
class Foo
def show
puts 'Foo#show'
end
end
class Bar < Foo
def show(text)
super
puts text
end
end
Bar.new.show('test') # ArgumentError: wrong number of arguments (1 for 0)
在这里,省略括号可不仅仅是品味(或约定)的问题,而是确实会影响代码的逻辑。
super
(没有括号)调用父类方法时,会将传给这个方法的参数原封不动地传给父类方法(因此在 Bar#show
里面的 super
会变成 super('test')
,引发了错误,因为父类的方法不接收参数)super()
(带括号)在调用父类方法时不带任何参数,正如我们期待的那样。Exception
class MyException < Exception
end
begin
raise MyException
rescue
puts 'Caught it!'
end
# MyException: MyException
# from (irb):17
# from /Users/karol/.rbenv/versions/2.1.0/bin/irb:11:in `<main>'
(上述代码不会捕捉到 MyException
,也不会显示 'Caught it!'
的消息。)
StandardError
或任何其后代子类(越精确越好)。永远不要直接继承 Exception
。rescue Exception
。如果你想要大范围捕捉异常,直接使用空的 rescue
语句(或者使用 rescue => e
来访问错误对象)。rescue
语句时,它会捕捉所有继承自 StandardError
的异常,而不是 Exception
。rescue Exception
(当然你不应该这样),你会捕捉到你无法恢复的错误(比如内存溢出错误)。而且,你会捕捉到 SIGTERM 这样的系统信号,导致你无法使用 CTRL+C 来中止你的脚本。延伸阅读:Why is it bad style to `rescue Exception => e` in Ruby?
class Foo::Bar
不同于 module Foo; class Bar
MY_SCOPE = 'Global'
module Foo
MY_SCOPE = 'Foo Module'
class Bar
def scope1
puts MY_SCOPE
end
end
end
class Foo::Bar
def scope2
puts MY_SCOPE
end
end
Foo::Bar.new.scope1 # => "Foo Module"
Foo::Bar.new.scope2 # => "Global"
总是使用长的,更清晰的,module 把 class 包围的写法:
module Foo
class Bar
end
end
module
关键字(class
和 def
也一样)会对其包围的区域创建新的词法作用域(lexical scope)。所以,上面的 module Foo
创建了 'Foo'
作用域,常量 MY_SCOPE
和它的值 'Foo Module'
就在其中。class Bar
,又会创建新的词法作用域(名为 'Foo::Bar'
),它能够访问父作用域('Foo'
)和定义在其中的所有常量。::
「捷径」来声明 Foo::Bar 时,class Foo::Bar
又创建了一个新的词法作用域,名字也叫 'Foo::Bar'
,但它没有父作用域,因此不能访问 'Foo'
里面的东西。class Foo::Bar
中我们只能访问定义在脚本的开头的 MY_SCOPE
常量(不在任何 module 中),其值为 'Global'
。延伸阅读:Ruby – Lexical scope vs Inheritance
bang!
方法如果什么都没做就会返回 nil
'foo'.upcase! # => "FOO"
'FOO'.upcase! # => nil
永远不要依赖于内建的 bang!
方法的返回值,比如在条件语句或流程控制中:
@name.upcase! and render :show
上面的代码会造成一些无法预测的行为(或者更准备地说,我们可以预测到当 @name
已经是全部大写的时候就会失败)。另外,这个示例也再一次说明了为什么你不应该使用 and/or
来控制流程。敲两个回车吧,不会有树被砍的。
@name.upcase!
render :show
attribute=(value)
方法永远返回传给它的 value
而无视 return
值class Foo
def self.bar=(value)
@foo = value
return 'OK'
end
end
Foo.bar = 3 # => 3
(注意这个赋值方法 bar=
返回了 3
,尽管我们显式地在最后 return 'OK'
。)
永远不要依赖赋值方法的返回值,比如下面的条件语句:
puts 'Assigned' if (Foo.bar = 3) == 'OK' # => nil
显然这个语句不会如你所想。
延伸阅读:ruby, define []= operator, why can’t control return value?
private
并不会让你的 self.method
成为私有方法class Foo
private
def self.bar
puts 'Not-so-private class method called'
end
end
Foo.bar # => "Not-so-private class method called"
(注意,如果这个方法真的是私有方法,那么 Foo.bar
就会抛出 NoMethodError
。)
要让你的类方法变得私有,你需要使用 private_class_method :method_name
或者把你的私有类方法放到 class << self
block 中:
class Foo
class << self
private
def bar
puts 'Class method called'
end
end
def self.baz
puts 'Another class method called'
end
private_class_method :baz
end
Foo.bar # => NoMethodError: private method `bar' called for Foo:Class
Foo.baz # => NoMethodError: private method `baz' called for Foo:Class
延伸阅读:creating private class method
上面列举的 Ruby 的坑可能看上去没什么大不了的,乍一看似乎只是属于代码风格和约定的范畴。
但相信我,如果你不重视它们,终有一天你会在 Ruby on Rails 的开发过程中碰到诡异无比的问题。它会让你心碎。因为你已经累觉不爱。然后孤独终老。永远。