Ruby 编码风格指南

好的风格是成功的一半。


更新记录

  • 2016.06.26: 初稿

网上已经有一份非常详尽的 Ruby 风格指南,但是光看是没办法快速入门的,而且前面提到的指南太详尽,有的时候反而不太实用,所以我决定一边学习一边把自己的理解记录下来(几乎全部内容都来自前面的链接,我只是根据自己习惯重新编排了一下),一份良好的风格指南是极佳的学习资料。

源代码排版

  • 使用 UTF-8 编码
  • 用两个空格缩进
  • 不使用分号,一行一条语句
  • 定义方法时,避免单行写法
    • 这个规则的一个例外是空方法: def no_op; end
  • 大括号前后添加空格,小括号和中括号不
    • 在插值表达式中的大括号两端不要添加空格
  • whencase 缩排在同一层级
  • 在各个方法定义之间添加空行,并且将方法分成若干合乎逻辑的段落
  • 避免在方法调用的最后一个参数之后添加逗号,尤其当参数没有分布在同一行时
  • 当方法调用参数过长时,将它们排列在多行并对齐。若对齐后长度超过行宽限制,将首个参数位置挪到下一行进行缩排也是可以接受的
  • 当构建数组时,若元素跨行,应当保持对齐
  • 使用 _ 语法改善大数的数值字面量的可读性,如 num = 1_000_000
  • 不要在注释与 def 之间添加空行
  • 将单行长度控制在 80 个字符内,避免行尾空格,文件以空白行结束
  • 不要使用区块注释
  • 永远不要在方法名与左括号之间添加空格
  • 运行 Ruby 解释器时,总是开启 -w 选项来。如果你忘了某个上述某个规则,它就会警告你

对于没有主体的类,倾向使用单行定义

# 差
class FooError < StandardError
end
# 勉强可以
class FooError < StandardError; end
# 好
FooError = Class.new(StandardError)

当将一个条件表达式的结果赋值给一个变量时,保持分支缩排在同一层级

# 好 - 并且更好地利用行宽
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

当给方法的参数赋予默认值时,在 = 前后添加空格

# 好
def some_method(arg1 = :default, arg2 = nil, arg3 = [])
# 做一些事情
end

语法

  • 永远不要在多行 if/unless 中使用 then
  • 在多行 if/unless 中,总是把条件表达式与 if/unless 放置在同一行
  • 单行情况下,倾向使用三元操作符(?:)而不是 if/then/else/end 结构。前者更为常见且简练
  • 避免使用多行三元操作符(?:)。使用 if/unless 来替代
  • 三元操作符的每个分支只写一个表达式。即不要嵌套三元操作符。嵌套情况请使用 if/else 结构
  • case 表达式中,单行情况使用 when x then ... 语法
  • 使用 ! 而不是 not
  • 永远不要使用 andor 关键字。使用 &&|| 来替代
  • 不要使用括号包裹 if/unless/while/until 的条件表达式
  • 对于无限循环,使用 Kernel#loop 而不是 while/until
  • 避免在不需要流程控制的情况下使用 return
  • 避免在不需要的情况下使用 self。(只有在调用 self 的修改器时才需要)
  • 避免局部变量遮蔽方法调用,除非它们具有相同效果
  • 当变量尚未初始化时,使用 ||= 对其进行初始化
  • 不要使用 ||= 对布尔变量进行初始化
  • 避免使用 case 语句等价操作符 ===。从名称可知,这是 case 语句隐式使用的操作符,在 case 语句外的场合中使用,会产生难以理解的代码
  • 能使用 == 就不要使用 eql?。提供更加严格比较的 eql? 在实践中极少使用
  • 未被使用的区块参数或局部变量,添加 _ 前缀或直接使用 _(尽管表意性略差)。这种做法可以抑制 Ruby 解释器或 RuboCop 等工具发出“变量尚未使用”的警告
  • 使用 $stdout/$stderr/$stdin 而不是 STDOUT/STDERR/STDINSTDOUT/STDERR/STDIN 是常量,尽管在 Ruby 中允许给常量重新赋值(可能是重定向某些流),但解释器会发出警告
  • 使用 warn 而不是 $stderr.puts。除了更加简练清晰外,warn 允许你在需要时通过设置解释器选项(使用 -W0 将警告级别设置为 0)来抑制警告
  • 倾向使用 map 而不是 collectfind 而不是 detectselect 而不是 find_allreduce 而不是 inject 以及 size 而不是 length。这不是一个硬性要求,如果使用别名可以增强可读性,使用它也没关系。这些别名方法继承自 Smalltalk 语言,但在别的语言并不通用。鼓励使用 select 而不是 find_all 的理由是前者与 reject 搭配起来一目了然
  • 不要使用 count 作为 size 的替代方案。除了 Array 外,其他 Enumerable 对象都需要通过枚举整个集合才可以确定数目
  • 倾向使用 reverse_each 而不是 reverse.each,因为某些混入 Enumerable 模块的类可能会提供 reverse_each 的高效版本。即使这些类没有提供专门特化的版本,继承自 Enumerable 的通用版本至少能保证性能与 reverse.each 相当

使用 :: 引用常量(包括类与模块)与构造器(比如 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_with_parameters(param1, param2)
# 省略主体
end

定义可选参数时,将可选参数放置在参数列表尾部。如果可选参数出现在列表头部,则此方法在调用时可能会产生预期之外的结果

# 差
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'

利用『ifcase 是表达式』的这个特性

# 差
if condition
result = x
else
result = y
end
# 好
result =
if condition
x
else
y
end

避免在多行区块后使用 if/unless 修饰语法

# 差
10.times do
# 省略多行主体
end if some_condition
# 好
if some_condition
10.times do
# 省略多行主体
end
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

对于后置条件循环语句,倾向使用 Kernel#loopbreak 的组合,而不是 begin/end/untilbegin/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)

不要在条件表达式中使用 =(赋值语句)的返回值,除非赋值语句包裹在括号之中。这种惯用法被称作条件表达式中的安全赋值

# 差 - 会出现警告
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

使用 &&= 预先检查变量是否存在,如果存在,则做相应动作。使用 &&= 语法可以省去 if 检查

# 差
if something
something = something.downcase
end
# 差
something = something ? something.downcase : nil
# 勉强可以
something = something.downcase if something
# 好
something = something && something.downcase
# 更好
something &&= something.downcase

避免使用 Perl 风格的特殊变量(比如 $:$; 等)。它们看起来非常神秘,但除了单行脚本,其他情况并不鼓励使用。建议使用 English 程序库提供的友好别名

# 差
$:.unshift File.dirname(__FILE__)
# 好
require 'English'
$LOAD_PATH.unshift File.dirname(__FILE__)

不要在方法中嵌套定义方法,使用 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

定义 lambda 方法时,如果有参数则使用括号

# 差
l = ->x, y { something(x, y) }
# 好
l = ->(x, y) { something(x, y) }

定义 lambda 方法时,如果无参数则省略括号

# 差
l = ->() { something }
# 好
l = -> { something }

倾向使用 proc 而不是 Proc.new

# 差
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)

倾向使用 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'

当你希望处理的变量类型是数组,但不太确定其是否真的是数组时,通过使用 Array() 来替代显式的数组类型检查与转换

# 差
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

不做显式的 non-nil 检查,除非检查对象是布尔变量

# 差
do_something if !something.nil?
do_something if something != nil
# 好
do_something if something
# 好 - 检查对象是布尔变量
def value_set?
!@some_boolean.nil?
end

避免使用 BEGIN 区块。永远不要使用 END 区块。使用 Kernel#at_exit 来替代

# 差
END { puts 'Goodbye!' }
# 好
at_exit { puts 'Goodbye!' }

倾向使用防御从句进行非法数据断言。防御从句是指处于方法顶部的条件语句,其能尽早地退出方法

# 差
def compute_thing(thing)
if thing[:foo]
update_with_bar(thing)
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

命名

  • 标识符使用英文命名
  • 符号、方法、变量使用蛇底式小写(snake_case
  • 类与模块使用驼峰式大小写(CamelCase)。(HTTP、RFC、XML 等首字母缩写应该仍旧保持大写形式)
  • 文件名使用蛇底式小写,如 hello_world.rb
  • 目录名使用蛇底式小写,如 lib/hello_world/hello_world.rb
  • 尽量使一个源文件中只有一个类或模块。文件名就是类名或模块名,但使用蛇底式小写而不是驼峰式大小写
  • 其他常量使用尖叫蛇底式大写(SCREAMING_SNAKE_CASE
  • 谓词方法(返回布尔值的方法)的名字应当以问号结尾。(比如 Array#empty?)。不返回布尔值的方法不应以问号结尾
  • 谓词方法的名字应当避免使用 isdoescan 等助动词作为前缀。这些助动词在实际场景中显得冗余,且与标准库的命名习惯(比如 empty?include?)很不一致
  • 具有潜在危险性的方法,当其存在对应安全版本的方法时,其名字应当以惊叹号结尾。(比如修改 self 或参数值的方法、相对 exit 方法不会在退出时运行 finalizers 执行清理工作的 exit! 方法等)
  • 尽量根据危险方法来定义对应安全版本的方法
  • 当配合单行区块使用 reduce 时,将参数命名为 |a, e|(accumulator/累加器,element/元素)
  • 当定义二元操作符时,将参数命名为 other<<[] 例外,因为其语义与此不同)

类与模块

在类定义中,使用一致的结构

class Person
# 首先是 extend 与 include
extend SomeModule
include AnotherModule
# 内部类
CustomErrorKlass = 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

如果嵌套类数目较多,进而导致外围类定义较长,则将它们从外围类中提取出来,分别放置在单独的以嵌套类命名的文件中,并将文件归类至以外围类命名的文件夹下

# 差
# 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

定义只有类方法的数据类型时,倾向使用模块而不是类。只有当需要实例化时才使用类

# 差
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

总是替那些用以表示领域模型的类提供一个适当的 to_s 方法

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 系列方法来定义琐碎的存取器或修改器

# 差
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

优先考虑使用 Struct.new。它替你定义了那些琐碎的访问器、构造器及比较操作符

# 好
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

倾向使用鸭子类型而不是继承

# 差
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

避免使用类变量(@@)。类变量在继承方面存在令人生厌的行为

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'

使用 def self.method 定义类方法。这种做法使得在代码重构时,即使修改了类名也无需做多次修改

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。因为定义期间 aliasself 指向的都是词法作用域,除非明确说明,否则该别名所引用的方法不会在运行期间被改变,或是在任何子类中被修改。因为 aliasdef 一样都是关键字,倾向使用裸字而不是符号或字符串。也就是说,使用 alias foo bar 而不是 alias :foo :bar

class Westerner
def first_name
@names.first
end
alias given_name first_name
end

另外需要了解 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

异常

对于异常处理,倾向使用 raise 而不是 fail

# 差
fail SomeException, 'message'
# 好
raise SomeException, 'message'

不要在带双参数形式的 raise 方法中显式指定 RuntimeError

# 差
raise RuntimeError, 'message'
# 好 - 默认就是 RuntimeError
raise 'message'

倾向使用带异常类、消息的双参数形式调用 raise 方法,而不是使用异常的实例

# 差 - 并无 raise SomeException.new('message') [, backtraces] 这种调用形式
raise SomeException.new('message')
# 好 - 与调用形式 raise SomeException [, 'message' [, backtraces]] 保持一致
raise SomeException, 'message'

永远不要从 ensure 区块返回。如果你显式地从 ensure 区块返回,那么其所在的方法会如同永远不会发生异常般的返回。事实上,异常被默默地丢弃了

def foo
raise
ensure
return 'very bad idea'
end

尽可能隐式地使用 begin/rescue/ensure/end 区块

# 差
def foo
begin
# 主逻辑
rescue
# 异常处理逻辑
end
end
# 好
def foo
# 主逻辑
rescue
# 异常处理逻辑
end

通过使用 contingency 方法(一个由 Avdi Grimm 创造的词)来减少 begin/rescue/ensure/end 区块的使用

# 差
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 }

避免捕获 Exception。这种做法会同时将信号与 exit 方法困住,导致你必须使用 kill -9 来终止进程

# 差 - 信号与 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

把较具体的异常放在处理链的较上层,不然它们永远不会被执行

# 差
begin
# 抛出异常
rescue StandardError => e
# 处理异常
rescue IOError => e
# 处理异常,但事实上永远不会被执行
end
# 好
begin
# 抛出异常
rescue IOError => e
# 处理异常
rescue StandardError => e
# 处理异常
end

ensure 区块释放程序的外部资源

f = File.open('testfile')
begin
# .. 文件操作
rescue
# .. 处理异常
ensure
f.close if f
end

在调用资源获取方法时,尽量使用具备自动清理功能的版本

# 差 - 需要显式关闭文件描述符
f = File.open('testfile')
# ...
f.close
# 好 - 文件描述符会被自动关闭
File.open('testfile') do |f|
# ...
end

集合

  • 当访问数组的首元素或尾元素时,倾向使用 firstlast 而不是 [0][-1]
  • 当处理的对象不存在重复元素时,使用 Set 来替代 ArraySet 是实现了无序且无重复元素的集合类型。它兼具 Array 的直观操作与 Hash 的快速查找
  • 利用“Ruby 1.9 之后的哈希是有序的”的这个特性
  • 当遍历集合时,不要改动它

对于数组与哈希,倾向使用字面量语法来构建实例(除非你需要给构造器传递参数)

# 差
arr = Array.new
hash = Hash.new
# 好
arr = []
hash = {}

当创建一组元素为单词(没有空格或特殊字符)的数组时,倾向使用 %w 而不是 []。此规则只适用于数组元素有两个或以上的时候

# 差
STATES = ['draft', 'open', 'closed']
# 好
STATES = %w(draft open closed)

当创建一组符号类型的数组(且不需要保持 Ruby 1.9 兼容性)时,倾向使用 %i。此规则只适用于数组元素有两个或以上的时候

# 差
STATES = [:draft, :open, :closed]
# 好
STATES = %i(draft open closed)

避免在数组中创造巨大的间隔

arr = []
arr[100] = 1 # 现在你有一个很多 nil 的数组

当哈希键为符号时,使用 Ruby 1.9 的字面量语法

# 差
hash = { :one => 1, :two => 2, :three => 3 }
# 好
hash = { one: 1, two: 2, three: 3 }

当哈希键既有符号又有字符串时,不要使用 Ruby 1.9 的字面量语法

# 差
{ a: 1, 'b' => 2 }
# 好
{ :a => 1, 'b' => 2 }

倾向使用 Hash#key? 而不是 Hash#has_key?,使用 Hash#value? 而不是 Hash#has_value?

# 差
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

# 差
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

heroes = { batman: 'Bruce Wayne', superman: 'Clark Kent' }
# 差 - 如果我们打错了哈希键,则难以发现这个错误
heroes[:batman] # => 'Bruce Wayne'
heroes[:supermann] # => nil
# 好 - fetch 会抛出 KeyError 使这个错误显而易见
heroes.fetch(:supermann)

当为哈希键的值提供默认值时,倾向使用 Hash#fetch 而不是自定义逻辑

batman = { name: 'Bruce Wayne', is_evil: false }
# 差 - 如果仅仅使用 || 操作符,那么当值为假时,我们不会得到预期结果
batman[:is_evil] || true # => true
# 好 - fetch 在遇到假值时依然可以正确工作
batman.fetch(:is_evil, true) # => false

当提供默认值的求值代码具有副作用或开销较大时,倾向使用 Hash#fetch 的区块形式

batman = { name: 'Bruce Wayne' }
# 差 - 此形式会立即求值,如果调用多次,可能会影响程序的性能
batman.fetch(:powers, obtain_batman_powers) # obtain_batman_powers 开销较大
# 好 - 此形式会惰性求值,只有抛出 KeyError 时,才会产生开销
batman.fetch(:powers) { obtain_batman_powers }

当需要一次性从哈希中获取多个键的值时,使用 Hash#values_at

# 差
email = data['email']
username = data['nickname']
# 好
email, username = data.values_at('email', 'nickname')

当为集合提供存取器时,尽量支持索引值为 nil 的访问形式

# 差
def awesome_things
@awesome_things
end
# 好
def awesome_things(index = nil)
if index && @awesome_things
@awesome_things[index]
else
@awesome_things
end
end

数值

通过 Integer 检查对象是否是数值类型,而不是 FixnumBignum。因为 FixnumBignum 表达的数值大小存在范围限定

timestamp = Time.now.to_i
# 差
timestamp.is_a? Fixnum
timestamp.is_a? Bignum
# 好
timestamp.is_a? Integer

字符串

倾向使用字符串插值或字符串格式化,而不是字符串拼接

# 差
email_with_name = user.name + ' <' + user.email + '>'
# 好
email_with_name = "#{user.name} <#{user.email}>"
# 好
email_with_name = format('%s <%s>', user.name, user.email)

不要忘记使用 {} 包裹字符串插值中的实例变量或全局变量

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 会自动调用它

# 差
message = "This is the #{result.to_s}."
# 好
message = "This is the #{result}."

当你需要构造巨大的数据块时,避免使用 String#+,使用 String#<< 来替代。String#<< 通过修改原始对象进行拼接工作,其比 String#+ 效率更高,因为后者需要产生一堆新的字符串对象

# 差
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

url = 'http://example.com'
str = 'lisp-case-rules'
# 差
url.gsub('http://', 'https://')
str.gsub('-', '_')
# 好
url.sub('http://', 'https://')
str.tr('-', '_')

其他

  • 总是开启 ruby -w 选项,以编写安全的代码
  • 避免使用哈希作为可选参数。这个方法是不是做太多事了?(对象构造器除外)
  • 避免单个方法的长度超过 10 行(不计入空行)。理想上,大部分方法应当不超过 5 行
  • 避免参数列表数目多于三或四个
  • 如果你真的需要“全局”方法,将它们添加到 Kernel 并设为私有
  • 使用 OptionParser 来解析复杂的命令行选项。使用 ruby -s 来处理琐碎的命令行选项
  • 使用 Time.now 而不是 Time.new 来获取当前的系统时间
  • 使用函数式思维编写程序,避免副作用
  • 不要修改参数值,除非那就是这个方法的作用

相关工具

  • RuboCop - 代码风格检查工具
  • RDoc - 编写 API 文档

参考资料

捧个钱场?