榜样很重要。
——墨菲警官《机器战警》
身为 Ruby 开发者,有件总是令我烦心的事——Python 开发者有一份好的编程风格参考指南(PEP-8)而我们永远没有一份官方指南,一份记录 Ruby 编程风格及最佳实践的指南。我确信风格很重要。我也相信像 Ruby 这样的黑客社区应该可以自己编写这梦寐以求的文档。
这份指南开始是作为我们公司内部的 Ruby 编程指南(由我所写的)。后来,我决定把成果贡献给广大的 Ruby 社区,况且这个世界再多一份公司内部文档又有何意义。然而由社区制定及策动的一系列 Ruby 编程实践、惯例及风格确能让世界受益。
从编写这份指南开始,我收到了来自世界各地优秀 Ruby 社区的很多用户反馈。衷心感谢所有建议及帮助!同心协力,我们能创造出让每一个 Ruby 开发者受益的资源。
顺道一提,如果你对 Rails 感兴趣,你可以看看这份 Ruby on Rails 风格指南。
这份 Ruby 风格指南推荐的是 Ruby 的最佳实践,现实世界中的 Ruby 程序员据此可以写出可维护的高质量代码。我们只说实际使用中的用法。指南再好,但里面说的过于理想化结果大家拒绝使用或者可能根本没人用,又有何意义。
本指南分为几个小节,每一小节由几条相关的规则构成。我尽力在每条规则后面说明理由(如果省略了说明,那是因为其理由显而易见)。
这些规则不是我凭空想象出来的——它们中的绝大部分来自我多年以来作为职业软件工程师的经验,来自 Ruby 社区成员的反馈和建议,以及几个评价甚高的 Ruby 编程资源,像《Programming Ruby》以及《The Ruby Programming Language》。
Ruby 社区尚未就某些规则达成明显的共识,比如字符串字面量的引号、哈希字面量两端是否应该添加空格、多行链式方法调用中 .
操作符的位置。对于这种情况,本指南列出了所有可选的流行风格,你可以任选其一并坚持使用。
本指南会一直更新,随着 Ruby 本身的发展,新的规则会添加进来,过时的规则会被剔除。
许多项目有其自己的编程风格指南(往往是源于本指南而创建)。当项目的风格指南与本指南发生冲突时,应以项目级的指南为准。
你可以使用 Pandoc 生成本指南的 PDF 或 HTML 版本。
RuboCop 工具会自动检查你的 Ruby 代码是否符合这份 Ruby 风格指南。
本指南有以下翻译版本:
所有风格都又丑又难读,自己的除外。几乎人人都这样想。把“自己的除外”拿掉,他们或许是对的...
——Jerry Coffin(论缩排)
-
使用
UTF-8
作为源文件的编码。 [link] -
每个缩排层级使用两个空格。不要使用制表符。 [link]
# 差 - 四个空格 def some_method do_something end # 好 def some_method do_something end
-
使用 Unix 风格的换行符。(*BSD/Solaris/Linux/OS X 系统的用户不需担心,Windows 用户则要格外小心。) [link]
-
如果你使用 Git,可用下面这个配置来保护你的项目不被 Windows 的换行符干扰:
$ git config --global core.autocrlf true
-
-
不要使用
;
隔开语句与表达式。推论:一行一条语句。 [link]# 差 puts 'foobar'; # 不必要的分号 puts 'foo'; puts 'bar' # 一行里有两个表达式 # 好 puts 'foobar' puts 'foo' puts 'bar' puts 'foo', 'bar' # 仅对 puts 适用
-
对于没有主体的类,倾向使用单行定义。 [link]
# 差 class FooError < StandardError end # 勉强可以 class FooError < StandardError; end # 好 FooError = Class.new(StandardError)
-
定义方法时,避免单行写法。尽管这种写法有时颇为普遍,但其略显古怪的定义语法容易使人犯错。无论如何,至少保证单行写法的方法不应该拥有一个以上的表达式。 [link]
# 差 def too_much; something; something_else; end # 勉强可以 - 注意第一个 ; 是必选的 def no_braces_method; body end # 勉强可以 - 注意第二个 ; 是可选的 def no_braces_method; body; end # 勉强可以 - 语法正确,但没有 ; 使得可读性欠佳 def some_method() body end # 好 def some_method body end
这个规则的一个例外是空方法。
# 好 def no_op; end
-
操作符前后适当地添加空格,在逗号
,
、冒号:
及分号;
之后。尽管 Ruby 解释器(大部分情况下)会忽略空格,但适量的空格可以增强代码的可读性。 [link]sum = 1 + 2 a, b = 1, 2 class FooError < StandardError; end
(对于操作符)唯一的例外是当使用指数操作符时:
# 差 e = M * c ** 2 # 好 e = M * c**2
-
(
、[
之后,]
、)
之前,不要添加任何空格。在{
前后,在}
之前添加空格。 [link]# 差 some( arg ).other [ 1, 2, 3 ].each{|e| puts e} # 好 some(arg).other [1, 2, 3].each { |e| puts e }
{
与}
需要额外说明,因为它们可以同时用在区块、哈希字面量及字符串插值中。对于哈希字面量,有两种可被接受的风格。第一种风格更具可读性(在 Ruby 社区里似乎更为流行)。第二种风格的优点是,在视觉上使得区块与哈希字面量有所区分。无论你选择何种风格,务必在使用时保持连贯性。
# 好 - { 之后 与 } 之前有空格 { one: 1, two: 2 } # 好 - { 之后 与 } 之前无空格 {one: 1, two: 2}
对于插值表达式,括号内两端不要添加空格。
# 差 "From: #{ user.first_name }, #{ user.last_name }" # 好 "From: #{user.first_name}, #{user.last_name}"
-
!
之后,不要添加任何空格。 [link]# 差 ! something # 好 !something
-
范围的字面量语法中,不要添加任何空格。 [link]
# 差 1 .. 3 'a' ... 'z' # 好 1..3 'a'...'z'
-
把
when
与case
缩排在同一层级。这是《Programming Ruby》与《The Ruby Programming Language》中早已确立的风格。 [link]# 差 case when song.name == 'Misty' puts 'Not again!' when song.duration > 120 puts 'Too long!' when Time.now.hour > 21 puts "It's too late" else song.play end # 好 case when song.name == 'Misty' puts 'Not again!' when song.duration > 120 puts 'Too long!' when Time.now.hour > 21 puts "It's too late" else song.play end
-
当将一个条件表达式的结果赋值给一个变量时,保持分支缩排在同一层级。 [link]
# 差 - 非常费解 kind = case year when 1850..1889 then 'Blues' when 1890..1909 then 'Ragtime' when 1910..1929 then 'New Orleans Jazz' when 1930..1939 then 'Swing' when 1940..1950 then 'Bebop' else 'Jazz' end result = if some_cond calc_something else calc_something_else end # 好 - 结构清晰 kind = case year when 1850..1889 then 'Blues' when 1890..1909 then 'Ragtime' when 1910..1929 then 'New Orleans Jazz' when 1930..1939 then 'Swing' when 1940..1950 then 'Bebop' else 'Jazz' end result = if some_cond calc_something else calc_something_else end # 好 - 并且更好地利用行宽 kind = case year when 1850..1889 then 'Blues' when 1890..1909 then 'Ragtime' when 1910..1929 then 'New Orleans Jazz' when 1930..1939 then 'Swing' when 1940..1950 then 'Bebop' else 'Jazz' end result = if some_cond calc_something else calc_something_else end
-
在各个方法定义之间添加空行,并且将方法分成若干合乎逻辑的段落。 [link]
def some_method data = initialize(options) data.manipulate! data.result end def some_method result end
-
在各个段落之间,使用一个空行分隔。 [link]
# 差 - 使用了两个空行 some_method some_method # 好 some_method some_method
-
在属性修饰器之后,使用一个空行分隔。 [link]
# 差 class Foo attr_reader :foo def foo # 做一些事情 end end # 好 class Foo attr_reader :foo def foo # 做一些事情 end end
-
在不同缩进的代码之间,不要使用空行分隔。 [link]
# 差 class Foo def foo begin do_something do something end rescue something end end end # 好 class Foo def foo begin do_something do something end rescue something end end end
-
避免在方法调用的最后一个参数之后添加逗号,尤其当参数没有分布在同一行时。 [link]
# 差 - 尽管移动、新增、删除参数颇为方便,但仍不推荐这种写法 some_method( size, count, color, ) # 差 some_method(size, count, color, ) # 好 some_method(size, count, color)
-
当给方法的参数赋予默认值时,在
=
前后添加空格。 [link]# 差 def some_method(arg1=:default, arg2=nil, arg3=[]) # 做一些事情 end # 好 def some_method(arg1 = :default, arg2 = nil, arg3 = []) # 做一些事情 end
尽管有几本 Ruby 书籍推荐使用第一种风格,但第二种在实践中更为常见(而且似乎更具可读性)。
-
避免在非必要的情形下使用续行符
\
。在实践中,除了字符串拼接,避免在其他任何地方使用续行。 [link]# 差 result = 1 - \ 2 # 好 - 但仍然丑到爆 result = 1 \ - 2 long_string = 'First part of the long string' \ ' and second part of the long string'
-
使用统一的风格进行多行链式方法调用。在 Ruby 社区中存在两种流行的风格:前置
.
(风格 A)与后置.
(风格 B)。 [link]-
(风格 A) 当多行链式方法调用需要另起一行继续时,将
.
放在第二行开头。# 差 - 需要查看第一行才能理解第二行在做什么 one.two.three. four # 好 - 立刻能够明白第二行在做什么 one.two.three .four
-
(风格 B) 将
.
放在第一行末尾,以表示当前表达式尚未结束。# 差 - 需要查看第二行才能知道链式方法调用是否结束 one.two.three .four # 好 - 立刻能够明白第二行还有其他方法调用 one.two.three. four
两种风格各自优点查阅这里。
-
-
当方法调用参数过长时,将它们排列在多行并对齐。若对齐后长度超过行宽限制,将首个参数位置挪到下一行进行缩排也是可以接受的。 [link]
# 初始(行太长了) def send_mail(source) Mailer.deliver(to: 'bob@example.com', from: 'us@example.com', subject: 'Important message', body: source.text) end # 差 - 双倍缩排 def send_mail(source) Mailer.deliver( to: 'bob@example.com', from: 'us@example.com', subject: 'Important message', body: source.text) end # 好 def send_mail(source) Mailer.deliver(to: 'bob@example.com', from: 'us@example.com', subject: 'Important message', body: source.text) end # 好 - 普通缩排 def send_mail(source) Mailer.deliver( to: 'bob@example.com', from: 'us@example.com', subject: 'Important message', body: source.text ) end
-
当构建数组时,若元素跨行,应当保持对齐。 [link]
# 差 - 没有对齐 menu_item = ['Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Baked beans', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam'] # 好 menu_item = [ 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Baked beans', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam' ] # 好 menu_item = ['Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Baked beans', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam']
-
使用
_
语法改善大数的数值字面量的可读性。 [link]# 差 - 有几个零? num = 1000000 # 好 - 方便人脑理解 num = 1_000_000
-
当数值需要前缀标识进制时,倾向使用小写字母。使用
0o
标识八进制,使用0x
标识十六进制,使用0b
标识二进制。十进制数值无需前缀(0d
)标识。 [link]# 差 num = 01234 num = 0O1234 num = 0X12AB num = 0B10101 num = 0D1234 num = 0d1234 # 好 - 方便区分数值前缀与具体数字 num = 0o1234 num = 0x12AB num = 0b10101 num = 1234
-
将单行长度控制在 80 个字符内。 [link]
-
避免行尾空格。 [link]
-
文件以空白行结束。 [link]
-
不要使用区块注释。它们不能被空白字符引导,且不如常规注释容易辨认。 [link]
# 差 =begin comment line another comment line =end # 好 # comment line # another comment line
-
使用
::
引用常量(包括类与模块)与构造器(比如Array()
、Nokogiri::HTML()
)。不要使用::
调用常规方法。 [link]# 差 SomeClass::some_method some_object::some_method # 好 SomeClass.some_method some_object.some_method SomeModule::SomeClass::SOME_CONST SomeModule::SomeClass()
-
使用
def
定义方法时,如果有参数则使用括号,如果无参数则省略括号。 [link]# 差 def some_method() # 省略主体 end # 好 def some_method # 省略主体 end # 差 def some_method_with_parameters param1, param2 # 省略主体 end # 好 def some_method_with_parameters(param1, param2) # 省略主体 end
-
方法调用应当使用括号包裹参数,尤其是第一个参数以
(
开头时,比如f((3 + 2) + 1)
; [link]x = Math.sin y # 差 x = Math.sin(y) # 好 array.delete e # 差 array.delete(e) # 好 temperance = Person.new 'Temperance', 30 # 差 temperance = Person.new('Temperance', 30) # 好
但在下述情况下可以省略括号:
-
无参调用
# 差 Kernel.exit!() 2.even?() fork() 'test'.upcase() # 好 Kernel.exit! 2.even? fork 'test'.upcase
-
内部 DSL 的组成部分(比如 Rake、Rails、RSpec)
validates(:name, presence: true) # 差 validates :name, presence: true # 好
-
具有“关键字”特性的方法
class Person attr_reader(:name, :age) # 差 attr_reader :name, :age # 好 # 省略主体 end puts(temperance.age) # 差 puts temperance.age # 好
-
-
定义可选参数时,将可选参数放置在参数列表尾部。如果可选参数出现在列表头部,则此方法在调用时可能会产生预期之外的结果。 [link]
# 差 def some_method(a = 1, b = 2, c, d) puts "#{a}, #{b}, #{c}, #{d}" end some_method('w', 'x') # => '1, 2, w, x' some_method('w', 'x', 'y') # => 'w, 2, x, y' some_method('w', 'x', 'y', 'z') # => 'w, x, y, z' # 好 def some_method(c, d, a = 1, b = 2) puts "#{a}, #{b}, #{c}, #{d}" end some_method('w', 'x') # => '1, 2, w, x' some_method('w', 'x', 'y') # => 'y, 2, w, x' some_method('w', 'x', 'y', 'z') # => 'y, z, w, x'
-
定义变量时,避免并行赋值。但当右值为方法调用返回值,或是与
*
操作符配合使用,或是交换两个变量的值,并行赋值也是可以接受的。并行赋值的可读性通常不如分开赋值。 [link]# 差 a, b, c, d = 'foo', 'bar', 'baz', 'foobar' # 好 a = 'foo' b = 'bar' c = 'baz' d = 'foobar' # 好 - 交换两个变量的值 a = 'foo' b = 'bar' a, b = b, a puts a # => 'bar' puts b # => 'foo' # 好 - 右值为方法调用返回值 def multi_return [1, 2] end first, second = multi_return # 好 - 与 * 操作符配合使用 first, *list = [1, 2, 3, 4] # first => 1, list => [2, 3, 4] hello_array = *'Hello' # => ["Hello"] a = *(1..3) # => [1, 2, 3]
-
除非必要,否则避免在并行赋值时使用单字符的
_
变量。优先考虑前缀形式的下划线变量,而不是直接使用_
,因为前者可以提供一定的语义信息。但当赋值语句左侧出现带*
操作符的变量时,使用_
也是可以接受的。 [link]foo = 'one,two,three,four,five' # 差 - 可有可无,且无任何有用信息 first, second, _ = foo.split(',') first, _, _ = foo.split(',') first, *_ = foo.split(',') # 好 a, = foo.split(',') a, b, = foo.split(',') # 好 - 可有可无,但提供了额外信息 first, _second = foo.split(',') first, _second, = foo.split(',') first, *_ending = foo.split(',') # 好 - 占位符,_ 担当最后一个元素 *beginning, _ = foo.split(',') *beginning, something, _ = foo.split(',')
-
永远不要使用
for
, 除非你很清楚为什么。大部分情况下,你应该使用迭代器。for
是由each
实现的,所以你绕弯了。另外,for
没有引入一个新的作用域 (each
有),因此在它内部定义的变量在外部仍是可见的。 [link]arr = [1, 2, 3] # 差 for elem in arr do puts elem end # 注意,elem 可在 for 循环外部被访问 elem # => 3 # 好 arr.each { |elem| puts elem } # 注意,elem 不可在 each 块外部被访问 elem # => NameError: undefined local variable or method `elem'
-
永远不要在多行
if
/unless
中使用then
。 [link]# 差 if some_condition then # 省略主体 end # 好 if some_condition # 省略主体 end
-
在多行
if/unless
中,总是把条件表达式与if/unless
放置在同一行。 [link]# 差 if some_condition do_something do_something_else end # 好 if some_condition do_something do_something_else end
-
倾向使用三元操作符(
?:
)而不是if/then/else/end
结构。前者更为常见且简练。 [link]# 差 result = if some_condition then something else something_else end # 好 result = some_condition ? something : something_else
-
三元操作符的每个分支只写一个表达式。即不要嵌套三元操作符。嵌套情况请使用
if/else
结构。 [link]# 差 some_condition ? (nested_condition ? nested_something : nested_something_else) : something_else # 好 if some_condition nested_condition ? nested_something : nested_something_else else something_else end
-
永远不要使用
if x; ...
。使用三元操作符来替代。 [link]# 差 result = if some_condition; something else something_else end # 好 result = some_condition ? something : something_else
-
利用“
if
与case
是表达式”的这个特性。 [link]# 差 if condition result = x else result = y end # 好 result = if condition x else y end
-
在
case
表达式中,单行情况使用when x then ...
语法。另一种语法when x: ...
在 Ruby 1.9 之后被移除了。 [link] -
不要使用
when x; ...
语法。参考前一条规则。 [link] -
使用
!
而不是not
。 [link]# 差 - 因为操作符的优先级,这里必须使用括号 x = (not something) # 好 x = !something
-
避免使用
!!
。 [link]!!
会将表达式结果转换为布尔值,但对于流程控制的表达式通常并不需要如此显式的转换过程。如果需要做nil
检查,那么调用对象的nil?
方法。# 差 x = 'test' # 令人费解的 nil 检查 if !!x # 省略主体 end # 好 x = 'test' if x # 省略主体 end
-
永远不要使用
and
与or
关键字。使用&&
与||
来替代。 [link]# 差 # 布尔表达式 ok = got_needed_arguments and arguments_are_valid # 流程控制 document.save or fail(RuntimeError, "Failed to save document!") # 好 # 布尔表达式 ok = got_needed_arguments && arguments_are_valid # 流程控制 fail(RuntimeError, "Failed to save document!") unless document.save # 流程控制 document.save || fail(RuntimeError, "Failed to save document!")
-
避免使用多行三元操作符(
?:
)。使用if
/unless
来替代。 [link] -
对于单行主体,倾向使用
if
/unless
修饰语法。另一种方法是使用流程控制&&
/||
。 [link]# 差 if some_condition do_something end # 好 do_something if some_condition # 好 - 使用流程控制 some_condition && do_something
-
避免在多行区块后使用
if
/unless
修饰语法。 [link]# 差 10.times do # 省略多行主体 end if some_condition # 好 if some_condition 10.times do # 省略多行主体 end end
-
避免使用嵌套
if
/unless
/while
/until
修饰语法。适当情况下,使用&&
/||
来替代。 [link]# 差 do_something if other_condition if some_condition # 好 do_something if some_condition && other_condition
-
对于否定条件,倾向使用
unless
而不是if
(或是使用流程控制||
)。 [link]# 差 do_something if !some_condition # 差 do_something if not some_condition # 好 do_something unless some_condition # 好 some_condition || do_something
-
不要使用
unless
与else
的组合。将它们改写成肯定条件。 [link]# 差 unless success? puts 'failure' else puts 'success' end # 好 if success? puts 'success' else puts 'failure' end
-
不要使用括号包裹流程控制中的条件表达式。 [link]
# 差 if (x > 10) # 省略主体 end # 好 if x > 10 # 省略主体 end
这个规则的一个例外是条件表达式中的安全赋值。
-
在多行
while/until
中,不要使用while/until condition do
。 [link]# 差 while x > 5 do # 省略主体 end until x > 5 do # 省略主体 end # 好 while x > 5 # 省略主体 end until x > 5 # 省略主体 end
-
对于单行主体,倾向使用
while/until
修饰语法。 [link]# 差 while some_condition do_something end # 好 do_something while some_condition
-
对于否定条件,倾向使用
until
而不是while
。 [link]# 差 do_something while !some_condition # 好 do_something until some_condition
-
对于无限循环,使用
Kernel#loop
而不是while/until
。 [link]# 差 while true do_something end until false do_something end # 好 loop do do_something end
-
对于后置条件循环语句,倾向使用
Kernel#loop
与break
的组合,而不是begin/end/until
或begin/end/while
。 [link]# 差 begin puts val val += 1 end while val < 0 # 好 loop do puts val val += 1 break unless val < 0 end
-
对于可选参数的哈希,省略其外围的花括号。 [link]
# 差 user.set({ name: 'John', age: 45, permissions: { read: true } }) # 好 user.set(name: 'John', age: 45, permissions: { read: true })
-
对于 DSL 的内部方法调用,同时省略其外围的圆括号与花括号。 [link]
class Person < ActiveRecord::Base # 差 validates(:name, { presence: true, length: { within: 1..10 } }) # 好 validates :name, presence: true, length: { within: 1..10 } end
-
当被调用方法是当前区块中唯一操作时,倾向使用简短的传参语法。 [link]
# 差 names.map { |name| name.upcase } # 好 names.map(&:upcase)
-
对于单行主体,倾向使用
{...}
而不是do...end
。对于多行主体,避免使用{...}
。对于“流程控制”或“方法定义”(比如 Rakefile、其他 DSL 构成片段),总是使用do...end
。避免在链式方法调用中使用do...end
。 [link]names = %w[Bozhidar Steve Sarah] # 差 names.each do |name| puts name end # 好 names.each { |name| puts name } # 差 names.select do |name| name.start_with?('S') end.map { |name| name.upcase } # 好 names.select { |name| name.start_with?('S') }.map(&:upcase)
某些人可能会争论在多行链式方法调用时使用
{...}
看起来还可以。但他们应该扪心自问——这样的代码真的可读吗?难道不能把区块内容提取出来放到小巧的方法里吗? -
优先考虑使用显式区块参数,以避免某些情况下通过创建区块的手法来传递参数给其他区块。此规则对性能有所影响,因为区块会被转换为
Proc
对象。 [link]require 'tempfile' # 差 def with_tmp_dir Dir.mktmpdir do |tmp_dir| Dir.chdir(tmp_dir) { |dir| yield dir } # 通过创建区块的手法来传递参数 end end # 好 def with_tmp_dir(&block) Dir.mktmpdir do |tmp_dir| Dir.chdir(tmp_dir, &block) end end with_tmp_dir do |dir| puts "dir is accessible as a parameter and pwd is set: #{dir}" end
-
避免在不需要流程控制的情况下使用
return
。 [link]# 差 def some_method(some_arr) return some_arr.size end # 好 def some_method(some_arr) some_arr.size end
-
避免在不需要的情况下使用
self
。(只有在调用self
的修改器、以保留字命名的方法、重载的运算符时才需要) [link]# 差 def ready? if self.last_reviewed_at > self.last_updated_at self.worker.update(self.content, self.options) self.status = :in_progress end self.status == :verified end # 好 def ready? if last_reviewed_at > last_updated_at worker.update(content, options) self.status = :in_progress end status == :verified end
-
避免局部变量遮蔽方法调用,除非它们具有相同效果。 [link]
class Foo attr_accessor :options # 勉强可以 def initialize(options) self.options = options # 此处 self.options 与 options 具有相同效果 end # 差 def do_something(options = {}) unless options[:when] == :later output(self.options[:message]) end end # 好 def do_something(params = {}) unless params[:when] == :later output(options[:message]) end end end
-
不要在条件表达式中使用
=
(赋值语句)的返回值,除非赋值语句包裹在括号之中。这种惯用法被称作条件表达式中的安全赋值。 [link]# 差 - 会出现警告 if v = array.grep(/foo/) do_something(v) ... end # 好 - 尽管 Ruby 解释器仍会出现警告,但 RuboCop 不会 if (v = array.grep(/foo/)) do_something(v) ... end # 好 v = array.grep(/foo/) if v do_something(v) ... end
-
优先考虑简短的自我赋值语法。 [link]
# 差 x = x + y x = x * y x = x**y x = x / y x = x || y x = x && y # 好 x += y x *= y x **= y x /= y x ||= y x &&= y
-
当变量尚未初始化时,使用
||=
对其进行初始化。 [link]# 差 name = name ? name : 'Bozhidar' # 差 name = 'Bozhidar' unless name # 好 - 当且仅当 name 为 nil 或 false 时,设置 name 的值为 'Bozhidar' name ||= 'Bozhidar'
-
不要使用
||=
对布尔变量进行初始化。 [link]# 差 - 设置 enabled 的值为 true,即使其原本的值是 false enabled ||= true # 好 enabled = true if enabled.nil?
-
使用
&&=
预先检查变量是否存在,如果存在,则做相应动作。使用&&=
语法可以省去if
检查。 [link]# 差 if something something = something.downcase end # 差 something = something ? something.downcase : nil # 勉强可以 something = something.downcase if something # 好 something = something && something.downcase # 更好 something &&= something.downcase
-
避免使用
case
语句等价操作符===
。从名称可知,这是case
语句隐式使用的操作符,在case
语句外的场合中使用,会产生难以理解的代码。 [link]# 差 Array === something (1..100) === 7 /something/ === some_string # 好 something.is_a?(Array) (1..100).include?(7) some_string =~ /something/
-
能使用
==
就不要使用eql?
。提供更加严格比较的eql?
在实践中极少使用。 [link]# 差 - 对于字符串,eql? 与 == 具有相同效果 'ruby'.eql? some_str # 好 'ruby' == some_str 1.0.eql? x # 当需要区别 Fixnum 1 与 Float 1.0 时,eql? 是具有意义的
-
避免使用 Perl 风格的特殊变量(比如
$:
、$;
等)。它们看起来非常神秘,但除了单行脚本,其他情况并不鼓励使用。建议使用English
程序库提供的友好别名。 [link]# 差 $:.unshift File.dirname(__FILE__) # 好 require 'English' $LOAD_PATH.unshift File.dirname(__FILE__)
-
永远不要在方法名与左括号之间添加空格。 [link]
# 差 f (3 + 2) + 1 # 好 f(3 + 2) + 1
-
运行 Ruby 解释器时,总是开启
-w
选项来。如果你忘了某个上述某个规则,它就会警告你! [link] -
不要在方法中嵌套定义方法,使用 lambda 方法来替代。 嵌套定义产生的方法,事实上和外围方法处于同一作用域(比如类作用域)。此外,“嵌套方法”会在定义它的外围方法每次调用时被重新定义。 [link]
# 差 def foo(x) def bar(y) # 省略主体 end bar(x) end # 好 - 作用同前,但 bar 不会在 foo 每次调用时被重新定义 def bar(y) # 省略主体 end def foo(x) bar(x) end # 好 def foo(x) bar = ->(y) { ... } bar.call(x) end
-
对于单行区块,使用新的 lambda 字面量定义语法。对于多行区块,使用
lambda
定义语法。 [link]# 差 l = lambda { |a, b| a + b } l.call(1, 2) # 好 - 但看起来怪怪的 l = ->(a, b) do tmp = a * 7 tmp * b / 50 end # 好 l = ->(a, b) { a + b } l.call(1, 2) l = lambda do |a, b| tmp = a * 7 tmp * b / 50 end
-
定义 lambda 方法时,如果有参数则使用括号。 [link]
# 差 l = ->x, y { something(x, y) } # 好 l = ->(x, y) { something(x, y) }
-
定义 lambda 方法时,如果无参数则省略括号。 [link]
# 差 l = ->() { something } # 好 l = -> { something }
-
倾向使用
proc
而不是Proc.new
。 [link]# 差 p = Proc.new { |n| puts n } # 好 p = proc { |n| puts n }
-
对于 lambda 方法或代码块,倾向使用
proc.call()
而不是proc[]
或proc.()
。 [link]# 差 - 看上去像是枚举器的存取操作 l = ->(v) { puts v } l[1] # 差 - 极少见的调用语法 l = ->(v) { puts v } l.(1) # 好 l = ->(v) { puts v } l.call(1)
-
未被使用的区块参数或局部变量,添加
_
前缀或直接使用_
(尽管表意性略差)。这种做法可以抑制 Ruby 解释器或 RuboCop 等工具发出“变量尚未使用”的警告。 [link]# 差 result = hash.map { |k, v| v + 1 } def something(x) unused_var, used_var = something_else(x) # ... end # 好 result = hash.map { |_k, v| v + 1 } def something(x) _unused_var, used_var = something_else(x) # ... end # 好 result = hash.map { |_, v| v + 1 } def something(x) _, used_var = something_else(x) # ... end
-
使用
$stdout/$stderr/$stdin
而不是STDOUT/STDERR/STDIN
。STDOUT/STDERR/STDIN
是常量,尽管在 Ruby 中允许给常量重新赋值(可能是重定向某些流),但解释器会发出警告。 [link] -
使用
warn
而不是$stderr.puts
。除了更加简练清晰外,warn
允许你在需要时通过设置解释器选项(使用-W0
将警告级别设置为 0)来抑制警告。 [link] -
倾向使用
sprintf
或其别名format
而不是相当晦涩的String#%
方法。 [link]# 差 '%d %d' % [20, 10] # => '20 10' # 好 sprintf('%d %d', 20, 10) # => '20 10' # 好 sprintf('%{first} %{second}', first: 20, second: 10) # => '20 10' format('%d %d', 20, 10) # => '20 10' # 好 format('%{first} %{second}', first: 20, second: 10) # => '20 10'
-
倾向使用
Array#join
而不是相当晦涩的带字符参数的Array#*
方法。 [link]# 差 %w[one two three] * ', ' # => 'one, two, three' # 好 %w[one two three].join(', ') # => 'one, two, three'
-
当你希望处理的变量类型是数组,但不太确定其是否真的是数组时,通过使用
Array()
来替代显式的数组类型检查与转换。 [link]# 差 paths = [paths] unless paths.is_a? Array paths.each { |path| do_something(path) } # 差 - 总是构建新的数组对象 [*paths].each { |path| do_something(path) } # 好 Array(paths).each { |path| do_something(path) }
-
通过使用范围或
Comparable#between?
来替代复杂的比较逻辑。 [link]# 差 do_something if x >= 1000 && x <= 2000 # 好 do_something if (1000..2000).include?(x) # 好 do_something if x.between?(1000, 2000)
-
倾向使用谓词方法而不是
==
操作符。但数值比较除外。 [link]# 差 if x % 2 == 0 end if x % 2 == 1 end if x == nil end # 好 if x.even? end if x.odd? end if x.nil? end if x.zero? end if x == 0 end
-
不做显式的
non-nil
检查,除非检查对象是布尔变量。 [link]# 差 do_something if !something.nil? do_something if something != nil # 好 do_something if something # 好 - 检查对象是布尔变量 def value_set? !@some_boolean.nil? end
-
避免使用
BEGIN
区块。 [link] -
永远不要使用
END
区块。使用Kernel#at_exit
来替代。 [link]# 差 END { puts 'Goodbye!' } # 好 at_exit { puts 'Goodbye!' }
-
避免使用 flip-flops 操作符。 [link]
-
流程控制中,避免使用嵌套条件。 [link]
倾向使用防御从句进行非法数据断言。防御从句是指处于方法顶部的条件语句,其能尽早地退出方法。
# 差 def compute_thing(thing) if thing[:foo] update_with_bar(thing[:foo]) if thing[:foo][:bar] partial_compute(thing) else re_compute(thing) end end end # 好 def compute_thing(thing) return unless thing[:foo] update_with_bar(thing[:foo]) return re_compute(thing) unless thing[:foo][:bar] partial_compute(thing) end
循环中,倾向使用
next
而不是条件区块。# 差 [0, 1, 2, 3].each do |item| if item > 1 puts item end end # 好 [0, 1, 2, 3].each do |item| next unless item > 1 puts item end
-
倾向使用
map
而不是collect
,find
而不是detect
,select
而不是find_all
,reduce
而不是inject
以及size
而不是length
。这不是一个硬性要求,如果使用别名可以增强可读性,使用它也没关系。这些别名方法继承自 Smalltalk 语言,但在别的语言并不通用。鼓励使用select
而不是find_all
的理由是前者与reject
搭配起来一目了然。 [link] -
不要使用
count
作为size
的替代方案。除了Array
外,其他Enumerable
对象都需要通过枚举整个集合才可以确定数目。 [link]# 差 some_hash.count # 好 some_hash.size
-
倾向使用
flat_map
而不是map + flatten
的组合。此规则并不适用于深度超过 2 层的数组。举例来说,如果users.first.songs == ['a', ['b','c']]
成立,则使用map + flatten
的组合而不是flat_map
。flat_map
只能平坦化一个层级,而flatten
能够平坦化任意多个层级。 [link]# 差 all_songs = users.map(&:songs).flatten.uniq # 好 all_songs = users.flat_map(&:songs).uniq
-
倾向使用
reverse_each
而不是reverse.each
,因为某些混入Enumerable
模块的类可能会提供reverse_each
的高效版本。即使这些类没有提供专门特化的版本,继承自Enumerable
的通用版本至少能保证性能与reverse.each
相当。 [link]# 差 array.reverse.each { ... } # 好 array.reverse_each { ... }
程序设计的真正难题是替事物命名及使缓存失效。
——Phil Karlton
-
标识符使用英文命名。 [link]
# 差 - 标识符使用非 ASCII 字符 заплата = 1_000 # 差 - 标识符使用拉丁文写法的保加利亚单词 zaplata = 1_000 # 好 salary = 1_000
-
符号、方法、变量使用蛇底式小写(
snake_case
)。 [link]# 差 :'some symbol' :SomeSymbol :someSymbol someVar = 5 var_10 = 10 def someMethod ... end def SomeMethod ... end # 好 :some_symbol some_var = 5 var10 = 10 def some_method ... end
-
给符号、方法、变量命名时,避免分隔字母与数字。 [link]
# 差 :some_sym_1 some_var_1 = 1 def some_method_1 # 做一些事情 end # 好 :some_sym1 some_var1 = 1 def some_method1 # 做一些事情 end
-
类与模块使用驼峰式大小写(
CamelCase
)。(HTTP、RFC、XML 等首字母缩写应该仍旧保持大写形式) [link]# 差 class Someclass ... end class Some_Class ... end class SomeXml ... end class XmlSomething ... end # 好 class SomeClass ... end class SomeXML ... end class XMLSomething ... end
-
文件名使用蛇底式小写,如
hello_world.rb
。 [link] -
目录名使用蛇底式小写,如
lib/hello_world/hello_world.rb
。 [link] -
尽量使一个源文件中只有一个类或模块。文件名就是类名或模块名,但使用蛇底式小写而不是驼峰式大小写。 [link]
-
其他常量使用尖叫蛇底式大写(SCREAMING_SNAKE_CASE)。 [link]
# 差 SomeConst = 5 # 好 SOME_CONST = 5
-
谓词方法(返回布尔值的方法)的名字应当以问号结尾。(比如
Array#empty?
)。不返回布尔值的方法不应以问号结尾。 [link] -
谓词方法的名字应当避免使用
is
、does
、can
等助动词作为前缀。这些助动词在实际场景中显得冗余,且与标准库的命名习惯(比如empty?
、include?
)很不一致。 [link]# 差 class Person def is_tall? true end def can_play_basketball? false end def does_like_candy? true end end # 好 class Person def tall? true end def basketball_player? false end def likes_candy? true end end
-
具有潜在危险性的方法,当其存在对应安全版本的方法时,其名字应当以惊叹号结尾。(比如修改
self
或参数值的方法、相对exit
方法不会在退出时运行 finalizers 执行清理工作的exit!
方法等) [link]# 差 - 没有对应安全版本的方法 class Person def update! end end # 好 class Person def update end end # 好 class Person def update! end def update end end
-
尽量根据危险方法来定义对应安全版本的方法。 [link]
class Array def flatten_once! res = [] each do |e| [*e].each { |f| res << f } end replace(res) end def flatten_once dup.flatten_once! end end
-
当定义二元操作符时,将参数命名为
other
(<<
与[]
例外,因为其语义与此不同)。 [link]def +(other) # 省略主体 end
良好的代码自身就是最佳的文档。当你要添加一个注释时,扪心自问,“如何改善代码让它不需要注释?” 改善代码,再写相应文档使之更清楚。
——Steve McConnell
-
编写让人一目了然的代码然后忽略这一节的其它部分。我是认真的! [link]
-
使用英文编写注释。 [link]
-
前导
#
与注释文本之间应当添加一个空格。 [link] -
避免无谓的注释。 [link]
# 差 counter += 1 # Increments counter by one.
-
及时更新注释。过时的注释比没有注释还要糟糕。 [link]
好的代码就像是好的笑话 —— 它不需要解释。
——Russ Olsen
- 避免替烂代码编写注释。重构它们使其变得一目了然。(要么做,要么不做,不要只是试试看。——Yoda) [link]
-
注解应该直接写在相关代码之前那行。 [link]
-
注解关键字后面,跟着一个冒号及空格,接着是描述问题的文本。 [link]
-
如果需要用多行来描述问题,后续行要放在
#
后面并缩排两个空格。 [link]def bar # FIXME: This has crashed occasionally since v3.2.1. It may # be related to the BarBazUtil upgrade. baz(:quux) end
-
当问题是显而易见时,任何文档都是多余的,注解应当放在有问题的那行末尾且不带任何多余说明。这个用法应该算是例外而不是规则。 [link]
def bar sleep 100 # OPTIMIZE end
-
使用
TODO
标记应当加入的特征与功能。 [link] -
使用
FIXME
标记需要修复的代码。 [link] -
使用
OPTIMIZE
标记可能引发性能问题的低效代码。 [link] -
使用
HACK
标记代码异味,即那些应当被重构的可疑编码习惯。 [link] -
使用
REVIEW
标记需要确认与编码意图是否一致的可疑代码。比如,REVIEW: Are we sure this is how the client does X currently?
。 [link] -
适当情况下,可以自行定制其他注解关键字,但别忘记在项目的
README
或类似文档中予以说明。 [link]
-
将魔术注释放置在所有代码和文档之上,如果代码中需要 shebangs,魔术注释应该只在 shebangs 下面。 [link]
# good # frozen_string_literal: true # Some documentation about Person class Person end # bad # Some documentation about Person # frozen_string_literal: true class Person end
# good #!/usr/bin/env ruby # frozen_string_literal: true App.parse(ARGV) # bad # frozen_string_literal: true #!/usr/bin/env ruby App.parse(ARGV)
-
有多个魔术注释时每行只写一个。 [link]
# good # frozen_string_literal: true # encoding: ascii-8bit # bad # -*- frozen_string_literal: true; encoding: ascii-8bit -*-
-
魔术注释和代码及文档之间间隔一个空行。 [link]
# good # frozen_string_literal: true # Some documentation for Person class Person # Some code end # bad # frozen_string_literal: true # Some documentation for Person class Person # Some code end
-
在类定义中,使用一致的结构。 [link]
class Person # 首先是 extend 与 include extend SomeModule include AnotherModule # 内部类 CustomError = Class.new(StandardError) # 接着是常量 SOME_CONSTANT = 20 # 接下来是属性宏 attr_reader :name # 跟着是其他宏(如果有的话) validates :name # 公开的类方法接在下一行 def self.some_method end # 初始化方法在类方法和实例方法之间 def initialize end # 跟着是公开的实例方法 def some_method end # 受保护及私有的方法等放在接近结尾的地方 protected def some_protected_method end private def some_private_method end end
-
在混入多个模块时,倾向使用多行语法。 [link]
# 差 class Person include Foo, Bar end # 好 class Person include Foo include Bar end
-
如果嵌套类数目较多,进而导致外围类定义较长,则将它们从外围类中提取出来,分别放置在单独的以嵌套类命名的文件中,并将文件归类至以外围类命名的文件夹下。 [link]
# 差 # foo.rb class Foo class Bar # 定义 30 多个方法 end class Car # 定义 20 多个方法 end # 定义 30 多个方法 end # 好 # foo.rb class Foo # 定义 30 多个方法 end # foo/bar.rb class Foo class Bar # 定义 30 多个方法 end end # foo/car.rb class Foo class Car # 定义 20 多个方法 end end
-
定义只有类方法的数据类型时,倾向使用模块而不是类。只有当需要实例化时才使用类。 [link]
# 差 class SomeClass def self.some_method # 省略主体 end def self.some_other_method # 省略主体 end end # 好 module SomeModule module_function def some_method # 省略主体 end def some_other_method # 省略主体 end end
-
当你想将模块的实例方法变成类方法时,倾向使用
module_function
而不是extend self
。 [link]# 差 module Utilities extend self def parse_something(string) # 做一些事情 end def other_utility_method(number, string) # 做一些事情 end end # 好 module Utilities module_function def parse_something(string) # 做一些事情 end def other_utility_method(number, string) # 做一些事情 end end
-
总是替那些用以表示领域模型的类提供一个适当的
to_s
方法。 [link]class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end def to_s "#{@first_name} #{@last_name}" end end
-
使用
attr
系列方法来定义琐碎的存取器或修改器。 [link]# 差 class Person def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end def first_name @first_name end def last_name @last_name end end # 好 class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end
-
对于访问器方法,避免使用
get_
作为名字前缀;对于更改器方法,避免使用set_
作为名字前缀。Ruby 语言中,通常使用attr_name
作为访问器的方法名,使用attr_name=
作为更改器的方法名。 [link]# 差 class Person def get_name "#{@first_name} #{@last_name}" end def set_name(name) @first_name, @last_name = name.split(' ') end end # 好 class Person def name "#{@first_name} #{@last_name}" end def name=(name) @first_name, @last_name = name.split(' ') end end
-
避免使用
attr
。使用attr_reader
与attr_accessor
来替代。 [link]# 差 - 创建单个存取方法(此方法在 Ruby 1.9 之后被移除了) attr :something, true attr :one, :two, :three # 类似于 attr_reader # 好 attr_accessor :something attr_reader :one, :two, :three
-
优先考虑使用
Struct.new
。它替你定义了那些琐碎的访问器、构造器及比较操作符。 [link]# 好 class Person attr_accessor :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end # 更好 Person = Struct.new(:first_name, :last_name) do end
-
不要扩展
Struct.new
实例化后的对象。对它进行扩展不但引入了毫无意义的类层次,而且在此文件被多次引入时可能会产生奇怪的错误。 [link]# 差 class Person < Struct.new(:first_name, :last_name) end # 好 Person = Struct.new(:first_name, :last_name)
-
优先考虑通过工厂方法的方式创建某些具有特定意义的实例对象。 [link]
class Person def self.create(options_hash) # 省略主体 end end
-
# 差 class Animal # 抽象方法 def speak end end # 继承父类 class Duck < Animal def speak puts 'Quack! Quack' end end # 继承父类 class Dog < Animal def speak puts 'Bau! Bau!' end end # 好 class Duck def speak puts 'Quack! Quack' end end class Dog def speak puts 'Bau! Bau!' end end
-
避免使用类变量(
@@
)。类变量在继承方面存在令人生厌的行为。 [link]class Parent @@class_var = 'parent' def self.print_class_var puts @@class_var end end class Child < Parent @@class_var = 'child' end Parent.print_class_var # => 此处打印的结果为 'child'
如你所见,在类的层次结构中所有类都会共享同一类变量。通常情况下,倾向使用类实例变量而不是类变量。
-
根据方法的目的与用途设置适当的可见级别(
private
、protected
)。不要什么都不做就把所有方法设置为public
(默认值)。毕竟我们写的是 Ruby 而不是 Python。 [link] -
把
public
、protected
、private
与其作用的方法缩排在同一层级。且在其上下各留一行以强调此可见级别作用于之后的所有方法。 [link]class SomeClass def public_method # ... end private def private_method # ... end def another_private_method # ... end end
-
使用
def self.method
定义类方法。这种做法使得在代码重构时,即使修改了类名也无需做多次修改。 [link]class TestClass # 差 def TestClass.some_method # 省略主体 end # 好 def self.some_other_method # 省略主体 end # 在需要定义多个类方法时,另一种便捷写法 class << self def first_method # 省略主体 end def second_method_etc # 省略主体 end end end
-
在类的词法作用域中定义方法别名时,倾向使用
alias
。因为定义期间alias
与self
指向的都是词法作用域,除非明确说明,否则该别名所引用的方法不会在运行期间被改变,或是在任何子类中被修改。 [link]class Westerner def first_name @names.first end alias given_name first_name end
因为
alias
与def
一样都是关键字,倾向使用裸字而不是符号或字符串。也就是说,使用alias foo bar
而不是alias :foo :bar
。另外需要了解 Ruby 是如何处理别名和继承的:别名所引用的原始方法是在定义期间被指定的,而不是运行期间。
class Fugitive < Westerner def first_name 'Nobody' end end
在这个例子中,
Fugitive#given_name
仍然调用原先的Westerner#first_name
方法,而不是Fugitive#first_name
。如果想要覆写Fugitive#given_name
,必须在子类中重新定义。class Fugitive < Westerner def first_name 'Nobody' end alias given_name first_name end
-
在运行期间定义模块方法、类方法、单件方法的别名时,总是使用
alias_method
。在上述情况下,使用alias
可能会导致预期之外的结果。 [link]module Mononymous def self.included(other) other.class_eval { alias_method :full_name, :given_name } end end class Sting < Westerner include Mononymous end
-
在模块方法,或是类方法内部调用自身其他方法时,通常省略模块名/类名/
self
。 [link]class TestClass # 差 def self.call(param1, param2) TestClass.new(param1).call(param2) end # 差 def self.call(param1, param2) self.new(param1).call(param2) end # 好 def self.call(param1, param2) new(param1).call(param2) end # 省略其他方法 end
-
对于异常处理,倾向使用
raise
而不是fail
。 [link]# 差 fail SomeException, 'message' # 好 raise SomeException, 'message'
-
不要在带双参数形式的
raise
方法中显式指定RuntimeError
。 [link]# 差 raise RuntimeError, 'message' # 好 - 默认就是 RuntimeError raise 'message'
-
倾向使用带异常类、消息的双参数形式调用
raise
方法,而不是使用异常的实例。 [link]# 差 - 并无 raise SomeException.new('message') [, backtraces] 这种调用形式 raise SomeException.new('message') # 好 - 与调用形式 raise SomeException [, 'message' [, backtraces]] 保持一致 raise SomeException, 'message'
-
永远不要从
ensure
区块返回。如果你显式地从ensure
区块返回,那么其所在的方法会如同永远不会发生异常般的返回。事实上,异常被默默地丢弃了。 [link]def foo raise ensure return 'very bad idea' end
-
尽可能隐式地使用
begin/rescue/ensure/end
区块。 [link]# 差 def foo begin # 主逻辑 rescue # 异常处理逻辑 end end # 好 def foo # 主逻辑 rescue # 异常处理逻辑 end
-
通过使用 contingency 方法(一个由 Avdi Grimm 创造的词)来减少
begin/rescue/ensure/end
区块的使用。 [link]# 差 begin something_that_might_fail rescue IOError # 处理 IOError end begin something_else_that_might_fail rescue IOError # 处理 IOError end # 好 def with_io_error_handling yield rescue IOError # 处理 IOError end with_io_error_handling { something_that_might_fail } with_io_error_handling { something_else_that_might_fail }
-
不要抑制异常。 [link]
# 差 begin # 抛出异常 rescue SomeError # 不做任何相关处理 end # 差 do_something rescue nil
-
避免使用
rescue
修饰语法。 [link]# 差 - 这里将会捕捉 StandardError 及其所有子孙类的异常 read_file rescue handle_error($!) # 好 - 这里只会捕获 Errno::ENOENT 及其所有子孙类的异常 def foo read_file rescue Errno::ENOENT => ex handle_error(ex) end
-
不要将异常处理作为流程控制使用。 [link]
# 差 begin n / d rescue ZeroDivisionError puts 'Cannot divide by 0!' end # 好 if d.zero? puts 'Cannot divide by 0!' else n / d end
-
避免捕获
Exception
。这种做法会同时将信号与exit
方法困住,导致你必须使用kill -9
来终止进程。 [link]# 差 - 信号与 exit 方法产生的异常会被捕获(除了 kill -9) begin exit rescue Exception puts "you didn't really want to exit, right?" # 处理异常 end # 好 - 没有指定具体异常的 rescue 子句默认捕获 StandardError begin # 抛出异常 rescue => e # 处理异常 end # 好 - 指定具体异常 StandardError begin # 抛出异常 rescue StandardError => e # 处理异常 end
-
把较具体的异常放在处理链的较上层,不然它们永远不会被执行。 [link]
# 差 begin # 抛出异常 rescue StandardError => e # 处理异常 rescue IOError => e # 处理异常,但事实上永远不会被执行 end # 好 begin # 抛出异常 rescue IOError => e # 处理异常 rescue StandardError => e # 处理异常 end
-
在
ensure
区块释放程序的外部资源。 [link]f = File.open('testfile') begin # .. 文件操作 rescue # .. 处理异常 ensure f.close if f end
-
在调用资源获取方法时,尽量使用具备自动清理功能的版本。 [link]
# 差 - 需要显式关闭文件描述符 f = File.open('testfile') # ... f.close # 好 - 文件描述符会被自动关闭 File.open('testfile') do |f| # ... end
-
倾向使用标准库中的异常类而不是引入新的类型。 [link]
-
对于数组与哈希,倾向使用字面量语法来构建实例(除非你需要给构造器传递参数)。 [link]
# 差 arr = Array.new hash = Hash.new # 好 arr = [] hash = {}
-
当创建一组元素为单词(没有空格或特殊字符)的数组时,倾向使用
%w
而不是[]
。此规则只适用于数组元素有两个或以上的时候。 [link]# 差 STATES = ['draft', 'open', 'closed'] # 好 STATES = %w[draft open closed]
-
当创建一组符号类型的数组(且不需要保持 Ruby 1.9 兼容性)时,倾向使用
%i
。此规则只适用于数组元素有两个或以上的时候。 [link]# 差 STATES = [:draft, :open, :closed] # 好 STATES = %i[draft open closed]
-
避免在数组与哈希的字面量语法的最后一个元素之后添加逗号,尤其当元素没有分布在同一行时。 [link]
# 差 - 尽管移动、新增、删除元素颇为方便,但仍不推荐这种写法 VALUES = [ 1001, 2020, 3333, ] # 差 VALUES = [1001, 2020, 3333, ] # 好 VALUES = [1001, 2020, 3333]
-
避免在数组中创造巨大的间隔。 [link]
arr = [] arr[100] = 1 # 现在你有一个很多 nil 的数组
-
当访问数组的首元素或尾元素时,倾向使用
first
或last
而不是[0]
或[-1]
。 [link] -
当处理的对象不存在重复元素时,使用
Set
来替代Array
。Set
是实现了无序且无重复元素的集合类型。它兼具Array
的直观操作与Hash
的快速查找。 [link] -
倾向使用符号而不是字符串作为哈希键。 [link]
# 差 hash = { 'one' => 1, 'two' => 2, 'three' => 3 } # 好 hash = { one: 1, two: 2, three: 3 }
-
避免使用可变对象作为哈希键。 [link]
-
当哈希键为符号时,使用 Ruby 1.9 的字面量语法。 [link]
# 差 hash = { :one => 1, :two => 2, :three => 3 } # 好 hash = { one: 1, two: 2, three: 3 }
-
当哈希键既有符号又有字符串时,不要使用 Ruby 1.9 的字面量语法。 [link]
# 差 { a: 1, 'b' => 2 } # 好 { :a => 1, 'b' => 2 }
-
倾向使用
Hash#key?
而不是Hash#has_key?
,使用Hash#value?
而不是Hash#has_value?
。 [link]# 差 hash.has_key?(:test) hash.has_value?(value) # 好 hash.key?(:test) hash.value?(value)
-
倾向使用
Hash#each_key
而不是Hash#keys.each
,使用Hash#each_value
而不是Hash#values.each
。 [link]# 差 hash.keys.each { |k| p k } hash.values.each { |v| p v } hash.each { |k, _v| p k } hash.each { |_k, v| p v } # 好 hash.each_key { |k| p k } hash.each_value { |v| p v }
-
当处理应该存在的哈希键时,使用
Hash#fetch
。 [link]heroes = { batman: 'Bruce Wayne', superman: 'Clark Kent' } # 差 - 如果我们打错了哈希键,则难以发现这个错误 heroes[:batman] # => 'Bruce Wayne' heroes[:supermann] # => nil # 好 - fetch 会抛出 KeyError 使这个错误显而易见 heroes.fetch(:supermann)
-
当为哈希键的值提供默认值时,倾向使用
Hash#fetch
而不是自定义逻辑。 [link]batman = { name: 'Bruce Wayne', is_evil: false } # 差 - 如果仅仅使用 || 操作符,那么当值为假时,我们不会得到预期结果 batman[:is_evil] || true # => true # 好 - fetch 在遇到假值时依然可以正确工作 batman.fetch(:is_evil, true) # => false
-
当提供默认值的求值代码具有副作用或开销较大时,倾向使用
Hash#fetch
的区块形式。 [link]batman = { name: 'Bruce Wayne' } # 差 - 此形式会立即求值,如果调用多次,可能会影响程序的性能 batman.fetch(:powers, obtain_batman_powers) # obtain_batman_powers 开销较大 # 好 - 此形式会惰性求值,只有抛出 KeyError 时,才会产生开销 batman.fetch(:powers) { obtain_batman_powers }
-
当需要一次性从哈希中获取多个键的值时,使用
Hash#values_at
。 [link]# 差 email = data['email'] username = data['nickname'] # 好 email, username = data.values_at('email', 'nickname')
-
利用“Ruby 1.9 之后的哈希是有序的”的这个特性。 [link]
-
当遍历集合时,不要改动它。 [link]
-
当访问集合中的元素时,倾向使用对象所提供的方法进行访问,而不是直接调用对象属性上的
[n]
方法。这种做法可以防止你在nil
对象上调用[]
。 [link]# 差 Regexp.last_match[1] # 好 Regexp.last_match(1)
-
当为集合提供存取器时,尽量支持索引值为
nil
的访问形式。 [link]# 差 def awesome_things @awesome_things end # 好 def awesome_things(index = nil) if index && @awesome_things @awesome_things[index] else @awesome_things end end
-
通过
Integer
检查对象是否是数值类型,而不是Fixnum
或Bignum
。因为Fixnum
或Bignum
表达的数值大小存在范围限定。 [link]timestamp = Time.now.to_i # 差 timestamp.is_a? Fixnum timestamp.is_a? Bignum # 好 timestamp.is_a? Integer
-
对于随机数的生成,倾向使用 Range 来表示,而不是 Integer + 偏移量,这样可以更加清晰地表达你的意图,类比于投掷骰子。 [link]
# 差 rand(6) + 1 # 好 rand(1..6)
-
倾向使用字符串插值或字符串格式化,而不是字符串拼接。 [link]
# 差 email_with_name = user.name + ' <' + user.email + '>' # 好 email_with_name = "#{user.name} <#{user.email}>" # 好 email_with_name = format('%s <%s>', user.name, user.email)
-
使用统一的风格创建字符串字面量。在 Ruby 社区中存在两种流行的风格:默认单引号(风格 A)与默认双引号(风格 B)。 [link]
-
(风格 A) 当你不需要字符串插值或特殊字符(比如
\t
、\n
、'
)时,倾向使用单引号。# 差 name = "Bozhidar" # 好 name = 'Bozhidar'
-
(风格 B) 除非字符串中包含双引号,或是你希望抑制转义字符,否则倾向使用双引号。
# 差 name = 'Bozhidar' # 好 name = "Bozhidar"
本指南使用第一种风格。
-
-
不要使用
?x
字面量语法。在 Ruby 1.9 之后,?x
与'x'
(只包含单个字符的字符串)是等价的。 [link]# 差 char = ?c # 好 char = 'c'
-
不要忘记使用
{}
包裹字符串插值中的实例变量或全局变量。 [link]class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end # 差 - 语法正确,但略显笨拙 def to_s "#@first_name #@last_name" end # 好 def to_s "#{@first_name} #{@last_name}" end end $global = 0 # 差 puts "$global = #$global" # 好 puts "$global = #{$global}"
-
在字符串插值中,不要显式调用
Object#to_s
方法,Ruby 会自动调用它。 [link]# 差 message = "This is the #{result.to_s}." # 好 message = "This is the #{result}."
-
当你需要构造巨大的数据块时,避免使用
String#+
,使用String#<<
来替代。String#<<
通过修改原始对象进行拼接工作,其比String#+
效率更高,因为后者需要产生一堆新的字符串对象。 [link]# 差 html = '' html += '<h1>Page title</h1>' paragraphs.each do |paragraph| html += "<p>#{paragraph}</p>" end # 好 - 并且效率更高 html = '' html << '<h1>Page title</h1>' paragraphs.each do |paragraph| html << "<p>#{paragraph}</p>" end
-
当存在更快速、更专业的替代方案时,不要使用
String#gsub
。 [link]url = 'http://example.com' str = 'lisp-case-rules' # 差 url.gsub('http://', 'https://') str.gsub('-', '_') # 好 url.sub('http://', 'https://') str.tr('-', '_')
-
heredocs 中的多行文本会保留各行的前导空白。因此做好如何缩排的规划。 [link]
code = <<-END.gsub(/^\s+\|/, '') |def test | some_method | other_method |end END # => "def test\n some_method\n other_method\nend\n"
-
使用 Ruby 2.3 新增的
<<~
操作符来缩排 heredocs 中的多行文本。 [link]# 差 - 使用 Powerpack 程序库的 String#strip_margin code = <<-END.strip_margin('|') |def test | some_method | other_method |end END # 差 code = <<-END def test some_method other_method end END # 好 code = <<~END def test some_method other_method end END
-
避免使用
DateTime
,除非你确实需要处理历法改革(儒略/格里历的改革),此时通过设置start
参数来明确你的意图。 [link]# 差 - 使用 DateTime 表示当前时间 DateTime.now # 好 - 使用 Time 表示当前时间 Time.now # 差 - 使用 DateTime 表示近现代日期 DateTime.iso8601('2016-06-29') # 好 - 使用 Date 表示近现代日期 Date.iso8601('2016-06-29') # 好 - 使用 DateTime 表示日期,通过设置 start 参数为 Date::ENGLANG 明确表示使用 England 历法改革版本 DateTime.iso8601('1751-04-23', Date::ENGLAND)
有些人在面对问题时,不经大脑便认为,“我知道,这里该用正则表达式”。现在他要面对两个问题了。
——Jamie Zawinski
-
如果只是在字符串中进行简单的文本搜索,不要使用正则表达式,比如
string['text']
。 [link] -
对于简单的构建操作,使用正则表达式作为索引即可。 [link]
match = string[/regexp/] # 获取匹配的内容 first_group = string[/text(grp)/, 1] # 获取匹配的分组(grp)的内容 string[/text (grp)/, 1] = 'replace' # string => 'text replace'
-
当你不需要分组结果时,使用非捕获组。 [link]
# 差 /(first|second)/ # 好 /(?:first|second)/
-
避免使用 Perl 风格的、用以代表最近的捕获组的特殊变量(比如
$1
、$2
等)。使用Regexp.last_match(n)
来替代。 [link]/(regexp)/ =~ string ... # 差 process $1 # 好 process Regexp.last_match(1)
-
避免使用数字来获取分组。因为很难明白它们代表的含义。使用命名分组来替代。 [link]
# 差 /(regexp)/ =~ string ... process Regexp.last_match(1) # 好 /(?<meaningful_var>regexp)/ =~ string ... process meaningful_var
-
在字符类别中,只有少数几个你需要特别关心的特殊字符:
^
、-
、\
、]
,所以你不需要转义[]
中的.
与中括号。 [link] -
小心使用
^
与$
,它们匹配的是一行的开始与结束,而不是字符串的开始与结束。如果你想要匹配整个字符串,使用\A
与\z
。(注意,\Z
实为/\n?\z/
) [link]string = "some injection\nusername" string[/^username$/] # 匹配成功 string[/\Ausername\z/] # 匹配失败
-
对于复杂的正则表达式,使用
x
修饰符。这种做法不但可以提高可读性,而且允许你加入必要的注释。注意的是,空白字符会被忽略。 [link]regexp = / start # some text \s # white space char (group) # first group (?:alt1|alt2) # some alternation end /x
-
对于复杂的替换,使用
sub/gsub
与哈希或区块组合的调用形式。 [link]words = 'foo bar' words.sub(/f/, 'f' => 'F') # => 'Foo bar' words.gsub(/\w+/) { |word| word.capitalize } # => 'Foo Bar'
-
只有当字符串中同时存在插值与双引号,且是单行时,才使用
%()
(%Q
的简写形式)。多行字符串,倾向使用 heredocs。 [link]# 差 - 不存在插值 %(<div class="text">Some text</div>) # 应当使用 '<div class="text">Some text</div>' # 差 - 不存在双引号 %(This is #{quality} style) # 应当使用 "This is #{quality} style" # 差 - 多行字符串 %(<div>\n<span class="big">#{exclamation}</span>\n</div>) # 应当使用 heredocs # 好 - 同时存在插值与双引号,且是单行字符串 %(<tr><td class="name">#{name}</td>)
-
避免使用
%()
或%q
,除非字符串同时存在'
与"
。优先考虑更具可读性的常规字符串,除非字符串中存在大量需要转义的字符。 [link]# 差 name = %q(Bruce Wayne) time = %q(8 o'clock) question = %q("What did you say?") # 好 name = 'Bruce Wayne' time = "8 o'clock" question = '"What did you say?"' quote = %q(<p class='quote'>"What did you say?"</p>)
-
只有当正则表达式中存在一个或以上的
/
字符时,才使用%r
。 [link]# 差 %r{\s+} # 好 %r{^/(.*)$} %r{^/blog/2011/(.*)$}
-
除非调用的命令使用了反引号(这种情况并不多见),否则不要使用
%x
。 [link]# 差 date = %x(date) # 好 date = `date` echo = %x(echo `date`)
-
避免使用
%s
。倾向使用:"some string"
来创建含有空白字符的符号。 [link] -
针对不同的百分号字面量,使用不同的括号类型。 [link]
- 针对构建字符串的
%q
,%Q
字面量,使用()
。 - 针对构建数组的
%w
,%i
,%W
,%I
字面量,使用[]
,以与常规的数组字面量保持一致。 - 针对构建正则的
%r
字面量,使用{}
,此乃惯例。 - 针对
%s
,%x
等其他字面量,使用()
。
# 差 %q{"Test's king!", John said.} # 好 %q("Test's king!", John said.) # 差 %w(one two three) %i(one two three) # 好 %w[one two three] %i[one two three] # 差 %r((\w+)-(\d+)) %r{\w{1,2}\d{2,5}} # 好 %r{(\w+)-(\d+)} %r|\w{1,2}\d{2,5}|
- 针对构建字符串的
-
避免无谓的元编程。 [link]
-
当编写程序库时,不要使核心类混乱(不要使用 monkey patch)。 [link]
-
对于
class_eval
方法,倾向使用区块形式,而不是字符串插值形式。 [link]-
当使用字符串插值形式时,总是提供
__FILE__
及__LINE__
,以使你的调用栈看起来具有意义:class_eval 'def use_relative_model_naming?; true; end', __FILE__, __LINE__
-
倾向使用
define_method
而不是class_eval { def ... }
-
-
当使用
class_eval
(或其他的eval
)的字符串插值形式时,添加一个注释区块来说明它是如何工作的(来自 Rails 代码中的技巧)。 [link]# 摘录自 activesupport/lib/active_support/core_ext/string/output_safety.rb UNSAFE_STRING_METHODS.each do |unsafe_method| if 'String'.respond_to?(unsafe_method) class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{unsafe_method}(*params, &block) # def capitalize(*params, &block) to_str.#{unsafe_method}(*params, &block) # to_str.capitalize(*params, &block) end # end def #{unsafe_method}!(*params) # def capitalize!(*params) @dirty = true # @dirty = true super # super end # end EOT end end
-
避免使用
method_missing
。它会使你的调用栈变得凌乱;其方法不被罗列在#methods
中;拼错的方法可能会默默地工作(nukes.launch_state = false
)。优先考虑使用委托、代理、或是define_method
来替代。如果你必须使用method_missing
的话,务必做到以下几点: [link]-
仅仅捕获那些具有良好语义前缀的方法,像是
find_by_*
——让你的代码愈确定愈好。 -
在语句的最后调用
super
。 -
委托到确定的、非魔术的方法,比如:
# 差 def method_missing?(meth, *params, &block) if /^find_by_(?<prop>.*)/ =~ meth # ... 一堆处理 find_by 的代码 else super end end # 好 def method_missing?(meth, *params, &block) if /^find_by_(?<prop>.*)/ =~ meth find_by(prop, *params, &block) else super end end # 最好的方式可能是在每个需要支持的属性被声明时,使用 define_method 定义对应的方法
-
倾向使用
public_send
而不是send
,因为send
会无视private/protected
的可见性。 [link]module Activatable extend ActiveSupport::Concern included do before_create :create_token end private def reset_token ... end def create_token ... end def activate! ... end end class Organization < ActiveRecord::Base include Activatable end linux_organization = Organization.find(...) # 差 - 会破坏对象的封装性 linux_organization.send(:reset_token) # 好 - 会抛出异常 linux_organization.public_send(:reset_token)
-
倾向使用
__send__
而不是send
,因为send
可能会被覆写。 [link]require 'socket' u1 = UDPSocket.new u1.bind('127.0.0.1', 4913) u2 = UDPSocket.new u2.connect('127.0.0.1', 4913) # 这里不会调用 u2 的 sleep 方法,而是通过 UDP socket 发送一条消息 u2.send :sleep, 0 # 动态调用 u2 的某个方法 u2.__send__ ...
-
总是开启
ruby -w
选项,以编写安全的代码。 [link] -
避免使用哈希作为可选参数。这个方法是不是做太多事了?(对象构造器除外) [link]
-
避免单个方法的长度超过 10 行(不计入空行)。理想上,大部分方法应当不超过 5 行。 [link]
-
避免参数列表数目多于三或四个。 [link]
-
如果你真的需要“全局”方法,将它们添加到
Kernel
并设为私有。 [link] -
使用模块实例变量而不是全局变量。 [link]
# 差 $foo_bar = 1 # 好 module Foo class << self attr_accessor :bar end end Foo.bar = 1
-
使用
OptionParser
来解析复杂的命令行选项。使用ruby -s
来处理琐碎的命令行选项。 [link] -
使用
Time.now
而不是Time.new
来获取当前的系统时间。 [link] -
使用函数式思维编写程序,避免副作用。 [link]
-
不要修改参数值,除非那就是这个方法的作用。 [link]
-
避免使用三层以上的嵌套区块。 [link]
-
保持一致性。在理想的世界里,遵循这些准则。 [link]
-
使用常识。 [link]
以下的一些工具可以帮助你自动检查项目中的 Ruby 代码是否符合这份指南。
RuboCop 是一个基于本指南的 Ruby 代码风格检查工具。RuboCop 涵盖了本指南相当大的部分,其同时支持 MRI 1.9 和 MRI 2.0,且与 Emacs 整合良好。
本指南仍在不断改进。某些规则可能缺乏恰当示例,某些规则可能尚未表述到位。任何对这些规则的改进都是对 Ruby 社区的有益帮助。希望在适当时候,这些问题都能够得到解决,暂且铭记于心。
这里的每条规则都不是定案。这只是我渴望与同样对 Ruby 编程风格感兴趣的大家一起工作,最终可以为整个 Ruby 社区创造一份有益的资源。欢迎发起讨论或提交一个带有改进性质的更新请求。在此提前感谢你的帮助!
你也可以通过 Gratipay 对此项目(或是 RuboCop)提供财务方面的支持。
很简单,只需参考贡献指南。
本指南基于 Creative Commons Attribution 3.0 Unported 授权许可。
一份社区驱动的风格指南,如果没多少人知道,对一个社区来说就没有多少用处。微博转发这份指南,分享给你的朋友或同事。我们得到的每个评价、建议或意见都可以让这份指南变得更好一点。而我们想要拥有最好的指南,不是吗?
共勉之,
Bozhidar