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

zhaowenchina · 2014年03月08日 · 最后由 xiao__liang 回复于 2016年05月31日 · 22747 次阅读
本帖已被管理员设置为精华贴

首发: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 的优先级比 = 低,而 && / || 的优先级比 =
  • andor 的优先级相同,而 && 的优先级比 ||

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

(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 == 1.0   # => true
1.eql? 1.0 # => false

最佳实践

只使用 == 运算符。

详情

=====eql?equal? 都是互不相同的运算符,各自有不同的用法,分别用于不同的场合。当你要进行比较时,总是使用 ==,除非你有特殊的需求(比如你 真的 需要区分 1.01)或者出于某些原因重写(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()(带括号)在调用父类方法时不带任何参数,正如我们期待的那样。

延伸阅读:Super keyword in Ruby

自定义异常不能继承 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 关键字(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

'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 的坑可能看上去没什么大不了的,乍一看似乎只是属于代码风格和约定的范畴。

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

感谢楼主,果然都是大坑

累觉不爱...

相当好....学习了....

到处都是坑啊。。。。

8 楼 已删除

时间长了就有可能出错,希望 ruby 能够减少那些带歧义的地方,现在记性不好了

话说求解释求帮助……昨天遇到最后那个坑……

class Filter
  class << self
    def foo
       ....
    end
  end
end

然后调用 foo 的时候 rails 告诉我Filter::Class这个 obj 下没有找到 foo………

#10 楼 @cassiuschen 当然不会有啊,class << self之后,你定义的foo方法是类方法啊,不是实例方法,不能通过f=Filter.new; f.foo调用,得Filter.foo调用

感谢!

干货,赞一个

感谢楼主,收益匪浅那,那也来看公司对 rails 的编码规范的要求了。

收藏,洗完澡认真拜读一下!楼主 赞!

我去...大多数坑都踩过.....

收藏!下来认真拜读

#11 楼 @Ryan 原来如此…感谢!

很多是第一次看见的奇葩写法,有些不管在什么语言中看起来都有病的写法我真不相信有人会犯,比如(Foo.bar = 3) == 'OK'…… 这不像是学 Ruby on Rails 会遇到的坑,而是转回头去学 ruby 半桶水的时候才会遇到的坑,rails 的教材从来只会讲最佳实践,不会讲茴香豆的茴字的其他写法,比方说,只学 rails 的时候,我根本没看到过也不可能想到有 class Foo::Bar这种写法…… 后来再把 ruby 元编程过一遍,知道原理后,应该会免疫大部分

张姿势了,感谢楼主

最佳实践:永远不要说永远不要,代码不仅仅是你一个人写的,要看懂别人的代码就要知道各种各样的写法的详情和渊源

感谢楼主!!

#24 楼 @luikore 看不懂别人的代码,可以说“可读性真差呀”

class Foo
  def self.bar=(value)
    @foo = value

    return 'OK'
  end
end

Foo.send("bar=", 1) => "OK"

有意思。

写得正好,

现在有点怀念 python 了

“它会让你心碎。因为你已经累觉不爱。然后孤独终老。永远。" 话说这篇译的真好。Ruby 实在是有很多奇淫技巧。不知道和作者是日本人有没有关系,感觉很符合日本人说话的习惯,一个意思可以用 N 多种不同的说法来说,比方说带敬语的不带敬语的 blablabla...

的确踩过几个坑,楼主威武。收藏了。

可以追加一个 jdk 的坑吗?

class Test {
    public static void main(String[] argv) {
        java.util.Date d = null;
        try {
            d = java.text.DateFormat.getDateInstance().parse("1901-1-1");
        } catch (Exception e) {
        }
        System.out.println(d.toGMTString());
        System.out.println(d.toLocaleString());
    }
}

不同 jdk 版本运行的时间不同,并且随机。1901 年真的是个坑。老大正纠结如何绕过.....

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

Ruby Version 1.9.3-p448, 结果都是false 怎么回事?

Ruby 最大的坑是 private 方法也能被子类继承。

不错,支持下!

#34 楼 @simlegate 你可以看看 surprise 的值

点赞!!

#11 楼 @Ryan 貌似也不行吧。你的这个说法也会报“NoMethodError: private method `bar' called for Foo:Class”

#41 楼 @a307697028 提问的同学没有定义私有,他定义的是公有类方法。。。你加上私有不是肯定不行么。

#42 楼 @Ryan 看见了。哈哈~

楼主,辛苦了。

感谢楼主翻译。

不过,不过我刚看了第一个,就太扯淡了。只用 && 或 ||,万万不敢苟同的。正好相反,在绝大多数情况下,应该使用 and 或 or, 只有在非常罕见的情况下,(涉及优先级不同,高低),才需要用 && 以及 || .

好吧,第二个有关 == 以及 eql? 的更扯淡了,各是各的用途,不解释了。

Rails 代码中捕捉异常一直写的是 rescue Exception => e ,我觉得没有什么大问题,因为 rescue 代码块已经做了处理,不会存在因为捕捉 InterruptSignalException 让程序 hang 住,而且有些异常是未知的,所以打出所有的异常是可行的。

赞一个

#27 楼 @hooopo 是啊,的却有意思。

  • way 1 method = Foo.method('bar=') method.call(3) => "OK"
  • way 2

    Foo.send('bar=', 3) => "'OK"
    
  • way 3

    eval "Foo.bar = 3" => 3
    

最佳实践 只使用 && / || 运算符。

无论如何这都不应该是最佳实践。and/or 的级别低是有其适用场景的。

曾中弹多次,感谢楼主分享总结!

and/or for control flow is my favorite.

初入门,还在看镐头,感谢楼主的分享!

#29 楼 @zlfera 這我有同感,可能是我還不完全理解 Ruby。 不過這些坑會有改善還是已經是 Ruby 法則內認可?

感谢楼主

#51 楼 @gingerhot ,待遇方面𣎴具备吸引力

 感谢楼主分享

重新拾起 ruby 从填坑开始

好贴。mark

好贴,很多细节以前没有注意到

#55 楼 @ksec 随着认识深入,会觉得不这么做才是坑...

在 ruby2.0 中 surprise = true and false surprise = true && false 这两者都是 false。。。。。。

#64 楼 @xifengzhu 1.9.3 里 surprise = true and false 结果也是 false

刚被 and 坑了一下……

@xifengzhu @scott1743 文章里说的是变量 surprise 的值,而不是表达式的值

坑得多了,就学会绕着走了。。

@zhaowenchina 确实是,是个坑..

学习 Mark 一下

非常感谢~

我靠 不知道怎么发帖啊。。。 只能在这里问了:ruby1.9.3 连接 mysql 总是报 Packets out of order: 0<>的错误 是咋回事儿啊各位

#74 楼 @luoxingshe 新用户貌似要过一段时间才能发帖 而且本帖太老了,就算回帖也顶不到首页了。。。

好文,学习了

编辑得辛苦了

收藏,好文

感谢楼主,从填坑开始

踩多了才知道

拜读之后才知道什么叫无知者无畏,我时刻在扮演着无畏的人。

关于 class Foo::Bar 不同于 module Foo; class Bar,存疑:如果将 MY_SCOPE = 'Foo Module' 移至 class Bar 内,则两种方法输出一致。

已经踩到过前面几个坑啦

需要 登录 后方可回复, 如果你还没有账号请 注册新账号