Ruby 中那些你绕不过的「坑」

原文:Ruby Gotchas that will come back to haunt you

大多数 Ruby on Rails 的初学者们都会为这个出色的框架着迷,在缺乏 Ruby 语言知识的情况下就开始开发应用程序。这也无可厚非。至少,除非这些初学者们坚持了下来,然后摇身一变,成了没有 Ruby 基础知识的「senior」开发者。

不论如何,不管初学者还是有经验的程序员,迟早都会遇到传说中的「Ruby 的坑」。这些平时埋伏很深的语言上的细微之处将会耗费我们数个小时的死命调试(puts “1”),查明真相后我们会惊呼「怎么会这样??!」,「好吧,我发誓这一次我会去看那本镐头封面的书!」,又或者,我们喊了声「操!」然后就去睡觉了。

我在这篇文章中列举了开发者们需要警惕的 Ruby 中常见的坑。我在每个条目中都给出了示例代码,包括了让人迷惑的或容易出错的代码。

另外,我也给出了最佳实践来简化你(和维护你代码的人)的生活。如果你对「最佳实践」不感冒,你也可以选择阅读详细的解释来了解为什么这个坑会引发 bug(多数情况下是因为它和你所想的不一样)。

and/or 不同于 &&/||

1
2
surprise = true and false # => surprise is true
surprise = true && false # => surprise is false

最佳实践

只使用 && / || 运算符。

详情

  • and / or 运算符的优先级比 && / ||
  • and / or 的优先级比 = 低,而 && / || 的优先级比 =
  • andor 的优先级相同,而 && 的优先级比 ||

我们来给上述示例代码加上括号,这样就可以明显地看出 and&& 在用法上的不同之处了。

1
2
(surprise = true) and false # => surprise is true
surprise = (true && false) # => surprise is false

也有这样的说法:and / or 用于流程控制,而 && / || 用于布尔型运算。但我认为:不要使用这些运算符的关键字版本(and / or / not),而使用更为清晰的 ifunless 等。更加明确,更少困惑,更少 bugs。

延伸阅读:Difference between “or” and || in Ruby?

eql? 不同于 ==(也不同于 equal?===

1
2
1 == 1.0 # => true
1.eql? 1.0 # => false

最佳实践

只使用 == 运算符。

详情

=====eql?equal? 都是互不相同的运算符,各自有不同的用法,分别用于不同的场合。当你要进行比较时,总是使用 ==,除非你有特殊的需求(比如你真的需要区分 1.01)或者出于某些原因重写(override)了某个运算符。

没错,eql? 可能看起来要比平凡的 == 更为聪明,但是你真的需要这样吗,去区分两个相同的东西

延伸阅读:What’s the difference between equal?, eql?, ===, and ==?

super 不同于 super()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()(带括号)在调用父类方法时不带任何参数,正如我们期待的那样。

延伸阅读:Super keyword in Ruby

自定义异常不能继承 Exception

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 包围的写法:

1
2
3
4
module Foo
class Bar
end
end

详情

  • module 关键字(classdef 也一样)会对其包围的区域创建新的词法作用域(lexical scope)。所以,上面的 module Foo 创建了 'Foo' 作用域,常量 MY_SCOPE 和它的值 'Foo Module' 就在其中。
  • 在这个 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

1
2
'foo'.upcase! # => "FOO"
'FOO'.upcase! # => nil

最佳实践

永远不要依赖于内建的 bang! 方法的返回值,比如在条件语句或流程控制中:

1
@name.upcase! and render :show

上面的代码会造成一些无法预测的行为(或者更准备地说,我们可以预测到当 @name 已经是全部大写的时候就会失败)。另外,这个示例也再一次说明了为什么你不应该使用 and/or 来控制流程。敲两个回车吧,不会有树被砍的。

1
2
3
@name.upcase!
render :show

attribute=(value) 方法永远返回传给它的 value 而无视 return

1
2
3
4
5
6
7
8
9
class Foo
def self.bar=(value)
@foo = value
return 'OK'
end
end
Foo.bar = 3 # => 3

(注意这个赋值方法 bar= 返回了 3,尽管我们显式地在最后 return 'OK'。)

最佳实践

永远不要依赖赋值方法的返回值,比如下面的条件语句:

1
puts 'Assigned' if (Foo.bar = 3) == 'OK' # => nil

显然这个语句不会如你所想。

延伸阅读:ruby, define []= operator, why can’t control return value?

private 并不会让你的 self.method 成为私有方法

1
2
3
4
5
6
7
8
9
10
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 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 的坑可能看上去没什么大不了的,乍一看似乎只是属于代码风格和约定的范畴。

但相信我,如果你不重视它们,终有一天你会在 Ruby on Rails 的开发过程中碰到诡异无比的问题。它会让你心碎。因为你已经累觉不爱。然后孤独终老。永远。