今天review了一次ruby编程规范,把一些之前我不熟识的在这里记录下来。
- 对于没有主体的类,倾向使用单行定义。 [link]
# 差
class FooError < StandardError
end
# 勉强可以
class FooError < StandardError; end
# 好
FooError = Class.new(StandardError)
- 把 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]
# 差 - 有几个零?
num = 1000000
# 好 - 方便人脑理解
num = 1_000_000
- 使用 &&= 预先检查变量是否存在,如果存在,则做相应动作。使用 &&= 语法可以省去 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
- 未被使用的区块参数或局部变量,添加 _ 前缀或直接使用 _(尽管表意性略差)。这种做法可以抑制 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]
倾向使用 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'
- 当你希望处理的变量类型是数组,但不太确定其是否真的是数组时,通过使用 [*var] 或 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)
- 倾向使用 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 { ... }
关于注释的一些说明
使用 TODO 标记应当加入的特征与功能。 [link]
使用 FIXME 标记需要修复的代码。 [link]
使用 OPTIMISE 标记可能引发性能问题的低效代码。 [link]
使用 HACK 标记代码异味,即那些应当被重构的可疑编码习惯。 [link]
使用 REVIEW 标记需要确认与编码意图是否一致的可疑代码。比如,REVIEW: Are we sure this is how the client does X currently?。 [link]
类定义
- 在类定义中,使用一致的结构。 [link]
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
- 如果嵌套类数目较多,进而导致外围类定义较长,则将它们从外围类中提取出来,分别放置在单独的以嵌套类命名的文件中,并将文件归类至以外围类命名的文件夹下。 [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
- 优先考虑通过工厂方法的方式创建某些具有特定意义的实例对象。 [link]
class Person
def self.create(options_hash)
# 省略主体
end
end
- 尽可能隐式地使用 begin/rescue/ensure/end区块。 [link]
# 差
def foo
begin
# 主逻辑
rescue
# 异常处理逻辑
end
end
# 好
def foo
# 主逻辑
rescue
# 异常处理逻辑
end
- 当创建一组元素为单词(没有空格或特殊字符)的数组时,倾向使用 %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)
- 当访问集合中的元素时,倾向使用对象所提供的方法进行访问,而不是直接调用对象属性上的 [n]方法。这种做法可以防止你在 nil 对象上调用 []。 [link]
# 差
Regexp.last_match[1]
# 好
Regexp.last_match(1)
- 当你需要构造巨大的数据块时,避免使用 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('-', '_')
- 使用 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
- 当你不需要分组结果时,使用非捕获组。 [link]
# 差
/(first|second)/
# 好
/(?:first|second)/
- 避免使用 Perl 风格的、用以代表最近的捕获组的特殊变量(比如 $1、$2 等)。使用 Regexp.last_match(n) 来替代。[link]
/(regexp)/ =~ string
...
# 差
process $1
# 好
process Regexp.last_match(1)
- 小心使用 ^ 与 $ ,它们匹配的是一行的开始与结束,而不是字符串的开始与结束。如果你想要匹配整个字符串,使用 \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
- 只有当字符串中同时存在插值与双引号,且是单行时,才使用 %()(%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/(.*)$}
避免使用 method_missing。它会使你的调用栈变得凌乱;其方法不被罗列在 #methods 中;拼错的方法可能会默默地工作(nukes.launch_state = false)。优先考虑使用委托、代理、或是 define_method
来替代。如果你必须使用 method_missing 的话,务必做到以下几点: [link]
- 确保同时定义了 respond_to_missing?。
- 仅仅捕获那些具有良好语义前缀的方法,像是 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 定义对应的方法