语法
语法
使用::
引用常量(包括类与模块)与构造器(比如Array()
、Nokogiri::HTML()
)。不要使用::
调用常规方法。# 差
SomeClass::some_method
some_object::some_method
# 好
SomeClass.some_method
some_object.some_method
SomeModule::SomeClass::SOME_CONST
SomeModule::SomeClass()
使用def
定义方法时,如果有参数则使用括号,如果无参数则省略括号。# 差
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)
;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 # 好
定义可选参数时,将可选参数放置在参数列表尾部。如果可选参数出现在列表头部,则此方法在调用时可能会产生预期之外的结果。# 差
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'
定义变量时,避免并行赋值。但当右值为方法调用返回值,或是与*
操作符配合使用,或是交换两个变量的值,并行赋值也是可以接受的。并行赋值的可读性通常不如分开赋值。# 差
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]
除非必要,否则避免在并行赋值时使用单字符的_
变量。优先考虑前缀形式的下划线变量,而不是直接使用_
,因为前者可以提供一定的语义信息。但当赋值语句左侧出现带*
操作符的变量时,使用_
也是可以接受的。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
有),因此在它内部定义的变量在外部仍是可见的。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 some_condition then
# 省略主体
end
# 好
if some_condition
# 省略主体
end
在多行if/unless
中,总是把条件表达式与if/unless
放置在同一行。# 差
if
some_condition
do_something
do_something_else
end
# 好
if some_condition
do_something
do_something_else
end
倾向使用三元操作符(?:
)而不是if/then/else/end
结构。前者更为常见且简练。# 差
result = if some_condition then something else something_else end
# 好
result = some_condition ? something : something_else
三元操作符的每个分支只写一个表达式。即不要嵌套三元操作符。嵌套情况请使用if/else
结构。# 差
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
# 差
result = if some_condition; something else something_else end
# 好
result = some_condition ? something : something_else
# 差
if condition
result = x
else
result = y
end
# 好
result =
if condition
x
else
y
end
在case
表达式中,单行情况使用when x then ...
语法。另一种语法when x: ...
在 Ruby 1.9 之后被移除了。# 差 - 因为操作符的优先级,这里必须使用括号
x = (not something)
# 好
x = !something
!!
会将表达式结果转换为布尔值,但对于流程控制的表达式通常并不需要如此显式的转换过程。如果需要做nil
检查,那么调用对象的nil?
方法。# 差
x = 'test'
# 令人费解的 nil 检查
if !!x
# 省略主体
end
# 好
x = 'test'
if x
# 省略主体
end
永远不要使用and
与or
关键字。使用&&
与||
来替代。# 差
# 布尔表达式
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
修饰语法。另一种方法是使用流程控制&&
/||
。# 差
if some_condition
do_something
end
# 好
do_something if some_condition
# 好 - 使用流程控制
some_condition && do_something
# 差
10.times do
# 省略多行主体
end if some_condition
# 好
if some_condition
10.times do
# 省略多行主体
end
end
避免使用嵌套if
/unless
/while
/until
修饰语法。适当情况下,使用&&
/||
来替代。# 差
do_something if other_condition if some_condition
# 好
do_something if some_condition && other_condition
对于否定条件,倾向使用unless
而不是if
(或是使用流程控制||
)。# 差
do_something if !some_condition
# 差
do_something if not some_condition
# 好
do_something unless some_condition
# 好
some_condition || do_something
不要使用unless
与else
的组合。将它们改写成肯定条件。# 差
unless success?
puts 'failure'
else
puts 'success'
end
# 好
if success?
puts 'success'
else
puts 'failure'
end
# 差
if (x > 10)
# 省略主体
end
# 好
if x > 10
# 省略主体
end
这个规则的一个例外是条件表达式中的安全赋值。
在多行while/until
中,不要使用while/until condition do
。# 差
while x > 5 do
# 省略主体
end
until x > 5 do
# 省略主体
end
# 好
while x > 5
# 省略主体
end
until x > 5
# 省略主体
end
# 差
while some_condition
do_something
end
# 好
do_something while some_condition
# 差
do_something while !some_condition
# 好
do_something until some_condition
对于无限循环,使用Kernel#loop
而不是while/until
。# 差
while true
do_something
end
until false
do_something
end
# 好
loop do
do_something
end
对于后置条件循环语句,倾向使用Kernel#loop
与break
的组合,而不是begin/end/until
或begin/end/while
。# 差
begin
puts val
val += 1
end while val < 0
# 好
loop do
puts val
val += 1
break unless val < 0
end
# 差
user.set({ name: 'John', age: 45, permissions: { read: true } })
# 好
user.set(name: 'John', age: 45, permissions: { read: true })
对于 DSL 的内部方法调用,同时省略其外围的圆括号与花括号。class Person < ActiveRecord::Base
# 差
validates(:name, { presence: true, length: { within: 1..10 } })
# 好
validates :name, presence: true, length: { within: 1..10 }
end
当被调用方法是当前区块中唯一操作时,倾向使用简短的传参语法。# 差
names.map { |name| name.upcase }
# 好
names.map(&:upcase)
对于单行主体,倾向使用{...}
而不是do...end
。对于多行主体,避免使用{...}
。对于“流程控制”或“方法定义”(比如 Rakefile、其他 DSL 构成片段),总是使用do...end
。避免在链式方法调用中使用do...end
。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
对象。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
# 差
def some_method(some_arr)
return some_arr.size
end
# 好
def some_method(some_arr)
some_arr.size
end
避免在不需要的情况下使用self
。(只有在调用self
的修改器、以保留字命名的方法、重载的运算符时才需要)# 差
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
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
不要在条件表达式中使用=
(赋值语句)的返回值,除非赋值语句包裹在括号之中。这种惯用法被称作条件表达式中的安全赋值。# 差 - 会出现警告
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
# 差
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
# 差
name = name ? name : 'Bozhidar'
# 差
name = 'Bozhidar' unless name
# 好 - 当且仅当 name 为 nil 或 false 时,设置 name 的值为 'Bozhidar'
name ||= 'Bozhidar'
# 差 - 设置 enabled 的值为 true,即使其原本的值是 false
enabled ||= true
# 好
enabled = true if enabled.nil?
使用&&=
预先检查变量是否存在,如果存在,则做相应动作。使用&&=
语法可以省去if
检查。# 差
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
语句外的场合中使用,会产生难以理解的代码。# 差
Array === something
(1..100) === 7
/something/ === some_string
# 好
something.is_a?(Array)
(1..100).include?(7)
some_string =~ /something/
能使用==
就不要使用eql?
。提供更加严格比较的eql?
在实践中极少使用。# 差 - 对于字符串,eql? 与 == 具有相同效果
'ruby'.eql? some_str
# 好
'ruby' == some_str
1.0.eql? x # 当需要区别 Fixnum 1 与 Float 1.0 时,eql? 是具有意义的
避免使用 Perl 风格的特殊变量(比如$:
、$;
等)。它们看起来非常神秘,但除了单行脚本,其他情况并不鼓励使用。建议使用English
程序库提供的友好别名。# 差
$:.unshift File.dirname(__FILE__)
# 好
require 'English'
$LOAD_PATH.unshift File.dirname(__FILE__)
# 差
f (3 + 2) + 1
# 好
f(3 + 2) + 1
不要在方法中嵌套定义方法,使用 lambda 方法来替代。 嵌套定义产生的方法,事实上和外围方法处于同一作用域(比如类作用域)。此外,“嵌套方法”会在定义它的外围方法每次调用时被重新定义。# 差
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
定义语法。# 差
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
# 差
l = ->x, y { something(x, y) }
# 好
l = ->(x, y) { something(x, y) }
# 差
l = ->() { something }
# 好
l = -> { something }
# 差
p = Proc.new { |n| puts n }
# 好
p = proc { |n| puts n }
对于 lambda 方法或代码块,倾向使用proc.call()
而不是proc[]
或proc.()
。# 差 - 看上去像是枚举器的存取操作
l = ->(v) { puts v }
l[1]
# 差 - 极少见的调用语法
l = ->(v) { puts v }
l.(1)
# 好
l = ->(v) { puts v }
l.call(1)
未被使用的区块参数或局部变量,添加_
前缀或直接使用_
(尽管表意性略差)。这种做法可以抑制 Ruby 解释器或 RuboCop 等工具发出“变量尚未使用”的警告。# 差
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 中允许给常量重新赋值(可能是重定向某些流),但解释器会发出警告。
使用warn
而不是$stderr.puts
。除了更加简练清晰外,warn
允许你在需要时通过设置解释器选项(使用-W0
将警告级别设置为 0)来抑制警告。
倾向使用sprintf
或其别名format
而不是相当晦涩的String#%
方法。# 差
'%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#*
方法。# 差
%w[one two three] * ', '
# => 'one, two, three'
# 好
%w[one two three].join(', ')
# => 'one, two, three'
# 差
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?
来替代复杂的比较逻辑。# 差
do_something if x >= 1000 && x <= 2000
# 好
do_something if (1000..2000).include?(x)
# 好
do_something if x.between?(1000, 2000)
# 差
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
# 差
do_something if !something.nil?
do_something if something != nil
# 好
do_something if something
# 好 - 检查对象是布尔变量
def value_set?
!@some_boolean.nil?
end
永远不要使用END
区块。使用Kernel#at_exit
来替代。# 差
END { puts 'Goodbye!' }
# 好
at_exit { puts 'Goodbye!' }
倾向使用防御从句进行非法数据断言。防御从句是指处于方法顶部的条件语句,其能尽早地退出方法。
# 差
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
搭配起来一目了然。
不要使用count
作为size
的替代方案。除了Array
外,其他Enumerable
对象都需要通过枚举整个集合才可以确定数目。# 差
some_hash.count
# 好
some_hash.size
倾向使用flat_map
而不是map + flatten
的组合。此规则并不适用于深度超过 2 层的数组。举例来说,如果users.first.songs == ['a', ['b','c']]
成立,则使用map + flatten
的组合而不是flat_map
。flat_map
只能平坦化一个层级,而flatten
能够平坦化任意多个层级。# 差
all_songs = users.map(&:songs).flatten.uniq
# 好
all_songs = users.flat_map(&:songs).uniq
倾向使用reverse_each
而不是reverse.each
,因为某些混入Enumerable
模块的类可能会提供reverse_each
的高效版本。即使这些类没有提供专门特化的版本,继承自Enumerable
的通用版本至少能保证性能与reverse.each
相当。# 差
array.reverse.each { ... }
# 好
array.reverse_each { ... }