份 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_somethingend# 好def some_method do_somethingend
使用 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 < StandardErrorend# 勉强可以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 bodyend
这个规则的一个例外是空方法。
# 好def no_op; end
操作符前后适当地添加空格,在逗号 ,
、冒号 :
及分号 ;
之后。尽管 Ruby 解释器(大部分情况下)会忽略空格,但适量的空格可以增强代码的可读性。[link]
sum = 1 + 2a, b = 1, 2class 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.playend# 好casewhen 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.playend
当将一个条件表达式的结果赋值给一个变量时,保持分支缩排在同一层级。[link]
# 差 - 非常费解kind = case yearwhen 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'endresult = if some_cond calc_somethingelse calc_something_elseend# 好 - 结构清晰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' endresult = 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' endresult = if some_cond calc_something else calc_something_else end
在各个方法定义之间添加空行,并且将方法分成若干合乎逻辑的段落。[link]
def some_method data = initialize(options) data.manipulate! data.resultenddef some_method resultend
在各个段落之间,使用一个空行分隔。[link]
# 差 - 使用了两个空行some_method some_method# 好some_method some_method
在属性修饰器之后,使用一个空行分隔。[link]
# 差class Foo attr_reader :foo def foo # 做一些事情 endend# 好class Foo attr_reader :foo def foo # 做一些事情 endend
在不同缩进的代码之间,不要使用空行分隔。[link]
# 差class Foo def foo begin do_something do something end rescue something end endend# 好class Foo def foo begin do_something do something end rescue something end endend
避免在方法调用的最后一个参数之后添加逗号,尤其当参数没有分布在同一行时。[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 \ - 2long_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 = 01234num = 0O1234num = 0X12ABnum = 0B10101num = 0D1234num = 0d1234# 好 - 方便区分数值前缀与具体数字num = 0o1234num = 0x12ABnum = 0b10101num = 1234
使用 RDoc 及其惯例来编写 API 文档。注意,不要在注释与 def
之间添加空行。[link]
将单行长度控制在 80 个字符内。[link]
避免行尾空格。[link]
文件以空白行结束。[link]
不要使用区块注释。它们不能被空白字符引导,且不如常规注释容易辨认。[link]
# 差=begincomment lineanother 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_methodSomeModule::SomeClass::SOME_CONSTSomeModule::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 # 好 # 省略主体endputs(temperance.age) # 差puts temperance.age # 好
定义可选参数时,将可选参数放置在参数列表尾部。如果可选参数出现在列表头部,则此方法在调用时可能会产生预期之外的结果。[link]
# 差def some_method(a = 1, b = 2, c, d) puts "#{a}, #{b}, #{c}, #{d}"endsome_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}"endsome_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, aputs a # => 'bar'puts b # => 'foo'# 好 - 右值为方法调用返回值def multi_return [1, 2]endfirst, 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 elemend# 注意,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_elseend# 好if some_condition do_something do_something_elseend
倾向使用三元操作符(?:
)而不是 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_elseelse something_elseend
永远不要使用 if x; ...
。使用三元操作符来替代。[link]
# 差result = if some_condition; something else something_else end# 好result = some_condition ? something : something_else
利用“if
与 case
是表达式”的这个特性。[link]
# 差if condition result = xelse result = yend# 好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_somethingend# 好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 # 省略多行主体 endend
避免使用嵌套 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 # 省略主体enduntil x > 5 do # 省略主体end# 好while x > 5 # 省略主体enduntil x > 5 # 省略主体end
对于单行主体,倾向使用 while/until
修饰语法。[link]
# 差while some_condition do_somethingend# 好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_somethingenduntil false do_somethingend# 好loop do do_somethingend
对于后置条件循环语句,倾向使用 Kernel#loop
与 break
的组合,而不是 begin/end/until
或 begin/end/while
。[link]
# 差begin puts val val += 1end while val < 0# 好loop do puts val val += 1 break unless val < 0end
对于可选参数的哈希,省略其外围的花括号。[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 nameend# 好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 } # 通过创建区块的手法来传递参数 endend# 好def with_tmp_dir(&block) Dir.mktmpdir do |tmp_dir| Dir.chdir(tmp_dir, &block) endendwith_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.sizeend# 好def some_method(some_arr) some_arr.sizeend
避免在不需要的情况下使用 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 == :verifiedend# 好def ready? if last_reviewed_at > last_updated_at worker.update(content, options) self.status = :in_progress end status == :verifiedend
避免局部变量遮蔽方法调用,除非它们具有相同效果。[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 endend
不要在条件表达式中使用 =
(赋值语句)的返回值,除非赋值语句包裹在括号之中。这种惯用法被称作条件表达式中的安全赋值。[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,即使其原本的值是 falseenabled ||= true# 好enabled = true if enabled.nil?
使用 &&=
预先检查变量是否存在,如果存在,则做相应动作。使用 &&=
语法可以省去 if
检查。[link]
# 差if something something = something.downcaseend# 差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_str1.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) # 省略主体enddef 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 / 50end# 好l = ->(a, b) { a + b } l.call(1, 2) l = lambda do |a, b| tmp = a * 7 tmp * b / 50end
定义 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? Arraypaths.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 == 0endif x % 2 == 1endif x == nilend# 好if x.even?endif x.odd?endif x.nil?endif x.zero?endif x == 0end
不做显式的 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 endend# 好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 endend# 好[0, 1, 2, 3].each do |item| next unless item > 1 puts itemend
倾向使用 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:someSymbolsomeVar = 5var_10 = 10def someMethod ...enddef SomeMethod ...end# 好:some_symbolsome_var = 5var10 = 10def some_method ...end
给符号、方法、变量命名时,避免分隔字母与数字。[link]
# 差:some_sym_1some_var_1 = 1def some_method_1 # 做一些事情end# 好:some_sym1some_var1 = 1def some_method1 # 做一些事情end
类与模块使用驼峰式大小写(CamelCase
)。(HTTP、RFC、XML 等首字母缩写应该仍旧保持大写形式)[link]
# 差class Someclass ...endclass Some_Class ...endclass SomeXml ...endclass XmlSomething ...end# 好class SomeClass ...endclass SomeXML ...endclass 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 endend# 好class Person def tall? true end def basketball_player? false end def likes_candy? true endend
具有潜在危险性的方法,当其存在对应安全版本的方法时,其名字应当以惊叹号结尾。(比如修改 self
或参数值的方法、相对 exit
方法不会在退出时运行 finalizers 执行清理工作的 exit!
方法等)[link]
# 差 - 没有对应安全版本的方法class Person def update! endend# 好class Person def update endend# 好class Person def update! end def update endend
尽量根据危险方法来定义对应安全版本的方法。[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! endend
当定义二元操作符时,将参数命名为 other
(<<
与 []
例外,因为其语义与此不同)。[link]
def +(other) # 省略主体end
注释
良好的代码自身就是最佳的文档。当你要添加一个注释时,扪心自问,“如何改善代码让它不需要注释?” 改善代码,再写相应文档使之更清楚。
——Steve McConnell
编写让人一目了然的代码然后忽略这一节的其它部分。我是认真的![link]
使用英文编写注释。[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 # OPTIMIZEend
使用 TODO
标记应当加入的特征与功能。[link]
使用 FIXME
标记需要修复的代码。[link]
使用 OPTIMIZE
标记可能引发性能问题的低效代码。[link]
使用 HACK
标记代码异味,即那些应当被重构的可疑编码习惯。[link]
使用 REVIEW
标记需要确认与编码意图是否一致的可疑代码。比如,REVIEW: Are we sure this is how the client does X currently?
。[link]
适当情况下,可以自行定制其他注解关键字,但别忘记在项目的 README
或类似文档中予以说明。[link]
Magic Comments
Place magic comments above all code and documentation. Magic comments should only go below shebangs if they are needed in your source file.[link]
# good# frozen_string_literal: true# Some documentation about Personclass Personend# bad# Some documentation about Person# frozen_string_literal: trueclass Personend
# good#!/usr/bin/env ruby# frozen_string_literal: trueApp.parse(ARGV)# bad# frozen_string_literal: true#!/usr/bin/env rubyApp.parse(ARGV)
Use one magic comment per line if you need multiple.[link]
# good# frozen_string_literal: true# encoding: ascii-8bit# bad# -*- frozen_string_literal: true; encoding: ascii-8bit -*-
Separate magic comments from code and documentation with a blank line.[link]
# good# frozen_string_literal: true# Some documentation for Personclass Person # Some codeend# bad# frozen_string_literal: true# Some documentation for Personclass Person # Some codeend
类与模块
在类定义中,使用一致的结构。[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 endend
在混入多个模块时,倾向使用多行语法。[link]
# 差class Person include Foo, Barend# 好class Person include Foo include Barend
如果嵌套类数目较多,进而导致外围类定义较长,则将它们从外围类中提取出来,分别放置在单独的以嵌套类命名的文件中,并将文件归类至以外围类命名的文件夹下。[link]
# 差# foo.rbclass Foo class Bar # 定义 30 多个方法 end class Car # 定义 20 多个方法 end # 定义 30 多个方法end# 好# foo.rbclass Foo # 定义 30 多个方法end# foo/bar.rbclass Foo class Bar # 定义 30 多个方法 endend# foo/car.rbclass Foo class Car # 定义 20 多个方法 endend
定义只有类方法的数据类型时,倾向使用模块而不是类。只有当需要实例化时才使用类。[link]
# 差class SomeClass def self.some_method # 省略主体 end def self.some_other_method # 省略主体 endend# 好module SomeModule module_function def some_method # 省略主体 end def some_other_method # 省略主体 endend
当你想将模块的实例方法变成类方法时,倾向使用 module_function
而不是 extend self
。[link]
# 差module Utilities extend self def parse_something(string) # 做一些事情 end def other_utility_method(number, string) # 做一些事情 endend# 好module Utilities module_function def parse_something(string) # 做一些事情 end def other_utility_method(number, string) # 做一些事情 endend
当设计类的层次结构时,确保它们符合里式替换原则。[link]
让你的类尽量满足 SOLID 原则 。[link]
总是替那些用以表示领域模型的类提供一个适当的 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}" endend
使用 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 endend# 好class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name endend
对于访问器方法,避免使用 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(' ') endend# 好class Person def name "#{@first_name} #{@last_name}" end def name=(name) @first_name, @last_name = name.split(' ') endend
避免使用 attr
。使用 attr_reader
与 attr_accessor
来替代。[link]
# 差 - 创建单个存取方法(此方法在 Ruby 1.9 之后被移除了)attr :something, trueattr :one, :two, :three # 类似于 attr_reader# 好attr_accessor :somethingattr_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 endend# 更好Person = Struct.new(:first_name, :last_name) doend
不要扩展 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) # 省略主体 endend
倾向使用鸭子类型而不是继承。[link]
# 差class Animal # 抽象方法 def speak endend# 继承父类class Duck < Animal def speak puts 'Quack! Quack' endend# 继承父类class Dog < Animal def speak puts 'Bau! Bau!' endend# 好class Duck def speak puts 'Quack! Quack' endendclass Dog def speak puts 'Bau! Bau!' endend
避免使用类变量(@@
)。类变量在继承方面存在令人生厌的行为。[link]
class Parent @@class_var = 'parent' def self.print_class_var puts @@class_var endendclass Child < Parent @@class_var = 'child'endParent.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 # ... endend
使用 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 endend
在类的词法作用域中定义方法别名时,倾向使用 alias
。因为定义期间 alias
与 self
指向的都是词法作用域,除非明确说明,否则该别名所引用的方法不会在运行期间被改变,或是在任何子类中被修改。[link]
class Westerner def first_name @names.first end alias given_name first_nameend
因为 alias
与 def
一样都是关键字,倾向使用裸字而不是符号或字符串。也就是说,使用 alias foo bar
而不是 alias :foo :bar
。
另外需要了解 Ruby 是如何处理别名和继承的:别名所引用的原始方法是在定义期间被指定的,而不是运行期间。
class Fugitive < Westerner def first_name 'Nobody' endend
在这个例子中,Fugitive#given_name
仍然调用原先的 Westerner#first_name
方法,而不是 Fugitive#first_name
。如果想要覆写 Fugitive#given_name
,必须在子类中重新定义。
class Fugitive < Westerner def first_name 'Nobody' end alias given_name first_nameend
在运行期间定义模块方法、类方法、单件方法的别名时,总是使用 alias_method
。在上述情况下,使用 alias
可能会导致预期之外的结果。[link]
module Mononymous def self.included(other) other.class_eval { alias_method :full_name, :given_name } endendclass Sting < Westerner include Mononymousend
在模块方法,或是类方法内部调用自身其他方法时,通常省略模块名/类名/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'# 好 - 默认就是 RuntimeErrorraise 'message'
倾向使用带异常类、消息的双参数形式调用 raise
方法,而不是使用异常的实例。[link]
# 差 - 并无 raise SomeException.new('message') [, backtraces] 这种调用形式raise SomeException.new('message')# 好 - 与调用形式 raise SomeException [, 'message' [, backtraces]] 保持一致raise SomeException, 'message'
永远不要从 ensure
区块返回。如果你显式地从 ensure
区块返回,那么其所在的方法会如同永远不会发生异常般的返回。事实上,异常被默默地丢弃了。[link]
def foo raiseensure return 'very bad idea'end
尽可能隐式地使用 begin/rescue/ensure/end
区块。[link]
# 差def foo begin # 主逻辑 rescue # 异常处理逻辑 endend# 好def foo # 主逻辑rescue # 异常处理逻辑end
通过使用 contingency 方法(一个由 Avdi Grimm 创造的词)来减少 begin/rescue/ensure/end
区块的使用。[link]
# 差begin something_that_might_failrescue IOError # 处理 IOErrorendbegin something_else_that_might_failrescue IOError # 处理 IOErrorend# 好def with_io_error_handling yieldrescue IOError # 处理 IOErrorendwith_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_filerescue Errno::ENOENT => ex handle_error(ex)end
不要将异常处理作为流程控制使用。[link]
# 差begin n / drescue ZeroDivisionError puts 'Cannot divide by 0!'end# 好if d.zero? puts 'Cannot divide by 0!'else n / dend
避免捕获 Exception
。这种做法会同时将信号与 exit
方法困住,导致你必须使用 kill -9
来终止进程。[link]
# 差 - 信号与 exit 方法产生的异常会被捕获(除了 kill -9)begin exitrescue Exception puts "you didn't really want to exit, right?" # 处理异常end# 好 - 没有指定具体异常的 rescue 子句默认捕获 StandardErrorbegin # 抛出异常rescue => e # 处理异常end# 好 - 指定具体异常 StandardErrorbegin # 抛出异常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 fend
在调用资源获取方法时,尽量使用具备自动清理功能的版本。[link]
# 差 - 需要显式关闭文件描述符f = File.open('testfile') # ...f.close# 好 - 文件描述符会被自动关闭File.open('testfile') do |f| # ...end
倾向使用标准库中的异常类而不是引入新的类型。[link]
集合
对于数组与哈希,倾向使用字面量语法来构建实例(除非你需要给构造器传递参数)。[link]
# 差arr = Array.newhash = 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_thingsend# 好def awesome_things(index = nil) if index && @awesome_things @awesome_things[index] else @awesome_things endend
数值
通过 Integer
检查对象是否是数值类型,而不是 Fixnum
或 Bignum
。因为 Fixnum
或 Bignum
表达的数值大小存在范围限定。[link]
timestamp = Time.now.to_i# 差timestamp.is_a? Fixnumtimestamp.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}" endend$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 |endEND# => "def test\n some_method\n other_method\nend\n"
使用 Ruby 2.3 新增的 <<~
操作符来缩排 heredocs 中的多行文本。[link]
# 差 - 使用 Powerpack 程序库的 String#strip_margincode = <<-END.strip_margin('|') |def test | some_method | other_method |endEND# 差code = <<-ENDdef test some_method other_methodendEND# 好code = <<~END def test some_method other_method endEND
日期与时间
避免使用 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 风格的、用以代表最近的捕获组的特殊变量(比如 、
等)。使用
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{"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}|
针对构建字符串的 %q
, %Q
字面量,使用 ()
。
针对构建数组的 %w
, %i
, %W
, %I
字面量,使用 []
,以与常规的数组字面量保持一致。
针对构建正则的 %r
字面量,使用 {}
,此乃惯例。
针对 %s
, %x
等其他字面量,使用 ()
。
元编程
避免无谓的元编程。[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.rbUNSAFE_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 endend
避免使用 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 endend# 好def method_missing?(meth, *params, &block) if /^find_by_(?<prop>.*)/ =~ meth find_by(prop, *params, &block) else super endend# 最好的方式可能是在每个需要支持的属性被声明时,使用 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! ... endendclass Organization < ActiveRecord::Base include Activatableendlinux_organization = Organization.find(...)# 差 - 会破坏对象的封装性linux_organization.send(:reset_token)# 好 - 会抛出异常linux_organization.public_send(:reset_token)
倾向使用 __send__
而不是 send
,因为 send
可能会被覆写。[link]
require 'socket'u1 = UDPSocket.newu1.bind('127.0.0.1', 4913) u2 = UDPSocket.newu2.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 endendFoo.bar = 1
使用 OptionParser
来解析复杂的命令行选项。使用 ruby -s
来处理琐碎的命令行选项。[link]
使用 Time.now
而不是 Time.new
来获取当前的系统时间。[link]
使用函数式思维编写程序,避免副作用。[link]
不要修改参数值,除非那就是这个方法的作用。[link]
避免使用三层以上的嵌套区块。[link]
保持一致性。在理想的世界里,遵循这些准则。[link]
使用常识。[link]
工具
以下的一些工具可以帮助你自动检查项目中的 Ruby 代码是否符合这份指南。
RuboCop
RuboCop 是一个基于本指南的 Ruby 代码风格检查工具。RuboCop 涵盖了本指南相当大的部分,其同时支持 MRI 1.9 和 MRI 2.0,且与 Emacs 整合良好。
RubyMine
RubyMine 的代码检查部分基于本指南。
TML <base> 元素
指定用于一个文档中包含的所有相对 URL 的根 URL。一份中只能有一个 <base> 元素。
包含属性href 和
target 可以指定a 标签的默认窗口打开行为
<base href="https://www.baidu.com/img/123" target="_banlk"></base> 默认就打开新的窗口 <a href="aaa">123</a>
使用
<base href="https://www.baidu.com/img/"></base> <img src="bd_logo1.png?where=super"></img>
虽然在codepen 的代码上编写的但是能够正确的通过base url + img 的src 定位显示出图片
content 标签
HTML <aside> 元素
表示一个和其余页面内容几乎无关的部分,被认为是独立于该内容的一部分并且可以被单独的拆分出来而不会使整体受影响。
HTML <blockquote> 元素
(或者 HTML 块级引用元素),代表其中的文字是引用内容。通常在渲染时,这部分的内容会有一定的缩进(注 中说明了如何更改)。若引文来源于网络,则可以将原内容的出处 URL 地址设置到 cite 特性上,若要以文本的形式告知读者引文的出处时,可以通过 <cite> 元素。
HTML <figure> 元素
代表一段独立的内容, 经常与说明(caption) <figcaption> 配合使用, 并且作为一个独立的引用单元。当它属于主内容流(main flow)时,它的位置独立于主体。这个标签经常是在主文中引用的图片,插图,表格,代码段等等,当这部分转移到附录中或者其他页面时不会影响到主体。
Inline text semantics
HTML 缩写元素(<abbr>)
用于展示缩写,并且可以通过可选的 title 属性提供完整的描述。
ps: 完整描述样式貌似不能自定义
HTML键盘输入元素(<kbd>)
用于表示用户输入,它将产生一个行内元素,以浏览器的默认monospace字体显示。
HTML标记文本元素(< Mark >)
表示为引用或符号目的而标记或突出显示的文本,这是由于标记的段落在封闭上下文中的相关性或重要性造成的。
ps:项目中大量使用span 标记的做法不符合html5 的语义化
HTML Ruby Base(<rb>)
元素用于分隔<ruby>注释的基本文本组件(即正在注释的文本)。一个<rb>元素应该包装基本文本的每个单独的原子段。
ps: 拼音注解
<samp> 元素
用于标识计算机程序输出,通常使用浏览器缺省的 monotype 字体(例如 Lucida Console)。
HTML 中的<small>
元素將使文本的字体变小一号。(例如从大变成中等,从中等变成小,从小变成超小)。在HTML5中,除了它的样式含义,这个元素被重新定义为表示边注释和附属细则,包括版权和法律文本。
HTML <sub> 元素定义了一个文本区域,出于排版的原因,与主要的文本相比,应该展示得更低并且更小。
ps: 下脚标
HTML <sup> 元素定义了一个文本区域,出于排版的原因,与主要的文本相比,应该展示得更高并且更小。
ps: 上脚标
HTML <u> 元素
使文本在其内容的基线下的一行呈现下划线。在HTML5中, 此元素表示具有未标注的文本跨度,显示渲染,非文本注释,例如将文本标记为中文文本中的专有名称(一个正确的中文标记), 或 将文本标记为拼写错误
HTML <map>
与 <area> 属性一起使用来定义一个图像映射(一个可点击的链接区域).
HTML <track> 元素
被当作媒体元素—<audio> 和 <video>的子元素来使用。它允许指定计时字幕(或者基于时间的数据),例如自动处理字幕。
HTML <object> 元素
(或者称作 HTML 嵌入对象元素)表示引入一个外部资源,这个资源可能是一张图片,一个嵌入的浏览上下文,亦或是一个插件所使用的资源
ps: 支持引入的资源类型
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
关于支持资源类型的测试
http://joliclic.free.fr/html/object-tag/en/index.php
CSS网页布局中,排版是一个麻烦的问题。作为一个优秀的网页设计师和Web前端开发人员,掌握一些简单的中文排版技巧是不可或缺的。所以今天小编特意整理了一些简单实用的技巧,希望对大家有所帮助。
字体
我们可以使用css样式为网页中的文字设置字体、字号、颜色等样式属性。
body{font-family:"宋体";}
这里注意不要设置不常用的字体,因为如果用户本地电脑上如果没有安装你设置的字体,就会显示浏览器默认的字体。(因为用户是否可以看到你设置的字体样式取决于用户本地电脑上是否安装你设置的字体。)
现在一般网页喜欢设置“微软雅黑”,如下代码:
body{font-family:"Microsoft Yahei";}
或
body{font-family:"微软雅黑";}
字号、颜色
可以使用下面代码设置网页中文字的字号为12像素,并把字体颜色设置为#666(灰色):
body{font-size:12px;color:#666}
粗体
可以使用下面代码实现设置文字以粗体样式显示出来。
p span{font-weight:bold;}
斜体
以下代码可以实现文字以斜体样式在浏览器中显示:
p a{font-style:italic;}
<p>三年级时,我还是一个<a>胆小如鼠</a>的小女孩。</p>
下划线
有些情况下想为文字设置为下划线样式,这样可以在视觉上强调文字,可以使用下面代码来实现:
p a{text-decoration:underline;}
<p>三年级时,我还是一个<a>胆小如鼠</a>的小女孩。</p>
删除线
如果想在网页上设置删除线怎么办,这个样式在电商网站上常会见到:
上图中的原价上的删除线使用下面代码就可以实现:
.oldPrice{text-decoration:line-through;}
缩进
中文文字中的段前习惯空两个文字的空白,这个特殊的样式可以用下面代码来实现:
p{text-indent:2em;}
注意:2em的意思就是文字的2倍大小
行间距(行高)
这一小节我们来学习一下另一个在段落排版中起重要作用的行间距(行高)属性(line-height),如下代码实现设置段落行间距为1.5倍。
p{line-height:1.5em;}
中文字间距、字母间距
如果想在网页排版中设置文字间隔或者字母间隔就可以使用 letter-spacing 来实现,如下面代码:
h1{
letter-spacing:50px;
}
注意:这个样式使用在英文单词时,是设置字母与字母之间的间距。
如果我想设置英文单词之间的间距呢?可以使用 word-spacing来实现。如下代码:
h1{
word-spacing:50px;
}
...
<h1>welcome to imooc!</h1>
对齐
想为块状元素中的文本、图片设置居中样式吗?可以使用text-align样式代码,如下代码可实现文本居中显示。
h1{
text-align:center;
}
<h1>了不起的盖茨比</h1>
同样可以设置居左:
h1{
text-align:left;
}
<h1>了不起的盖茨比</h1>
还可以设置居右:
h1{
text-align:right;
}
<h1>了不起的盖茨比</h1>
图文环绕
在css中有一个常见的图文环绕效果。实现方式主要是通过float标签,将图片左浮动,或者右浮动。其相邻的文字,就会环绕图片排列,代码和效果如下:
竖排文字
使用writing-mode实现。writing-mode属性有两个值lr-tb和tb-rl,前者是默认的左-右、上-下,后者是上-下、右-左。
比如:
p{ writing-mode: tb-rl;}
可以结合direction排版。
首字下沉
伪对象:first-letter配合font-size、float可以制作首字下沉效果。
比如:
p:first-letter{ padding: 6px; font-size: 32pt; float: left;}
汉字注音
如果我们想为汉字注音,就可以使用ruby标签和ruby-align属性来实现,比如:
<ruby>注音<rt style="font-size:11px;">zhuyin</rt></ruby>
然后通过ruby-align设置其对齐方式。
这是一个比较冷门的技巧,可能平时使用不多,但小编觉得不妨提供给大家预防不时之需。
以上就是小编要跟大家分享的CSS网页布局中文排版技巧,虽然很简单,但简单的过程中其实暗藏玄机,如果大家喜欢还请记得收藏哦~
*请认真填写需求信息,我们会在24小时内与您取得联系。