简介

榜样很重要。

— 警官亚历克斯·J·墨菲 / 机器战警

本 Ruby 风格指南推荐最佳实践,以便现实世界中的 Ruby 程序员可以编写可由其他现实世界中的 Ruby 程序员维护的代码。反映现实世界用法的风格指南会被使用,而坚持一个被人们拒绝的理想的风格指南则有风险根本不会被使用——无论它有多好。

本指南分为几个相关指南部分。我们尝试添加了指南背后的基本原理(如果省略了,我们假设它很明显)。

我们并非凭空想出所有指南——它们主要基于编辑们的专业经验、来自 Ruby 社区成员的反馈和建议,以及各种备受推崇的 Ruby 编程资源,例如 "Programming Ruby""The Ruby Programming Language"

本风格指南随着时间的推移而发展,因为会识别出额外的约定,而过去的约定会因 Ruby 本身的更改而变得过时。

提示

如果您使用 Rails 或 RSpec,您可能想查看补充的 Ruby on Rails 风格指南RSpec 风格指南

提示
RuboCop 是一个基于本风格指南的静态代码分析器(代码风格检查器)和格式化程序。

指导原则

程序必须为人们编写,而仅仅是偶然地为机器执行。

— 哈罗德·阿贝尔森
计算机程序的结构和解释

众所周知,代码被阅读的次数远多于被编写的次数。这里提供的指南旨在提高代码的可读性,并使其在各种 Ruby 代码中保持一致。它们也旨在反映 Ruby 的现实世界用法,而不是随机的理想。当我们必须在非常成熟的做法和主观上更好的替代方案之间做出选择时,我们选择推荐成熟的做法。[1]

在 Ruby 社区中,某些领域没有明确的共识,例如字符串文字引号、哈希文字中的空格、多行方法链中的点位置等。在这些情况下,所有流行的风格都得到认可,您可以选择一种并始终如一地应用它。

在创建本指南之前,Ruby 已经存在了 15 年以上,该语言的灵活性以及缺乏通用标准导致了几乎所有内容的多种风格。让大家围绕社区标准的理念团结起来需要大量的时间和精力,我们还有很多工作要做。

Ruby 以其对程序员友好的优化而闻名。我们相信本指南将帮助您优化以实现最大的程序员幸福感。

关于一致性的说明

愚蠢的一致性是小人物的恶灵,受到小政治家、哲学家和神学家们的崇拜。

— 拉尔夫·沃尔多·爱默生

风格指南是关于一致性的。与本风格指南保持一致很重要。项目内部的一致性更为重要。一个类或方法内部的一致性是最重要的。

但是,要知道何时不一致——有时风格指南的建议并不适用。如有疑问,请使用您的最佳判断。查看其他示例并决定哪种看起来最好。不要犹豫,随时提问!

特别是:不要为了遵守本指南而破坏向后兼容性!

忽略特定指南的其他一些充分理由

  • 当应用该指南会使代码可读性降低时,即使对于习惯阅读遵循本风格指南的代码的人来说也是如此。

  • 为了与周围也违反该指南的代码保持一致(可能是出于历史原因)——尽管这也是清理他人混乱的机会(以真正的 XP 风格)。

  • 因为所讨论的代码早于该指南的引入,并且没有其他理由修改该代码。

  • 当代码需要与不支持风格指南推荐的功能的旧版 Ruby 保持兼容时。

翻译

本指南的翻译提供以下语言

注意
这些翻译并非由我们的编辑团队维护,因此其质量和完整程度可能会有所不同。翻译后的指南版本通常落后于上游的英文版本。

源代码布局

几乎每个人都认为,除了他们自己的风格之外,其他所有风格都是丑陋且难以阅读的。去掉“除了他们自己的风格之外”,他们可能就对了……​

— 杰里·科芬(关于缩进)

源编码

使用 UTF-8 作为源文件编码。

提示
从 Ruby 2.0 开始,UTF-8 已成为默认的源文件编码。

制表符还是空格?

仅使用空格进行缩进。不要使用硬制表符。

缩进

每个缩进级别使用两个空格(也称为软制表符)。

# bad - four spaces
def some_method
    do_something
end

# good
def some_method
  do_something
end

最大行长

将行限制为 80 个字符。

提示
大多数编辑器和 IDE 都具有帮助您完成此操作的配置选项。它们通常会突出显示超过长度限制的行。
为什么在一个现代宽屏显示器的世界里还要费心使用 80 个字符?

如今,许多人认为 80 个字符的最大行长只是过去遗留下来的东西,在今天意义不大。毕竟,现代显示器可以轻松地在单行上容纳 200 多个字符。但是,坚持使用较短的代码行仍然有一些重要的优势。

首先也是最重要的,大量研究表明,人类垂直阅读的速度要快得多,而过长的文本行会阻碍阅读过程。如前所述,本样式指南的指导原则之一是优化我们为人类消费而编写的代码。

此外,限制所需的编辑器窗口宽度可以使多个文件并排打开,并且在使用将两个版本以相邻列显示的代码审查工具时效果很好。

大多数工具中的默认换行会破坏代码的视觉结构,使其更难理解。选择这些限制是为了避免在窗口宽度设置为 80 的编辑器中换行,即使工具在换行时在最后一列放置一个标记符号。一些基于 Web 的工具可能根本不提供动态换行。

一些团队强烈偏好更长的行长。对于仅由一个团队维护或主要由一个团队维护的代码,如果该团队能够就这个问题达成一致,则可以将行长限制增加到 100 个字符,甚至增加到 120 个字符。请克制住超过 120 个字符的冲动。

禁止尾随空格

避免尾随空格。

提示
大多数编辑器和 IDE 都有配置选项来可视化尾随空格并在保存时自动将其删除。

行尾

使用 Unix 风格的行尾。[2]

提示

如果您使用的是 Git,您可能需要添加以下配置设置来保护您的项目免受 Windows 行尾的侵入

$ git config --global core.autocrlf true

我应该用换行符终止文件吗?

每个文件以换行符结尾。

提示
这应该通过编辑器配置完成,而不是手动完成。

我应该用 ; 终止表达式吗?

不要使用 ; 来终止语句和表达式。

# bad
puts 'foobar'; # superfluous semicolon

# good
puts 'foobar'

每行一个表达式

每行使用一个表达式。

# bad
puts 'foo'; puts 'bar' # two expressions on the same line

# good
puts 'foo'
puts 'bar'

puts 'foo', 'bar' # this applies to puts in particular

运算符方法调用

在运算符方法调用中,避免在不需要的地方使用点。

# bad
num.+ 42

# good
num + 42

空格和运算符

在运算符周围、逗号、冒号和分号之后使用空格。空格可能(大部分)与 Ruby 解释器无关,但正确使用空格是编写易于阅读代码的关键。

# bad
sum=1+2
a,b=1,2
class FooError<StandardError;end

# good
sum = 1 + 2
a, b = 1, 2
class FooError < StandardError; end

有一些例外

  • 指数运算符

# bad
e = M * c ** 2

# good
e = M * c**2
  • 有理数文字中的斜杠

# bad
o_scale = 1 / 48r

# good
o_scale = 1/48r
  • 安全导航运算符

# bad
foo &. bar
foo &.bar
foo&. bar

# good
foo&.bar

安全导航

避免 &. 的链式调用。用 . 和显式检查替换。例如,如果用户保证有地址,并且地址保证有邮政编码

# bad
user&.address&.zip

# good
user && user.address.zip

如果这种更改引入了过多的条件逻辑,请考虑其他方法,例如委托

# bad
user && user.address && user.address.zip

# good
class User
  def zip
    address&.zip
  end
end
user&.zip

空格和大括号

([ 之后没有空格,]) 之前没有空格。在大括号周围和 } 之前使用空格。

# bad
some( arg ).other
[ 1, 2, 3 ].each{|e| puts e}

# good
some(arg).other
[1, 2, 3].each { |e| puts e }

{} 需要一些澄清,因为它们用于块和哈希文字,以及字符串插值。

对于哈希文字,两种风格都被认为是可以接受的。第一个变体更易读(并且可以说在 Ruby 社区中更受欢迎)。第二个变体的优点是增加了块和哈希文字之间的视觉差异。无论您选择哪一个 - 一致地应用它。

# good - space after { and before }
{ one: 1, two: 2 }

# good - no space after { and before }
{one: 1, two: 2}

对于插值表达式,大括号内不应该有填充空格。

# bad
"From: #{ user.first_name }, #{ user.last_name }"

# good
"From: #{user.first_name}, #{user.last_name}"

感叹号后没有空格

! 后面不要加空格。

# bad
! something

# good
!something

范围字面量内不要加空格

范围字面量内不要加空格。

# bad
1 .. 3
'a' ... 'z'

# good
1..3
'a'...'z'

when 缩进到 case 的级别

when 缩进到与 case 相同的级别。

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

# good
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
历史回顾

这是“The Ruby Programming Language”和“Programming Ruby”中确立的风格。从历史上看,它源于 caseswitch 语句不是块,因此不应该缩进,而 whenelse 关键字是标签(在 C 语言中编译时,它们实际上是 JMP 调用的标签)。

缩进条件赋值

将条件表达式的结果赋值给变量时,保留其分支的常规对齐方式。

# bad - pretty convoluted
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

# good - it's apparent what's going on
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

# good (and a bit more width efficient)
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

方法之间空行

在方法定义之间使用空行,以及在方法内部将方法分解成逻辑段落。

# bad
def some_method
  data = initialize(options)
  data.manipulate!
  data.result
end
def some_other_method
  result
end

# good
def some_method
  data = initialize(options)

  data.manipulate!

  data.result
end

def some_other_method
  result
end

两个或多个空行

不要连续使用多个空行。

# bad - It has two empty lines.
some_method


some_method

# good
some_method

some_method

属性访问器周围的空行

在属性访问器周围使用空行。

# bad
class Foo
  attr_reader :foo
  def foo
    # do something...
  end
end

# good
class Foo
  attr_reader :foo

  def foo
    # do something...
  end
end

访问修饰符周围的空行

在访问修饰符周围使用空行。

# bad
class Foo
  def bar; end
  private
  def baz; end
end

# good
class Foo
  def bar; end

  private

  def baz; end
end

主体周围的空行

不要在方法、类、模块、块主体周围使用空行。

# bad
class Foo

  def foo

    begin

      do_something do

        something

      end

    rescue

      something

    end

    true

  end

end

# good
class Foo
  def foo
    begin
      do_something do
        something
      end
    rescue
      something
    end
  end
end

方法参数中的尾随逗号

避免在方法调用中最后一个参数后使用逗号,尤其是在参数不在单独的行上时。

# bad - easier to move/add/remove parameters, but still not preferred
some_method(
  size,
  count,
  color,
)

# bad
some_method(size, count, color, )

# good
some_method(size, count, color)

等号周围的空格

在为方法参数分配默认值时,在 = 运算符周围使用空格

# bad
def some_method(arg1=:default, arg2=nil, arg3=[])
  # do something...
end

# good
def some_method(arg1 = :default, arg2 = nil, arg3 = [])
  # do something...
end

虽然一些 Ruby 书籍建议使用第一种风格,但第二种风格在实践中更为突出(并且可以说更易读)。

表达式中的行延续

避免在不需要的情况下使用\进行行延续。在实践中,避免使用行延续来进行除了字符串连接之外的任何操作。

# bad (\ is not needed here)
result = 1 - \
         2

# bad (\ is required, but still ugly as hell)
result = 1 \
         - 2

# good
result = 1 -
         2

long_string = 'First part of the long string' \
              ' and second part of the long string'

多行方法链

采用一致的多行方法链风格。Ruby 社区中有两种流行的风格,它们都被认为是好的 - 前导.和尾随.

前导.

当在另一行继续链式方法调用时,将.保留在第二行。

# bad - need to consult first line to understand second line
one.two.three.
  four

# good - it's immediately clear what's going on the second line
one.two.three
  .four

尾随.

当在另一行继续链式方法调用时,在第一行包含.以指示表达式继续。

# bad - need to read ahead to the second line to know that the chain continues
one.two.three
  .four

# good - it's immediately clear that the expression continues beyond the first line
one.two.three.
  four

关于两种替代风格优缺点的讨论可以在这里找到。

方法参数对齐

如果方法调用的参数跨越多行,则对齐这些参数。当由于行长限制而无法对齐参数时,对第一行之后的行进行单行缩进也是可以接受的。

# starting point (line is too long)
def send_mail(source)
  Mailer.deliver(to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text)
end

# bad (double indent)
def send_mail(source)
  Mailer.deliver(
      to: '[email protected]',
      from: '[email protected]',
      subject: 'Important message',
      body: source.text)
end

# good
def send_mail(source)
  Mailer.deliver(to: '[email protected]',
                 from: '[email protected]',
                 subject: 'Important message',
                 body: source.text)
end

# good (normal indent)
def send_mail(source)
  Mailer.deliver(
    to: '[email protected]',
    from: '[email protected]',
    subject: 'Important message',
    body: source.text
  )
end

隐式选项哈希

重要
从 Ruby 2.7 开始,选项哈希周围的花括号不再是可选的。

省略隐式选项哈希周围的外部花括号。

# bad
user.set({ name: 'John', age: 45, permissions: { read: true } })

# good
user.set(name: 'John', age: 45, permissions: { read: true })

DSL 方法调用

省略属于内部 DSL(例如 Rake、Rails、RSpec)的方法的外部花括号和圆括号。

class Person < ActiveRecord::Base
  # bad
  attr_reader(:name, :age)
  # good
  attr_reader :name, :age

  # bad
  validates(:name, { presence: true, length: { within: 1..10 } })
  # good
  validates :name, presence: true, length: { within: 1..10 }
end

方法调用中的空格

不要在方法名称和左括号之间放置空格。

# bad
puts (x + y)

# good
puts(x + y)

方括号访问中的空格

不要在接收器名称和左括号之间放置空格。

# bad
collection [index_or_key]

# good
collection[index_or_key]

多行数组对齐

对齐跨越多行的数组字面量的元素。

# bad - single indent
menu_item = %w[Spam Spam Spam Spam Spam Spam Spam Spam
  Baked beans Spam Spam Spam Spam Spam]

# good
menu_item = %w[
  Spam Spam Spam Spam Spam Spam Spam Spam
  Baked beans Spam Spam Spam Spam Spam
]

# good
menu_item =
  %w[Spam Spam Spam Spam Spam Spam Spam Spam
     Baked beans Spam Spam Spam Spam Spam]

命名约定

编程中唯一真正的困难是缓存失效和命名事物。

— Phil Karlton

标识符使用英文

标识符使用英文命名。

# bad - identifier is a Bulgarian word, using non-ascii (Cyrillic) characters
заплата = 1_000

# bad - identifier is a Bulgarian word, written with Latin letters (instead of Cyrillic)
zaplata = 1_000

# good
salary = 1_000

符号、方法和变量使用蛇形命名法

符号、方法和变量使用 snake_case 命名。

# bad
:'some symbol'
:SomeSymbol
:someSymbol

someVar = 5

def someMethod
  # some code
end

def SomeMethod
  # some code
end

# good
:some_symbol

some_var = 5

def some_method
  # some code
end

包含数字后缀的标识符

符号、方法和变量中的数字不要与字母分开。

# bad
:some_sym_1

some_var_1 = 1

var_10 = 10

def some_method_1
  # some code
end

# good
:some_sym1

some_var1 = 1

var10 = 10

def some_method1
  # some code
end

类和模块使用驼峰命名法

注意
CapitalCase 也称为 UpperCamelCaseCapitalWordsPascalCase

类和模块使用 CapitalCase 命名。(保持缩写词如 HTTP、RFC、XML 大写)。

# bad
class Someclass
  # some code
end

class Some_Class
  # some code
end

class SomeXml
  # some code
end

class XmlSomething
  # some code
end

# good
class SomeClass
  # some code
end

class SomeXML
  # some code
end

class XMLSomething
  # some code
end

文件使用蛇形命名法

文件命名使用 snake_case,例如 hello_world.rb

目录使用蛇形命名法

目录命名使用 snake_case,例如 lib/hello_world/hello_world.rb

每个文件一个类

每个源文件只包含一个类/模块。文件命名与类/模块名称一致,将 CapitalCase 替换为 snake_case

常量使用大写蛇形命名法

其他常量(不包括类和模块)使用 SCREAMING_SNAKE_CASE 命名。

# bad
SomeConst = 5

# good
SOME_CONST = 5

谓词方法后缀

谓词方法(返回布尔值的函数)的名称应该以问号结尾(例如 Array#empty?)。不返回布尔值的函数不应该以问号结尾。

# bad
def even(value)
end

# good
def even?(value)
end

谓词方法前缀

避免在谓词方法中使用辅助动词,例如 isdoescan。这些词是多余的,并且与 Ruby 核心库中布尔方法的风格不一致,例如 empty?include?

# bad
class Person
  def is_tall?
    true
  end

  def can_play_basketball?
    false
  end

  def does_like_candy?
    true
  end
end

# good
class Person
  def tall?
    true
  end

  def basketball_player?
    false
  end

  def likes_candy?
    true
  end
end

危险方法后缀

如果存在安全版本的危险方法,则潜在危险方法(例如修改self或参数的方法、exit!(不像exit那样运行最终化器)等)的名称应以感叹号结尾。

# bad - there is no matching 'safe' method
class Person
  def update!
  end
end

# good
class Person
  def update
  end
end

# good
class Person
  def update!
  end

  def update
  end
end

安全方法与危险方法之间的关系

如果可能,请根据带感叹号(危险)方法定义不带感叹号(安全)方法。

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

未使用的变量前缀

使用_作为未使用的块参数和局部变量的前缀。也可以只使用_(尽管它不太描述性)。此约定被 Ruby 解释器和 RuboCop 等工具识别,它们会抑制未使用的变量警告。

# bad
result = hash.map { |k, v| v + 1 }

def something(x)
  unused_var, used_var = something_else(x)
  # some code
end

# good
result = hash.map { |_k, v| v + 1 }

def something(x)
  _unused_var, used_var = something_else(x)
  # some code
end

# good
result = hash.map { |_, v| v + 1 }

def something(x)
  _, used_var = something_else(x)
  # some code
end

other参数

在定义二元运算符和类似运算符的方法时,对于操作数具有“对称”语义的运算符,将参数命名为other。对称语义意味着运算符的两侧通常是相同类型或可强制转换类型。

具有对称语义的运算符和类似运算符方法(参数应命名为other):`, `-`, `+, /, %, *, ==, >, <, |, &, ^, eql?, equal?

具有非对称语义的运算符(参数不应命名为other):<<, [](操作数之间的集合/项目关系),===(模式/可匹配关系)。

请注意,只有当运算符的两侧具有相同的语义时,才应遵循此规则。Ruby 核心中的一个突出例外是,例如,Array#*(int)

# good
def +(other)
  # body omitted
end

# bad
def <<(other)
  @internal << other
end

# good
def <<(item)
  @internal << item
end

# bad
# Returns some string multiplied `other` times
def *(other)
  # body omitted
end

# good
# Returns some string multiplied `num` times
def *(num)
  # body omitted
end

控制流

for循环

不要使用for,除非你确切地知道为什么。大多数情况下,应该使用迭代器。for是用each实现的(所以你添加了一层间接性),但有一个区别 - for不会引入新的作用域(不像each),并且在其块中定义的变量将在其外部可见。

arr = [1, 2, 3]

# bad
for elem in arr do
  puts elem
end

# note that elem is accessible outside of the for loop
elem # => 3

# good
arr.each { |elem| puts elem }

# elem is not accessible outside each block
elem # => NameError: undefined local variable or method `elem'

多行表达式中的then

不要在多行if/unless/when/in中使用then

# bad
if some_condition then
  # body omitted
end

# bad
case foo
when bar then
  # body omitted
end

# bad
case expression
in pattern then
  # body omitted
end

# good
if some_condition
  # body omitted
end

# good
case foo
when bar
  # body omitted
end

# good
case expression
in pattern
  # body omitted
end

条件放置

在多行条件语句中,始终将条件放在与if/unless相同的行上。

# bad
if
  some_condition
  do_something
  do_something_else
end

# good
if some_condition
  do_something
  do_something_else
end

三元运算符与if

优先使用三元运算符 (?:) 而不是 if/then/else/end 结构。它更常见,显然也更简洁。

# bad
result = if some_condition then something else something_else end

# good
result = some_condition ? something : something_else

嵌套的三元运算符

在三元运算符中,每个分支使用一个表达式。这也意味着三元运算符不能嵌套。在这种情况下,优先使用 if/else 结构。

# bad
some_condition ? (nested_condition ? nested_something : nested_something_else) : something_else

# good
if some_condition
  nested_condition ? nested_something : nested_something_else
else
  something_else
end

if 中的分号

不要使用 if x; …​。使用三元运算符代替。

# bad
result = if some_condition; something else something_else end

# good
result = some_condition ? something : something_else

case vs if-else

当比较值在每个子句中都相同时,优先使用 case 而不是 if-elsif

# bad
if status == :active
  perform_action
elsif status == :inactive || status == :hibernating
  check_timeout
else
  final_action
end

# good
case status
when :active
  perform_action
when :inactive, :hibernating
  check_timeout
else
  final_action
end

if/case 返回结果

利用 ifcase 是表达式这一事实,它们会返回一个结果。

# bad
if condition
  result = x
else
  result = y
end

# good
result =
  if condition
    x
  else
    y
  end

单行案例

对于单行案例,使用 when x then …​

注意
从 Ruby 1.9 开始,备用语法 when x: …​ 已被移除。

when 中的分号

不要使用 when x; …​。请参阅上一条规则。

in 中的分号

不要使用 in pattern; …​。对于单行 in 模式分支,使用 in pattern then …​

# bad
case expression
in pattern; do_something
end

# good
case expression
in pattern then do_something
end

! vs not

使用 ! 而不是 not

# bad - parentheses are required because of op precedence
x = (not something)

# good
x = !something

双重否定

避免不必要的 !! 使用。

!! 将值转换为布尔值,但在控制表达式的条件中,您不需要这种显式转换;使用它只会掩盖您的意图。

仅当有充分理由限制结果为 truefalse 时,才考虑使用它。例如,输出到特定格式或 API(如 JSON),或作为 predicate? 方法的返回值。在这些情况下,也考虑进行空值检查:!something.nil?

# bad
x = 'test'
# obscure nil check
if !!x
  # body omitted
end

# good
x = 'test'
if x
  # body omitted
end

# good
def named?
  !name.nil?
end

# good
def banned?
  !!banned_until&.future?
end

and/or

在布尔上下文中不要使用 andor - andor 是控制流运算符,应该按此使用。它们的优先级非常低,可以用作指定流程序列的简写形式,例如“评估表达式 1,并且只有当它不成功(返回 nil)时,才评估表达式 2”。这对于在不破坏阅读流程的情况下引发错误或提前返回特别有用。

# good: and/or for control flow
x = extract_arguments or raise ArgumentError, "Not enough arguments!"
user.suspended? and return :denied

# bad
# and/or in conditions (their precedence is low, might produce unexpected result)
if got_needed_arguments and arguments_valid
  # ...body omitted
end
# in logical expression calculation
ok = got_needed_arguments and arguments_valid

# good
# &&/|| in conditions
if got_needed_arguments && arguments_valid
  # ...body omitted
end
# in logical expression calculation
ok = got_needed_arguments && arguments_valid

# bad
# &&/|| for control flow (can lead to very surprising results)
x = extract_arguments || raise(ArgumentError, "Not enough arguments!")

避免在一个表达式中使用多个控制流运算符,因为这很快就会变得令人困惑。

# bad
# Did author mean conditional return because `#log` could result in `nil`?
# ...or was it just to have a smart one-liner?
x = extract_arguments and log("extracted") and return

# good
# If the intention was conditional return
x = extract_arguments
if x
  return if log("extracted")
end
# If the intention was just "log, then return"
x = extract_arguments
if x
  log("extracted")
  return
end
注意
在社区中,使用andor来组织控制流是否是一个好主意一直是一个有争议的话题。但如果你确实要使用它们,请优先使用这些运算符而不是&&/||。因为不同的运算符具有不同的语义,这使得更容易判断你是在处理逻辑表达式(将被简化为布尔值)还是控制流。
为什么使用andor作为逻辑运算符是一个坏主意?

简单来说,因为它们会增加一些认知负担,因为它们的行为不像其他语言中类似命名的逻辑运算符。

首先,andor运算符的优先级低于=运算符,而&&||运算符的优先级高于=运算符,这是基于运算符优先级的顺序。

foo = true and false # results in foo being equal to true. Equivalent to (foo = true) and false
bar = false or true  # results in bar being equal to false. Equivalent to (bar = false) or true

另外,&&的优先级高于||,而andor的优先级相同。有趣的是,尽管andor的灵感来自Perl,但它们在Perl中并没有不同的优先级。

true or true and false # => false (it's effectively (true or true) and false)
true || true && false # => true (it's effectively true || (true && false)
false or true and false # => false (it's effectively (false or true) and false)
false || true && false # => false (it's effectively false || (true && false))

多行三元运算符

避免使用多行?:(三元运算符);使用if/unless代替。

if作为修饰符

当你有单行主体时,优先使用修饰符if/unless。另一个好的选择是使用控制流and/or

# bad
if some_condition
  do_something
end

# good
do_something if some_condition

# another good option
some_condition and do_something

多行if修饰符

避免在非平凡的多行块末尾使用修饰符if/unless

# bad
10.times do
  # multi-line body omitted
end if some_condition

# good
if some_condition
  10.times do
    # multi-line body omitted
  end
end

嵌套修饰符

避免嵌套修饰符if/unless/while/until的使用。如果合适,优先使用&&/||

# bad
do_something if other_condition if some_condition

# good
do_something if some_condition && other_condition

if vs unless

对于否定条件(或控制流||),优先使用unless而不是if

# bad
do_something if !some_condition

# bad
do_something if not some_condition

# good
do_something unless some_condition

# another good option
some_condition || do_something

使用elseunless

不要将unlesselse一起使用。将这些重写为正向情况优先。

# bad
unless success?
  puts 'failure'
else
  puts 'success'
end

# good
if success?
  puts 'success'
else
  puts 'failure'
end

条件周围的括号

不要在控制表达式的条件周围使用括号。

# bad
if (x > 10)
  # body omitted
end

# good
if x > 10
  # body omitted
end
注意
此规则有一个例外,即条件中的安全赋值

多行 while do

不要将 while/until condition do 用于多行 while/until

# bad
while x > 5 do
  # body omitted
end

until x > 5 do
  # body omitted
end

# good
while x > 5
  # body omitted
end

until x > 5
  # body omitted
end

while 作为修饰符

当您有一个单行主体时,请优先使用修饰符 while/until

# bad
while some_condition
  do_something
end

# good
do_something while some_condition

whileuntil

对于负条件,请优先使用 until 而不是 while

# bad
do_something while !some_condition

# good
do_something until some_condition

无限循环

当您需要无限循环时,请使用 Kernel#loop 而不是 while/until

# bad
while true
  do_something
end

until false
  do_something
end

# good
loop do
  do_something
end

breakloop

对于循环后测试,请使用带 breakKernel#loop 而不是 begin/end/untilbegin/end/while

# bad
begin
  puts val
  val += 1
end while val < 0

# good
loop do
  puts val
  val += 1
  break unless val < 0
end

显式 return

在控制流不需要的情况下,避免使用 return

# bad
def some_method(some_arr)
  return some_arr.size
end

# good
def some_method(some_arr)
  some_arr.size
end

显式 self

在不需要的情况下,避免使用 self。(仅在调用 self 写访问器、以保留字命名的方法或可重载运算符时才需要。)

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

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

  # ok
  def initialize(options)
    self.options = options
    # both options and self.options are equivalent here
  end

  # bad
  def do_something(options = {})
    unless options[:when] == :later
      output(self.options[:message])
    end
  end

  # good
  def do_something(params = {})
    unless params[:when] == :later
      output(options[:message])
    end
  end
end

条件中的安全赋值

不要在条件表达式中使用 =(赋值)的返回值,除非赋值被括号包围。这在 Ruby 程序员中是一个相当流行的习惯用法,有时被称为条件中的安全赋值

# bad (+ a warning)
if v = array.grep(/foo/)
  do_something(v)
  # some code
end

# good (MRI would still complain, but RuboCop won't)
if (v = array.grep(/foo/))
  do_something(v)
  # some code
end

# good
v = array.grep(/foo/)
if v
  do_something(v)
  # some code
end

BEGIN

避免使用 BEGIN 块。

END

不要使用END块。请改用Kernel#at_exit

# bad
END { puts 'Goodbye!' }

# good
at_exit { puts 'Goodbye!' }

嵌套条件语句

避免使用嵌套条件语句来控制流程。

当可以断言数据无效时,请优先使用保护子句。保护子句是在函数开头的一个条件语句,它可以尽快地退出函数。

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

# good
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,而不是条件块。

# bad
[0, 1, 2, 3].each do |item|
  if item > 1
    puts item
  end
end

# good
[0, 1, 2, 3].each do |item|
  next unless item > 1
  puts item
end

异常

raise vs fail

对于异常,请优先使用raise而不是fail

# bad
fail SomeException, 'message'

# good
raise SomeException, 'message'

显式抛出RuntimeError

raise的两个参数版本中,不要显式指定RuntimeError

# bad
raise RuntimeError, 'message'

# good - signals a RuntimeError by default
raise 'message'

异常类消息

请优先向raise提供异常类和消息作为两个独立的参数,而不是异常实例。

# bad
raise SomeException.new('message')
# Note that there is no way to do `raise SomeException.new('message'), backtrace`.

# good
raise SomeException, 'message'
# Consistent with `raise SomeException, 'message', backtrace`.

ensure中的return

不要从ensure块中返回。如果在ensure块中显式地从方法中返回,则返回将优先于任何抛出的异常,方法将返回,就好像没有抛出任何异常一样。实际上,异常将被静默地抛弃。

# bad
def foo
  raise
ensure
  return 'very bad idea'
end

隐式begin

尽可能使用隐式begin块

# bad
def foo
  begin
    # main logic goes here
  rescue
    # failure handling goes here
  end
end

# good
def foo
  # main logic goes here
rescue
  # failure handling goes here
end

应急方法

通过使用应急方法(由Avdi Grimm创造的术语)来减少begin块的激增。

# bad
begin
  something_that_might_fail
rescue IOError
  # handle IOError
end

begin
  something_else_that_might_fail
rescue IOError
  # handle IOError
end

# good
def with_io_error_handling
  yield
rescue IOError
  # handle IOError
end

with_io_error_handling { something_that_might_fail }

with_io_error_handling { something_else_that_might_fail }

抑制异常

不要抑制异常。

# bad
begin
  do_something # an exception occurs here
rescue SomeError
end

# good
begin
  do_something # an exception occurs here
rescue SomeError
  handle_exception
end

# good
begin
  do_something # an exception occurs here
rescue SomeError
  # Notes on why exception handling is not performed
end

# good
do_something rescue nil

使用rescue作为修饰符

避免使用rescue的修饰符形式。

# bad - this catches exceptions of StandardError class and its descendant classes
read_file rescue handle_error($!)

# good - this catches only the exceptions of Errno::ENOENT class and its descendant classes
def foo
  read_file
rescue Errno::ENOENT => e
  handle_error(e)
end

使用异常来控制流程

不要使用异常来控制流程。

# bad
begin
  n / d
rescue ZeroDivisionError
  puts 'Cannot divide by 0!'
end

# good
if d.zero?
  puts 'Cannot divide by 0!'
else
  n / d
end

盲目救援

避免救援Exception 类。这将捕获信号和对exit 的调用,需要您kill -9 进程。

# bad
begin
  # calls to exit and kill signals will be caught (except kill -9)
  exit
rescue Exception
  puts "you didn't really want to exit, right?"
  # exception handling
end

# good
begin
  # a blind rescue rescues from StandardError, not Exception as many
  # programmers assume.
rescue => e
  # exception handling
end

# also good
begin
  # an exception occurs here
rescue StandardError => e
  # exception handling
end

异常救援顺序

将更具体的异常放在救援链的更高位置,否则它们将永远不会被救援。

# bad
begin
  # some code
rescue StandardError => e
  # some handling
rescue IOError => e
  # some handling that will never be executed
end

# good
begin
  # some code
rescue IOError => e
  # some handling
rescue StandardError => e
  # some handling
end

从文件读取

当仅在单个操作中从头到尾读取文件时,使用便利方法File.readFile.binread

## text mode
# bad (only when reading from beginning to end - modes: 'r', 'rt', 'r+', 'r+t')
File.open(filename).read
File.open(filename, &:read)
File.open(filename) { |f| f.read }
File.open(filename) do |f|
  f.read
end
File.open(filename, 'r').read
File.open(filename, 'r', &:read)
File.open(filename, 'r') { |f| f.read }
File.open(filename, 'r') do |f|
  f.read
end

# good
File.read(filename)

## binary mode
# bad (only when reading from beginning to end - modes: 'rb', 'r+b')
File.open(filename, 'rb').read
File.open(filename, 'rb', &:read)
File.open(filename, 'rb') { |f| f.read }
File.open(filename, 'rb') do |f|
  f.read
end

# good
File.binread(filename)

写入文件

当仅打开文件以在单个操作中创建/替换其内容时,使用便利方法File.writeFile.binwrite

## text mode
# bad (only truncating modes: 'w', 'wt', 'w+', 'w+t')
File.open(filename, 'w').write(content)
File.open(filename, 'w') { |f| f.write(content) }
File.open(filename, 'w') do |f|
  f.write(content)
end

# good
File.write(filename, content)

## binary mode
# bad (only truncating modes: 'wb', 'w+b')
File.open(filename, 'wb').write(content)
File.open(filename, 'wb') { |f| f.write(content) }
File.open(filename, 'wb') do |f|
  f.write(content)
end

# good
File.binwrite(filename, content)

释放外部资源

ensure 块中释放程序获得的外部资源。

f = File.open('testfile')
begin
  # .. process
rescue
  # .. handle error
ensure
  f.close if f
end

自动释放外部资源

尽可能使用资源获取方法的版本,这些版本在可能的情况下进行自动资源清理。

# bad - you need to close the file descriptor explicitly
f = File.open('testfile')
# some action on the file
f.close

# good - the file descriptor is closed automatically
File.open('testfile') do |f|
  # some action on the file
end

原子文件操作

在确认文件存在检查后进行文件操作时,频繁的并行文件操作可能会导致难以重现的问题。因此,最好使用原子文件操作。

# bad - race condition with another process may result in an error in `mkdir`
unless Dir.exist?(path)
  FileUtils.mkdir(path)
end

# good - atomic and idempotent creation
FileUtils.mkdir_p(path)

# bad - race condition with another process may result in an error in `remove`
if File.exist?(path)
  FileUtils.remove(path)
end

# good - atomic and idempotent removal
FileUtils.rm_f(path)

标准异常

优先使用标准库中的异常,而不是引入新的异常类。

赋值与比较

并行赋值

避免使用并行赋值来定义变量。并行赋值在它是方法调用(例如Hash#values_at)的返回值时、与 splat 运算符一起使用时或用于交换变量赋值时是允许的。并行赋值不如单独赋值易读。

# bad
a, b, c, d = 'foo', 'bar', 'baz', 'foobar'

# good
a = 'foo'
b = 'bar'
c = 'baz'
d = 'foobar'

# good - swapping variable assignment
# Swapping variable assignment is a special case because it will allow you to
# swap the values that are assigned to each variable.
a = 'foo'
b = 'bar'

a, b = b, a
puts a # => 'bar'
puts b # => 'foo'

# good - method return
def multi_return
  [1, 2]
end

first, second = multi_return

# good - use with splat
first, *list = [1, 2, 3, 4] # first => 1, list => [2, 3, 4]

hello_array = *'Hello' # => ["Hello"]

a = *(1..3) # => [1, 2, 3]

值交换

交换 2 个值时使用并行赋值。

# bad
tmp = x
x = y
y = tmp

# good
x, y = y, x

处理解构赋值中的尾部下划线变量

在并行赋值期间,避免使用不必要的尾部下划线变量。命名下划线变量比下划线变量更可取,因为它们提供了上下文。当赋值左侧定义了 splat 变量且 splat 变量不是下划线时,需要使用尾部下划线变量。

# bad
foo = 'one,two,three,four,five'
# Unnecessary assignment that does not provide useful information
first, second, _ = foo.split(',')
first, _, _ = foo.split(',')
first, *_ = foo.split(',')

# good
foo = 'one,two,three,four,five'
# The underscores are needed to show that you want all elements
# except for the last number of underscore elements
*beginning, _ = foo.split(',')
*beginning, something, _ = foo.split(',')

a, = foo.split(',')
a, b, = foo.split(',')
# Unnecessary assignment to an unused variable, but the assignment
# provides us with useful information.
first, _second = foo.split(',')
first, _second, = foo.split(',')
first, *_ending = foo.split(',')

自赋值

尽可能使用简写自赋值运算符。

# bad
x = x + y
x = x * y
x = x**y
x = x / y
x = x || y
x = x && y

# good
x += y
x *= y
x **= y
x /= y
x ||= y
x &&= y

条件变量初始化简写

使用 ||= 仅在变量未初始化时初始化它们。

# bad
name = name ? name : 'Bozhidar'

# bad
name = 'Bozhidar' unless name

# good - set name to 'Bozhidar', only if it's nil or false
name ||= 'Bozhidar'
警告

不要使用 ||= 初始化布尔变量。(考虑如果当前值恰好是 false 会发生什么。)

# bad - would set enabled to true even if it was false
enabled ||= true

# good
enabled = true if enabled.nil?

存在性检查简写

使用 &&= 预处理可能存在也可能不存在的变量。使用 &&= 仅在变量存在时才会更改其值,从而无需使用 if 检查其存在性。

# bad
if something
  something = something.downcase
end

# bad
something = something ? something.downcase : nil

# ok
something = something.downcase if something

# good
something = something && something.downcase

# better
something &&= something.downcase

身份比较

在比较 object_id 时,优先使用 equal? 而不是 ==Object#equal? 用于比较对象的标识,而 Object#== 用于进行值比较。

# bad
foo.object_id == bar.object_id

# good
foo.equal?(bar)

类似地,优先使用 Hash#compare_by_identity 而不是使用 object_id 作为键。

# bad
hash = {}
hash[foo.object_id] = :bar
if hash.key?(baz.object_id) # ...

# good
hash = {}.compare_by_identity
hash[foo] = :bar
if hash.key?(baz) # ...

请注意,Set 也提供了 Set#compare_by_identity

显式使用大小写相等运算符

避免显式使用大小写相等运算符 ===。顾名思义,它旨在被 case 表达式隐式使用,在 case 表达式之外使用它会导致一些非常混乱的代码。

# bad
Array === something
(1..100) === 7
/something/ === some_string

# good
something.is_a?(Array)
(1..100).include?(7)
some_string.match?(/something/)
注意
对于 BasicObject 的直接子类,使用 is_a? 不是一种选择,因为 BasicObject 不提供该方法(它是在 Object 中定义的)。在这些罕见的情况下,可以使用 ===

is_a?kind_of?

优先使用 is_a? 而不是 kind_of?。这两个方法是同义词,但 is_a? 是在实际应用中更常用的名称。

# bad
something.kind_of?(Array)

# good
something.is_a?(Array)

is_a?instance_of?

优先使用 is_a? 而不是 instance_of?

虽然这两种方法很相似,但is_a?会考虑整个继承链(超类和包含的模块),这通常是您想要做的事情。另一方面,instance_of?仅当对象是您正在检查的特定类的实例(而不是子类)时才返回true

# bad
something.instance_of?(Array)

# good
something.is_a?(Array)

instance_of? 与类比较

使用Object#instance_of?代替类比较来判断相等性。

# bad
var.class == Date
var.class.equal?(Date)
var.class.eql?(Date)
var.class.name == 'Date'

# good
var.instance_of?(Date)

==eql?

==可以满足需求时,不要使用eql?eql?提供的更严格的比较语义在实践中很少需要。

# bad - eql? is the same as == for strings
'ruby'.eql? some_str

# good
'ruby' == some_str
1.0.eql? x # eql? makes sense here if want to differentiate between Integer and Float 1

块、Proc 和 Lambda

Proc 调用简写

当调用的方法是块的唯一操作时,使用 Proc 调用简写。

# bad
names.map { |name| name.upcase }

# good
names.map(&:upcase)

单行块分隔符

对于单行块,优先使用{…​}而不是do…​end。避免在多行块中使用{…​}(多行链式调用总是很丑陋)。对于“控制流”和“方法定义”(例如在 Rakefiles 和某些 DSL 中),始终使用do…​end。在链式调用时避免使用do…​end

names = %w[Bozhidar Filipp Sarah]

# bad
names.each do |name|
  puts name
end

# good
names.each { |name| puts name }

# bad
names.select do |name|
  name.start_with?('S')
end.map { |name| name.upcase }

# good
names.select { |name| name.start_with?('S') }.map(&:upcase)

有些人会争论说,使用{…​}的多行链式调用看起来还可以,但他们应该问问自己 - 这段代码真的可读吗?块的内容可以提取到简洁的方法中吗?

单行do…​end

使用多行do…​end块而不是单行do…​end块。

# bad
foo do |arg| bar(arg) end

# good
foo do |arg|
  bar(arg)
end

# bad
->(arg) do bar(arg) end

# good
->(arg) { bar(arg) }

显式块参数

考虑使用显式块参数来避免编写只将参数传递给另一个块的块字面量。

require 'tempfile'

# bad
def with_tmp_dir
  Dir.mktmpdir do |tmp_dir|
    Dir.chdir(tmp_dir) { |dir| yield dir }  # block just passes arguments
  end
end

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

块参数中的尾部逗号

避免在块参数的最后一个参数后使用逗号,除非只有一个参数存在,并且删除它会影响功能(例如,数组解构)。

# bad - easier to move/add/remove parameters, but still not preferred
[[1, 2, 3], [4, 5, 6]].each do |a, b, c,|
  a + b + c
end

# good
[[1, 2, 3], [4, 5, 6]].each do |a, b, c|
  a + b + c
end

# bad
[[1, 2, 3], [4, 5, 6]].each { |a, b, c,| a + b + c }

# good
[[1, 2, 3], [4, 5, 6]].each { |a, b, c| a + b + c }

# good - this comma is meaningful for array destructuring
[[1, 2, 3], [4, 5, 6]].map { |a,| a }

嵌套方法定义

不要使用嵌套方法定义,使用 lambda 代替。嵌套方法定义实际上会在与外部方法相同的范围内(例如类)生成方法。此外,每次调用包含其定义的方法时,都会重新定义“嵌套方法”。

# bad
def foo(x)
  def bar(y)
    # body omitted
  end

  bar(x)
end

# good - the same as the previous, but no bar redefinition on every foo call
def bar(y)
  # body omitted
end

def foo(x)
  bar(x)
end

# also good
def foo(x)
  bar = ->(y) { ... }
  bar.call(x)
end

多行 Lambda 定义

对于单行代码块,使用新的 lambda 字面量语法。对于多行代码块,使用 lambda 方法。

# bad
l = lambda { |a, b| a + b }
l.call(1, 2)

# correct, but looks extremely awkward
l = ->(a, b) do
  tmp = a * 7
  tmp * b / 50
end

# good
l = ->(a, b) { a + b }
l.call(1, 2)

l = lambda do |a, b|
  tmp = a * 7
  tmp * b / 50
end

带参数的 Stabby Lambda 定义

定义带参数的 stabby lambda 时,不要省略参数括号。

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

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

无参数的 Stabby Lambda 定义

定义无参数的 stabby lambda 时,省略参数括号。

# bad
l = ->() { something }

# good
l = -> { something }

proc vs Proc.new

优先使用 proc 而不是 Proc.new

# bad
p = Proc.new { |n| puts n }

# good
p = proc { |n| puts n }

Proc 调用

对于 lambda 和 proc,优先使用 proc.call() 而不是 proc[]proc.()

# bad - looks similar to Enumeration access
l = ->(v) { puts v }
l[1]

# bad - most compact form, but might be confusing for newcomers to Ruby
l = ->(v) { puts v }
l.(1)

# good - a bit verbose, but crystal clear
l = ->(v) { puts v }
l.call(1)

方法

简短方法

避免方法长度超过 10 行代码 (LOC)。理想情况下,大多数方法的长度应小于 5 行代码。空行不计入相关 LOC。

顶层方法

避免顶层方法定义。将它们组织到模块、类或结构体中。

注意
在脚本中使用顶层方法定义是可以的。
# bad
def some_method; end

# good
class SomeClass
  def some_method; end
end

无单行方法

避免使用单行方法。虽然它们在现实中比较流行,但它们定义语法的几个特殊之处使得它们的应用不可取。无论如何,单行方法中不应该超过一个表达式。

注意
Ruby 3 引入了单行方法定义的替代语法,将在本指南的下一节中讨论。
# bad
def too_much; something; something_else; end

# okish - notice that the first ; is required
def no_braces_method; body end

# okish - notice that the second ; is optional
def no_braces_method; body; end

# okish - valid syntax, but no ; makes it kind of hard to read
def some_method() body end

# good
def some_method
  body
end

规则的一个例外是空主体方法。

# good
def no_op; end

无限方法

仅对具有单行代码体的 Ruby 3.0 的无限方法定义使用。理想情况下,此类方法定义应该既简单(单个表达式)又没有副作用。

注意
重要的是要理解,本指南并不与上一条指南相矛盾。我们仍然建议不要使用单行方法定义,但如果要使用此类方法,则优先使用无限方法。
# bad
def fib(x) = if x < 2
  x
else
  fib(x - 1) + fib(x - 2)
end

# good
def the_answer = 42
def get_x = @x
def square(x) = x * x

# Not (so) good: has side effect
def set_x(x) = (@x = x)
def print_foo = puts("foo")

双冒号

仅使用 :: 来引用常量(包括类和模块)和构造函数(如 Array()Nokogiri::HTML())。不要将 :: 用于常规方法调用。

# bad
SomeClass::some_method
some_object::some_method

# good
SomeClass.some_method
some_object.some_method
SomeModule::SomeClass::SOME_CONST
SomeModule::SomeClass()

冒号方法定义

不要使用 :: 来定义类方法。

# bad
class Foo
  def self::some_method
  end
end

# good
class Foo
  def self.some_method
  end
end

方法定义括号

当有参数时,使用带括号的 def。当方法不接受任何参数时,省略括号。

# bad
def some_method()
  # body omitted
end

# good
def some_method
  # body omitted
end

# bad
def some_method_with_parameters param1, param2
  # body omitted
end

# good
def some_method_with_parameters(param1, param2)
  # body omitted
end

方法调用括号

在方法调用参数周围使用括号,尤其是在第一个参数以左括号 ( 开头时,例如 f((3 + 2) + 1)

# bad
x = Math.sin y
# good
x = Math.sin(y)

# bad
array.delete e
# good
array.delete(e)

# bad
temperance = Person.new 'Temperance', 30
# good
temperance = Person.new('Temperance', 30)

无参数方法调用

对于没有参数的方法调用,始终省略括号。

# bad
Kernel.exit!()
2.even?()
fork()
'test'.upcase()

# good
Kernel.exit!
2.even?
fork
'test'.upcase

在 Ruby 中具有“关键字”状态的方法

对于在 Ruby 中具有“关键字”状态的方法,始终省略括号。

注意
不幸的是,并不完全清楚哪些方法具有“关键字”状态。人们普遍认为声明性方法具有“关键字”状态。然而,对于哪些非声明性方法(如果有)具有“关键字”状态,人们意见不一。
在 Ruby 中具有“关键字”状态的非声明性方法

对于具有“关键字”状态的非声明性方法(例如,各种 Kernel 实例方法),两种风格都被认为是可以接受的。最流行的风格是省略括号。理由:代码更易读,方法调用看起来更像关键字。另一种不太流行但仍然可以接受的风格是包含括号。理由:这些方法具有普通语义,那么为什么要区别对待它们呢?而且,通过不担心哪些方法具有“关键字”状态,更容易实现统一的风格。无论你选择哪一种,都要始终如一地应用它。

# good (most popular)
puts temperance.age
system 'ls'
exit 1

# also good (less popular)
puts(temperance.age)
system('ls')
exit(1)

使用带参数的 super

在使用带参数的 super 时,始终使用括号。

# bad
super name, age

# good
super(name, age)
重要
在不带参数的情况下调用 super 时,supersuper() 的含义不同。请根据您的使用情况选择合适的选项。

参数过多

避免参数列表超过三个或四个参数。

可选参数

将可选参数定义在参数列表的末尾。在 Ruby 中,在参数列表开头定义可选参数会导致一些意想不到的结果。

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

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

关键字参数顺序

将必需的关键字参数放在可选关键字参数之前。否则,如果可选关键字参数隐藏在中间,就很难发现它们。

# bad
def some_method(foo: false, bar:, baz: 10)
  # body omitted
end

# good
def some_method(bar:, foo: false, baz: 10)
  # body omitted
end

布尔型关键字参数

在将布尔型参数传递给方法时,使用关键字参数。

# bad
def some_method(bar = false)
  puts bar
end

# bad - common hack before keyword args were introduced
def some_method(options = {})
  bar = options.fetch(:bar, false)
  puts bar
end

# good
def some_method(bar: false)
  puts bar
end

some_method            # => false
some_method(bar: true) # => true

关键字参数与可选参数

优先使用关键字参数而不是可选参数。

# bad
def some_method(a, b = 5, c = 1)
  # body omitted
end

# good
def some_method(a, b: 5, c: 1)
  # body omitted
end

关键字参数与选项哈希

使用关键字参数代替选项哈希。

# bad
def some_method(options = {})
  bar = options.fetch(:bar, false)
  puts bar
end

# good
def some_method(bar: false)
  puts bar
end

参数转发

使用 Ruby 2.7 的参数转发功能。

# bad
def some_method(*args, &block)
  other_method(*args, &block)
end

# bad
def some_method(*args, **kwargs, &block)
  other_method(*args, **kwargs, &block)
end

# bad
# Please note that it can cause unexpected incompatible behavior
# because `...` forwards block also.
# https://github.com/rubocop/rubocop/issues/7549
def some_method(*args)
  other_method(*args)
end

# good
def some_method(...)
  other_method(...)
end

块转发

使用 Ruby 3.1 的匿名块转发功能。

在大多数情况下,块参数的名称类似于 &block&proc。它们的名称没有信息,而 & 对于语法意义来说已经足够了。

# bad
def some_method(&block)
  other_method(&block)
end

# good
def some_method(&)
  other_method(&)
end

私有全局方法

如果您确实需要“全局”方法,请将它们添加到 Kernel 中并将其设为私有。

类与模块

一致的类

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

class Person
  # extend/include/prepend go first
  extend SomeModule
  include AnotherModule
  prepend YetAnotherModule

  # inner classes
  class CustomError < StandardError
  end

  # constants are next
  SOME_CONSTANT = 20

  # afterwards we have attribute macros
  attr_reader :name

  # followed by other macros (if any)
  validates :name

  # public class methods are next in line
  def self.some_method
  end

  # initialization goes between class methods and other instance methods
  def initialize
  end

  # followed by other public instance methods
  def some_method
  end

  # protected and private methods are grouped near the end
  protected

  def some_protected_method
  end

  private

  def some_private_method
  end
end

Mixin 分组

将多个 Mixin 分成单独的语句。

# bad
class Person
  include Foo, Bar
end

# good
class Person
  # multiple mixins go in separate statements
  include Foo
  include Bar
end

单行类

对于没有主体类的类定义,建议使用两行格式。这样更容易阅读、理解和修改。

# bad
FooError = Class.new(StandardError)

# okish
class FooError < StandardError; end

# ok
class FooError < StandardError
end
注意
许多编辑器/工具无法正确理解 Class.new 的用法。尝试查找类定义的人可能会尝试使用 grep "class FooError"。最后一点区别是,使用 Class.new 形式时,类的名称在基类的 inherited 回调中不可用。总的来说,最好坚持使用基本的双行风格。

文件类

不要将多行类嵌套在类中。尝试将这些嵌套类分别放在各自的文件中,并将文件命名为与包含类相同的名称。

# bad

# foo.rb
class Foo
  class Bar
    # 30 methods inside
  end

  class Car
    # 20 methods inside
  end

  # 30 methods inside
end

# good

# foo.rb
class Foo
  # 30 methods inside
end

# foo/bar.rb
class Foo
  class Bar
    # 30 methods inside
  end
end

# foo/car.rb
class Foo
  class Car
    # 20 methods inside
  end
end

命名空间定义

使用显式嵌套来定义(和重新打开)命名空间类和模块。使用作用域解析运算符可能会导致意外的常量查找,因为 Ruby 的 词法作用域 依赖于定义时的模块嵌套。

module Utilities
  class Queue
  end
end

# bad
class Utilities::Store
  Module.nesting # => [Utilities::Store]

  def initialize
    # Refers to the top level ::Queue class because Utilities isn't in the
    # current nesting chain.
    @queue = Queue.new
  end
end

# good
module Utilities
  class WaitingList
    Module.nesting # => [Utilities::WaitingList, Utilities]

    def initialize
      @queue = Queue.new # Refers to Utilities::Queue
    end
  end
end

模块与类

对于只有类方法的类,建议使用模块。只有在需要创建实例时才使用类。

# bad
class SomeClass
  def self.some_method
    # body omitted
  end

  def self.some_other_method
    # body omitted
  end
end

# good
module SomeModule
  module_function

  def some_method
    # body omitted
  end

  def some_other_method
    # body omitted
  end
end

module_function

当您想要将模块的实例方法转换为类方法时,建议使用 module_function 而不是 extend self

# bad
module Utilities
  extend self

  def parse_something(string)
    # do stuff here
  end

  def other_utility_method(number, string)
    # do some more stuff
  end
end

# good
module Utilities
  module_function

  def parse_something(string)
    # do stuff here
  end

  def other_utility_method(number, string)
    # do some more stuff
  end
end

Liskov

在设计类层次结构时,请确保它们符合 Liskov 替换原则

SOLID 设计

尝试使您的类尽可能 SOLID

定义 to_s

始终为表示域对象的类提供适当的 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 家族

使用 attr 函数族来定义简单的访问器或修改器。

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

# good
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= 则用于修改器(写入器)。

# bad
class Person
  def get_name
    "#{@first_name} #{@last_name}"
  end

  def set_name(name)
    @first_name, @last_name = name.split(' ')
  end
end

# good
class Person
  def name
    "#{@first_name} #{@last_name}"
  end

  def name=(name)
    @first_name, @last_name = name.split(' ')
  end
end

attr

避免使用 attr。使用 attr_readerattr_accessor 代替。

# bad - creates a single attribute accessor (deprecated in Ruby 1.9)
attr :something, true
attr :one, :two, :three # behaves as attr_reader

# good
attr_accessor :something
attr_reader :one, :two, :three

Struct.new

考虑使用 Struct.new,它可以为您定义简单的访问器、构造函数和比较运算符。

# good
class Person
  attr_accessor :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

# better
Person = Struct.new(:first_name, :last_name) do
end

不要扩展 Struct.new

不要扩展由 Struct.new 初始化的实例。扩展它会引入一个多余的类级别,并且如果文件被多次加载,也可能会导致奇怪的错误。

# bad
class Person < Struct.new(:first_name, :last_name)
end

# good
Person = Struct.new(:first_name, :last_name)

不要扩展 Data.define

不要扩展由 Data.define 初始化的实例。扩展它会引入一个多余的类级别。

# bad
class Person < Data.define(:first_name, :last_name)
end

Person.ancestors
# => [Person, #<Class:0x0000000105abed88>, Data, Object, (...)]

# good
Person = Data.define(:first_name, :last_name)

Person.ancestors
# => [Person, Data, Object, (...)]

鸭子类型

优先使用 鸭子类型 而不是继承。

# bad
class Animal
  # abstract method
  def speak
  end
end

# extend superclass
class Duck < Animal
  def speak
    puts 'Quack! Quack'
  end
end

# extend superclass
class Dog < Animal
  def speak
    puts 'Bau! Bau!'
  end
end

# good
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 # => will print 'child'

正如您所见,类层次结构中的所有类实际上共享一个类变量。通常应该优先使用类实例变量而不是类变量。

利用访问修饰符(例如 privateprotected

根据方法的预期用途,为方法分配适当的可见性级别 (privateprotected)。不要将所有内容都设置为 public(默认值)。

访问修饰符缩进

publicprotectedprivate 方法缩进与它们所应用的方法定义相同的程度。在可见性修饰符上方留一个空行,在下方留一个空行,以强调它适用于其下方的所有方法。

# good
class SomeClass
  def public_method
    # some code
  end

  private

  def private_method
    # some code
  end

  def another_private_method
    # some code
  end
end

定义类方法

使用 def self.method 定义类方法。这使得代码更容易重构,因为类名不会重复。

class TestClass
  # bad
  def TestClass.some_method
    # body omitted
  end

  # good
  def self.some_other_method
    # body omitted
  end

  # Also possible and convenient when you
  # have to define many class methods.
  class << self
    def first_method
      # body omitted
    end

    def second_method_etc
      # body omitted
    end
  end
end

词法别名方法

在词法类范围内为方法创建别名时,优先使用 alias,因为 self 在此上下文中的解析也是词法的,它清楚地向用户传达了别名间接关系在运行时或任何子类中不会被更改,除非明确说明。

class Westerner
  def first_name
    @names.first
  end

  alias given_name first_name
end

由于 aliasdef 一样是关键字,因此优先使用裸字参数而不是符号或字符串。换句话说,使用 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_method,因为 alias 的词法范围会导致这些情况下的不可预测性。

module Mononymous
  def self.included(other)
    other.class_eval { alias_method :full_name, :given_name }
  end
end

class Sting < Westerner
  include Mononymous
end

类和 self

当类(或模块)方法调用其他此类方法时,在调用其他此类方法时,省略使用前导 self 或自己的名称后跟一个 .。这在“服务类”或其他类似概念中经常看到,其中类被视为函数。这种约定往往会减少此类类中的重复样板代码。

class TestClass
  # bad - more work when class renamed/method moved
  def self.call(param1, param2)
    TestClass.new(param1).call(param2)
  end

  # bad - more verbose than necessary
  def self.call(param1, param2)
    self.new(param1).call(param2)
  end

  # good
  def self.call(param1, param2)
    new(param1).call(param2)
  end

  # ...other methods...
end

在块中定义常量

不要在块中定义常量,因为块的范围不会以任何方式隔离或命名空间常量。

相反,在块之外定义常量,或者如果在外部范围内定义常量有问题,则使用变量或方法。

# bad - FILES_TO_LINT is now defined globally
task :lint do
  FILES_TO_LINT = Dir['lib/*.rb']
  # ...
end

# good - files_to_lint is only defined inside the block
task :lint do
  files_to_lint = Dir['lib/*.rb']
  # ...
end

类:构造函数

工厂方法

考虑添加工厂方法,以提供创建特定类实例的其他合理方法。

class Person
  def self.create(options_hash)
    # body omitted
  end
end

构造函数中的析取赋值

在构造函数中,避免对实例变量进行不必要的析取赋值 (||=)。优先使用普通赋值。在 Ruby 中,实例变量(以 @ 开头)在赋值之前为 nil,因此在大多数情况下,析取是不必要的。

# bad
def initialize
  @x ||= 1
end

# good
def initialize
  @x = 1
end

注释

好的代码本身就是最好的文档。在你准备添加注释时,问问自己:“如何改进代码,使这个注释不再需要?”。改进代码,然后对其进行文档化,使其更加清晰。

— 史蒂夫·麦康奈尔

无注释

编写自文档化代码,忽略本节的其余内容。说真的!

原理注释

如果如何可以自文档化,但为什么不行(例如,代码绕过了不明显的库行为,或实现了来自学术论文的算法),请添加一个注释来解释代码背后的原理。

# bad

x = BuggyClass.something.dup

def compute_dependency_graph
  ...30 lines of recursive graph merging...
end

# good

# BuggyClass returns an internal object, so we have to dup it to modify it.
x = BuggyClass.something.dup

# This is algorithm 6.4(a) from Worf & Yar's _Amazing Graph Algorithms_ (2243).
def compute_dependency_graph
  ...30 lines of recursive graph merging...
end

英文注释

用英文编写注释。

哈希空间

在注释的开头 # 字符和注释文本之间使用一个空格。

英文语法

超过一个词的注释应首字母大写并使用标点符号。在句号后使用 一个空格

无多余注释

避免多余的注释。

# bad
counter += 1 # Increments counter by one.

注释维护

保持现有注释的最新状态。过时的注释比没有注释更糟糕。

重构,不要注释

好的代码就像一个好笑话:不需要解释。

— 老程序员格言
作者:Russ Olsen

避免写注释来解释糟糕的代码。重构代码使其自解释。(“做或不做,没有尝试。” 绝地大师尤达)

注释注解

注解位置

注解通常应该写在与相关代码紧邻的上一行。

# bad
def bar
  baz(:quux) # FIXME: This has crashed occasionally since v3.2.1.
end

# good
def bar
  # FIXME: This has crashed occasionally since v3.2.1.
  baz(:quux)
end

注解关键字格式

注解关键字后面跟着一个冒号和一个空格,然后是一个描述问题的注释。

# bad
def bar
  # FIXME This has crashed occasionally since v3.2.1.
  baz(:quux)
end

# good
def bar
  # FIXME: This has crashed occasionally since v3.2.1.
  baz(:quux)
end

多行注解缩进

如果需要多行来描述问题,后续行应该在 # 后缩进三个空格(一个通用缩进加两个用于缩进目的)。

def bar
  # FIXME: This has crashed occasionally since v3.2.1. It may
  #   be related to the BarBazUtil upgrade.
  baz(:quux)
end

行内注解

在问题过于明显,任何文档都会显得多余的情况下,可以在有问题的行末添加注解,不带注释。这种用法应该是例外,而不是规则。

def bar
  sleep 100 # OPTIMIZE
end

TODO

使用 TODO 来标记将来应该添加的缺失功能或功能。

FIXME

使用 FIXME 来标记需要修复的损坏代码。

OPTIMIZE

使用 OPTIMIZE 来标记可能导致性能问题的缓慢或低效代码。

HACK

使用 HACK 来标记代码异味,其中使用了有问题的编码实践,应该重构掉。

REVIEW

使用 REVIEW 来标记任何需要检查以确认其按预期工作的内容。例如:REVIEW: 我们确定这是客户当前执行 X 的方式吗?

文档注释

如果觉得合适,可以使用其他自定义注释关键字,但请务必在项目的 README 或类似文件中记录它们。

魔法注释

魔法注释优先

将魔法注释放在文件中的所有代码和文档之上(除了 shebang,将在下面讨论)。

# bad
# Some documentation about Person

# frozen_string_literal: true
class Person
end

# good
# frozen_string_literal: true

# Some documentation about Person
class Person
end

在 shebang 下面

当文件中存在 shebang 时,将魔法注释放在 shebang 下面。

# bad
# frozen_string_literal: true
#!/usr/bin/env ruby

App.parse(ARGV)

# good
#!/usr/bin/env ruby
# frozen_string_literal: true

App.parse(ARGV)

每行一个魔法注释

如果需要多个魔法注释,请每行使用一个。

# bad
# -*- frozen_string_literal: true; encoding: ascii-8bit -*-

# good
# frozen_string_literal: true
# encoding: ascii-8bit

将魔法注释与代码分开

用空行将魔法注释与代码和文档分开。

# bad
# frozen_string_literal: true
# Some documentation for Person
class Person
  # Some code
end

# good
# frozen_string_literal: true

# Some documentation for Person
class Person
  # Some code
end

集合

字面数组和哈希

优先使用字面数组和哈希创建表示法(除非你需要向它们的构造函数传递参数)。

# bad
arr = Array.new
hash = Hash.new

# good
arr = []
arr = Array.new(10)
hash = {}
hash = Hash.new(0)

%w

当你需要一个单词数组(没有空格和特殊字符的非空字符串)时,优先使用 %w 而不是字面数组语法。仅对包含两个或更多元素的数组应用此规则。

# bad
STATES = ['draft', 'open', 'closed']

# good
STATES = %w[draft open closed]

%i

当你需要一个符号数组(并且不需要维护 Ruby 1.9 兼容性)时,优先使用 %i 而不是字面数组语法。仅对包含两个或更多元素的数组应用此规则。

# bad
STATES = [:draft, :open, :closed]

# good
STATES = %i[draft open closed]

没有尾随数组逗号

避免在 ArrayHash 字面量的最后一个项目之后使用逗号,尤其是在项目不在单独的行上时。

# bad - easier to move/add/remove items, but still not preferred
VALUES = [
           1001,
           2020,
           3333,
         ]

# bad
VALUES = [1001, 2020, 3333, ]

# good
VALUES = [1001, 2020, 3333]

没有间隙数组

避免在数组中创建巨大的间隙。

arr = []
arr[100] = 1 # now you have an array with lots of nils

firstlast

当从数组中访问第一个或最后一个元素时,优先使用 firstlast 而不是 [0][-1]firstlast 更容易理解,特别是对于经验不足的 Ruby 程序员或来自具有不同索引语义的语言的人来说。

arr = [1, 2, 3]

# ok
arr[0]  # => 1
arr[-1] # => 3

# (arguably) better
arr.first # => 1
arr.last  # => 3

# good - assignments can only be done via []=
arr[0] = 2
arr[-1] = 5

Set 与 Array

在处理唯一元素时,使用 Set 而不是 ArraySet 实现了一个无序值的集合,没有重复项。这结合了 Array 的直观交互操作功能和 Hash 的快速查找功能。

符号作为键

优先使用符号而不是字符串作为哈希键。

# bad
hash = { 'one' => 1, 'two' => 2, 'three' => 3 }

# good
hash = { one: 1, two: 2, three: 3 }

不可变键

避免使用可变对象作为哈希键。

哈希字面量

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

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

# good
hash = { one: 1, two: 2, three: 3 }

哈希字面量值

当你的哈希键和值相同时,使用 Ruby 3.1 哈希字面量值语法。

# bad
hash = { one: one, two: two, three: three }

# good
hash = { one:, two:, three: }

哈希字面量作为数组的最后一个元素

如果哈希字面量是数组的最后一个元素,请将其用大括号括起来。

# bad
[1, 2, one: 1, two: 2]

# good
[1, 2, { one: 1, two: 2 }]

不要混合哈希语法

不要在同一个哈希字面量中混合使用 Ruby 1.9 哈希语法和哈希火箭。当你的键不是符号时,请坚持使用哈希火箭语法。

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

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

避免使用 Hash[] 构造函数

Hash::[] 是 Ruby 2.1 之前用于从键值对数组或扁平键值列表中构造哈希的一种方法。它具有模糊的语义,并且在代码中看起来很神秘。从 Ruby 2.1 开始,可以使用 Enumerable#to_h 从键值对列表中构造哈希,并且应该优先使用它。对于使用字面量键值对的 Hash[],应该优先使用哈希字面量。

# bad
Hash[ary]
Hash[a, b, c, d]

# good
ary.to_h
{a => b, c => d}

Hash#key?

使用 Hash#key? 代替 Hash#has_key?,使用 Hash#value? 代替 Hash#has_value?

# bad
hash.has_key?(:test)
hash.has_value?(value)

# good
hash.key?(:test)
hash.value?(value)

Hash#each

使用 Hash#each_key 代替 Hash#keys.each,使用 Hash#each_value 代替 Hash#values.each

# bad
hash.keys.each { |k| p k }
hash.values.each { |v| p v }
hash.each { |k, _v| p k }
hash.each { |_k, v| p v }

# good
hash.each_key { |k| p k }
hash.each_value { |v| p v }

Hash#fetch

在处理应该存在的哈希键时,使用 Hash#fetch

heroes = { batman: 'Bruce Wayne', superman: 'Clark Kent' }
# bad - if we make a mistake we might not spot it right away
heroes[:batman] # => 'Bruce Wayne'
heroes[:supermann] # => nil

# good - fetch raises a KeyError making the problem obvious
heroes.fetch(:supermann)

Hash#fetch 默认值

通过 Hash#fetch 为哈希键引入默认值,而不是使用自定义逻辑。

batman = { name: 'Bruce Wayne', is_evil: false }

# bad - if we just use || operator with falsey value we won't get the expected result
batman[:is_evil] || true # => true

# good - fetch works correctly with falsey values
batman.fetch(:is_evil, true) # => false

使用哈希块

如果需要评估的代码可能具有副作用或成本高昂,请优先使用块而不是 Hash#fetch 中的默认值。

batman = { name: 'Bruce Wayne' }

# bad - if we use the default value, we eager evaluate it
# so it can slow the program down if done multiple times
batman.fetch(:powers, obtain_batman_powers) # obtain_batman_powers is an expensive call

# good - blocks are lazy evaluated, so only triggered in case of KeyError exception
batman.fetch(:powers) { obtain_batman_powers }

Hash#values_atHash#fetch_values

当需要连续从哈希中检索多个值时,使用 Hash#values_atHash#fetch_values

# bad
email = data['email']
username = data['nickname']

# bad
keys = %w[email nickname].freeze
email, username = keys.map { |key| data[key] }

# good
email, username = data.values_at('email', 'nickname')

# also good
email, username = data.fetch_values('email', 'nickname')

Hash#transform_keysHash#transform_values

在转换哈希的键或值时,优先使用 `transform_keys` 或 `transform_values`,而不是 `each_with_object` 或 `map`。

# bad
{a: 1, b: 2}.each_with_object({}) { |(k, v), h| h[k] = v * v }
{a: 1, b: 2}.map { |k, v| [k.to_s, v] }.to_h

# good
{a: 1, b: 2}.transform_values { |v| v * v }
{a: 1, b: 2}.transform_keys { |k| k.to_s }

有序哈希

依赖于 Ruby 1.9 及更高版本中哈希是有序的事实。

不要修改集合

遍历集合时不要修改它。

直接访问元素

访问集合元素时,如果提供了替代形式的读取器方法,请避免通过 `[n]` 进行直接访问。这可以防止你在 `nil` 上调用 `[]`。

# bad
Regexp.last_match[1]

# good
Regexp.last_match(1)

提供集合的替代访问器

提供集合的访问器时,请提供替代形式,以防止用户在访问集合中的元素之前检查 `nil`。

# bad
def awesome_things
  @awesome_things
end

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

map/find/select/reduce/include?/size

优先使用 `map` 而不是 `collect`,`find` 而不是 `detect`,`select` 而不是 `find_all`,`reduce` 而不是 `inject`,`include?` 而不是 `member?`,以及 `size` 而不是 `length`。这不是硬性要求;如果使用别名可以提高可读性,则可以使用它。这些押韵的方法继承自 Smalltalk,在其他编程语言中并不常见。鼓励使用 `select` 而不是 `find_all` 的原因是,它与 `reject` 配合得很好,并且它的名称非常直观。

countsize

不要使用 `count` 来代替 `size`。对于除 `Array` 之外的 `Enumerable` 对象,它将遍历整个集合以确定其大小。

# bad
some_hash.count

# good
some_hash.size

flat_map

使用 `flat_map` 而不是 `map` + `flatten`。这并不适用于深度大于 2 的数组,例如,如果 `users.first.songs == ['a', ['b','c']]`,则使用 `map + flatten` 而不是 `flat_map`。`flat_map` 将数组扁平化 1 层,而 `flatten` 将其完全扁平化。

# bad
all_songs = users.map(&:songs).flatten.uniq

# good
all_songs = users.flat_map(&:songs).uniq

reverse_each

优先使用 reverse_each 而不是 reverse.each,因为一些包含 Enumerable 模块的类会提供更高效的实现。即使在最坏的情况下,某个类没有提供专门的实现,从 Enumerable 继承的通用实现至少与使用 reverse.each 一样高效。

# bad
array.reverse.each { ... }

# good
array.reverse_each { ... }

Object#yield_self vs Object#then

方法 Object#thenObject#yield_self 更受欢迎,因为名称 then 表明了意图,而不是行为。这使得生成的代码更容易阅读。

# bad
obj.yield_self { |x| x.do_something }

# good
obj.then { |x| x.do_something }
注意
您可以在 这里 阅读有关此指南背后的原因的更多信息。

使用范围切片

使用范围切片数组以提取一些元素(例如 ary[2..5])是一种流行的技术。下面您将找到一些在使用它时需要注意的小事项。

  • [0..-1]ary[0..-1] 中是多余的,并且只是 ary 的同义词。

# bad - you're selecting all the elements of the array
ary[0..-1]
ary[0..nil]
ary[0...nil]

# good
ary
  • Ruby 2.6 引入了无限范围,这提供了一种更简单的方法来描述一个一直到数组末尾的切片。

# bad - hard to process mentally
ary[1..-1]
ary[1..nil]

# good - easier to read and more concise
ary[1..]
  • Ruby 2.7 引入了无头范围,这在切片中也很方便。但是,与 ary[1..-1] 中有些模糊的 -1 不同,ary[0..42] 中的 0 作为起点很清楚。事实上,将其更改为 ary[..42] 可能会降低可读性。因此,使用类似 ary[0..42] 的代码是可以的。另一方面,ary[nil..42] 应该替换为 ary[..42]arr[0..42]

# bad - hard to process mentally
ary[nil..42]

# good - easier to read
ary[..42]
ary[0..42]

数字

数字中的下划线

在大型数字字面量中添加下划线以提高可读性。

# bad - how many 0s are there?
num = 1000000

# good - much easier to parse for the human brain
num = 1_000_000

数字字面量前缀

对于数字字面量前缀,优先使用小写字母。0o 用于八进制,0x 用于十六进制,0b 用于二进制。不要使用 0d 前缀表示十进制字面量。

# bad
num = 01234
num = 0O1234
num = 0X12AB
num = 0B10101
num = 0D1234
num = 0d1234

# good - easier to separate digits from the prefix
num = 0o1234
num = 0x12AB
num = 0b10101
num = 1234

整数类型检查

使用 Integer 检查整数的类型。由于 Fixnum 是平台相关的,在 32 位和 64 位机器上检查它将返回不同的结果。

timestamp = Time.now.to_i

# bad
timestamp.is_a?(Fixnum)
timestamp.is_a?(Bignum)

# good
timestamp.is_a?(Integer)

随机数

在生成随机数时,优先使用范围而不是带偏移量的整数,因为它清楚地表明了您的意图。想象一下模拟掷骰子

# bad
rand(6) + 1

# good
rand(1..6)

浮点数除法

当对两个整数执行浮点数除法时,可以使用 fdiv 或将其中一个整数转换为浮点数。

# bad
a.to_f / b.to_f

# good
a.to_f / b
a / b.to_f
a.fdiv(b)

浮点数比较

避免对浮点数进行(不)相等比较,因为它们不可靠。

浮点数本身就存在精度问题,而对它们进行精确相等比较几乎永远不是想要的语义。通过 ==/!= 运算符进行比较会检查浮点数的表示形式是否完全相同,如果您执行了任何涉及精度损失的算术运算,这将非常不可能。

# bad
x == 0.1
x != 0.1

# good - using BigDecimal
x.to_d == 0.1.to_d

# good - not an actual float comparison
x == Float::INFINITY

# good
(x - 0.1).abs < Float::EPSILON

# good
tolerance = 0.0001
(x - 0.1).abs < tolerance

# Or some other epsilon based type of comparison:
# https://www.embeddeduse.com/2019/08/26/qt-compare-two-floats/

指数表示法

使用指数表示法表示数字时,最好使用规范的科学计数法,即使用介于 1(包含)和 10(不包含)之间的尾数。如果指数为零,则完全省略指数。

目标是避免十的幂和指数表示法之间的混淆,因为快速阅读 10e7 的人可能会认为它是 10 的 7 次方(一个然后是 7 个零),而实际上它是 10 的 8 次方(一个然后是 8 个零)。如果您想要 10 的 7 次方,您应该使用 1e7

幂表示法 指数表示法 输出

10 ** 7

1e7

10000000

10 ** 6

1e6

1000000

10 ** 7

10e6

10000000

人们可能会倾向于使用另一种工程表示法,其中指数必须始终是 3 的倍数,以便于转换为千 / 百万 / …​ 系统。

# bad
10e6
0.3e4
11.7e5
3.14e0

# good
1e7
3e3
1.17e6
3.14

另一种:工程表示法

# bad
3.2e7
0.1e5
12e4

# good
1e6
17e6
0.98e9

字符串

字符串插值

优先使用字符串插值和字符串格式化而不是字符串连接

# bad
email_with_name = user.name + ' <' + user.email + '>'

# good
email_with_name = "#{user.name} <#{user.email}>"

# good
email_with_name = format('%s <%s>', user.name, user.email)

一致的字符串字面量

采用一致的字符串字面量引用风格。在 Ruby 社区中,有两种流行的风格,这两种风格都被认为是好的 - 默认情况下使用单引号和默认情况下使用双引号。

注意
本指南中的字符串字面量默认使用单引号。

单引号

如果您不需要字符串插值或特殊符号,例如 \t\n' 等,请优先使用单引号字符串。

# bad
name = "Bozhidar"

name = 'De\'Andre'

# good
name = 'Bozhidar'

name = "De'Andre"

双引号

除非您的字符串字面量包含 " 或您想要抑制的转义字符,否则优先使用双引号。

# bad
name = 'Bozhidar'

sarcasm = "I \"like\" it."

# good
name = "Bozhidar"

sarcasm = 'I "like" it.'

没有字符字面量

不要使用字符字面量语法 ?x。自 Ruby 1.9 以来,它基本上是多余的 - ?x 将被解释为 'x'(包含单个字符的字符串)。

# bad
char = ?c

# good
char = 'c'

花括号插值

在将实例变量和全局变量插值到字符串中时,不要省略{}

class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  # bad - valid, but awkward
  def to_s
    "#@first_name #@last_name"
  end

  # good
  def to_s
    "#{@first_name} #{@last_name}"
  end
end

$global = 0
# bad
puts "$global = #$global"

# good
puts "$global = #{$global}"

不要使用to_s

不要对插值的物体使用Object#to_s。它会自动调用。

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

# good
message = "This is the #{result}."

字符串连接

当需要构建大型数据块时,避免使用String#`。相反,使用`String#<<`。连接会就地修改字符串实例,并且始终比String#更快,后者会创建一堆新的字符串对象。

# bad
html = ''
html += '<h1>Page title</h1>'

paragraphs.each do |paragraph|
  html += "<p>#{paragraph}</p>"
end

# good and also fast
html = ''
html << '<h1>Page title</h1>'

paragraphs.each do |paragraph|
  html << "<p>#{paragraph}</p>"
end

不要滥用gsub

在可以使用更快、更专业的替代方案的情况下,不要使用String#gsub

url = 'http://example.com'
str = 'lisp-case-rules'

# bad
url.gsub('http://', 'https://')
str.gsub('-', '_')

# good
url.sub('http://', 'https://')
str.tr('-', '_')

String#chars

优先使用String#chars而不是使用空字符串或正则表达式字面量参数的String#split

注意
从 Ruby 2.0 开始,这些情况的行为相同。
# bad
string.split(//)
string.split('')

# good
string.chars

sprintf

优先使用sprintf及其别名format,而不是相当神秘的String#%方法。

# bad
'%d %d' % [20, 10]
# => '20 10'

# good
sprintf('%d %d', 20, 10)
# => '20 10'

# good
sprintf('%<first>d %<second>d', first: 20, second: 10)
# => '20 10'

format('%d %d', 20, 10)
# => '20 10'

# good
format('%<first>d %<second>d', first: 20, second: 10)
# => '20 10'

命名格式标记

当使用命名格式字符串标记时,优先使用%<name>s而不是%{name},因为它编码了有关值类型的信息。

# bad
format('Hello, %{name}', name: 'John')

# good
format('Hello, %<name>s', name: 'John')

长字符串

将长字符串分成多行,但不要使用+连接它们。如果要添加换行符,请使用 heredoc。否则使用\

# bad
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. " +
"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, " +
"when an unknown printer took a galley of type and scrambled it to make a type specimen book."

# good
<<~LOREM
  Lorem Ipsum is simply dummy text of the printing and typesetting industry.
  Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
  when an unknown printer took a galley of type and scrambled it to make a type specimen book.
LOREM

# good
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. "\
"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, "\
"when an unknown printer took a galley of type and scrambled it to make a type specimen book."

Heredocs

波浪形 Heredoc

对于格式良好的多行字符串,使用 Ruby 2.3 的波浪形 heredocs。

# bad - using Powerpack String#strip_margin
code = <<-RUBY.strip_margin('|')
  |def test
  |  some_method
  |  other_method
  |end
RUBY

# also bad
code = <<-RUBY
def test
  some_method
  other_method
end
RUBY

# good
code = <<~RUBY
  def test
    some_method
    other_method
  end
RUBY

Heredoc 分隔符

为 heredocs 使用描述性的分隔符。分隔符为 heredoc 内容添加了有价值的信息,作为额外的好处,如果使用正确的分隔符,一些编辑器可以突出显示 heredoc 中的代码。

# bad
code = <<~END
  def foo
    bar
  end
END

# good
code = <<~RUBY
  def foo
    bar
  end
RUBY

# good
code = <<~SUMMARY
  An imposing black structure provides a connection between the past and
  the future in this enigmatic adaptation of a short story by revered
  sci-fi author Arthur C. Clarke.
SUMMARY

Heredoc 方法调用

将带有 heredoc 接收者的方法调用放在 heredoc 定义的第一行。如果添加或删除新行,不良形式具有很大的错误可能性。

# bad
query = <<~SQL
  select foo from bar
SQL
.strip_indent

# good
query = <<~SQL.strip_indent
  select foo from bar
SQL

Heredoc 参数闭合括号

将带有 heredoc 参数的函数调用中的闭合括号放在 heredoc 定义的第一行。如果删除闭合括号之前的换行符,则不良格式可能会导致错误。

# bad
foo(<<~SQL
  select foo from bar
SQL
)

# good
foo(<<~SQL)
  select foo from bar
SQL

日期和时间

Time.now

在获取当前系统时间时,优先使用 Time.now 而不是 Time.new

不使用 DateTime

除非您需要考虑历史上的日历改革,否则不要使用 DateTime - 如果需要,请明确指定 start 参数以清楚地说明您的意图。

# bad - uses DateTime for current time
DateTime.now

# good - uses Time for current time
Time.now

# bad - uses DateTime for modern date
DateTime.iso8601('2016-06-29')

# good - uses Date for modern date
Date.iso8601('2016-06-29')

# good - uses DateTime with start argument for historical date
DateTime.iso8601('1751-04-23', Date::ENGLAND)

正则表达式

有些人遇到问题时会想:“我知道了,我会使用正则表达式。” 现在他们有两个问题了。

— Jamie Zawinski

纯文本搜索

如果您只需要在字符串中进行纯文本搜索,请不要使用正则表达式。

foo = 'I am an example string'

# bad - using a regular expression is an overkill here
foo =~ /example/

# good
foo['example']

使用正则表达式作为字符串索引

对于简单的构造,您可以直接通过字符串索引使用正则表达式。

match = string[/regexp/]             # get content of matched regexp
first_group = string[/text(grp)/, 1] # get content of captured group
string[/text (grp)/, 1] = 'replace'  # string => 'text replace'

优先使用非捕获组

当您不使用捕获结果时,请使用非捕获组。

# bad
/(first|second)/

# good
/(?:first|second)/

不要混合命名捕获和编号捕获

不要在正则表达式字面量中混合命名捕获和编号捕获。因为如果混合使用,编号捕获将被忽略。

# bad - There is no way to access `(BAR)` capturing.
m = /(?<foo>FOO)(BAR)/.match('FOOBAR')
p m[:foo] # => "FOO"
p m[1]    # => "FOO"
p m[2]    # => nil   - not "BAR"

# good - Both captures are accessible with names.
m = /(?<foo>FOO)(?<bar>BAR)/.match('FOOBAR')
p m[:foo] # => "FOO"
p m[:bar] # => "BAR"

# good - `(?:BAR)` is non-capturing grouping.
m = /(?<foo>FOO)(?:BAR)/.match('FOOBAR')
p m[:foo] # => "FOO"

# good - Both captures are accessible with numbers.
m = /(FOO)(BAR)/.match('FOOBAR')
p m[1] # => "FOO"
p m[2] # => "BAR"

按名称引用命名正则表达式捕获

优先使用名称来引用命名正则表达式捕获,而不是使用数字。

# bad
m = /(?<foo>FOO)(?<bar>BAR)/.match('FOOBAR')
p m[1] # => "FOO"
p m[2] # => "BAR"

# good
m = /(?<foo>FOO)(?<bar>BAR)/.match('FOOBAR')
p m[:foo] # => "FOO"
p m[:bar] # => "BAR"

避免使用 Perl 风格的最后一个正则表达式组匹配器

不要使用表示最后一个正则表达式组匹配的 cryptic Perl 遗留变量($1$2 等)。请改用 Regexp.last_match(n)

/(regexp)/ =~ string
...

# bad
process $1

# good
process Regexp.last_match(1)

避免使用编号组

避免使用编号组,因为很难跟踪它们包含的内容。可以使用命名组代替。

# bad
/(regexp)/ =~ string
# some code
process Regexp.last_match(1)

# good
/(?<meaningful_var>regexp)/ =~ string
# some code
process meaningful_var

限制转义

字符类只有几个特殊字符需要关注:^-\],所以不要转义.[]中的方括号。

脱字符和美元符号正则表达式

小心使用^$,因为它们匹配的是行首/行尾,而不是字符串结尾。如果你想匹配整个字符串,请使用:\A\z(不要与\Z混淆,它等效于/\n?\z/)。

string = "some injection\nusername"
string[/^username$/]   # matches
string[/\Ausername\z/] # doesn't match

多行正则表达式

对于多行正则表达式,使用x(自由间距)修饰符。

注意
这被称为自由间距模式。在这种模式下,前导和尾随空格将被忽略。
# bad
regex = /start\
\s\
(group)\
(?:alt1|alt2)\
end/

# good
regexp = /
  start
  \s
  (group)
  (?:alt1|alt2)
  end
/x

注释复杂正则表达式

对于复杂的正则表达式,使用x修饰符。这使得它们更易读,你可以添加一些有用的注释。

regexp = /
  start         # some text
  \s            # white space char
  (group)       # first group
  (?:alt1|alt2) # some alternation
  end
/x

使用带块或哈希的gsub进行复杂替换

对于复杂的替换,sub/gsub可以与块或哈希一起使用。

words = 'foo bar'
words.sub(/f/, 'f' => 'F') # => 'Foo bar'
words.gsub(/\w+/) { |word| word.capitalize } # => 'Foo Bar'

百分号字面量

%q简写

对于需要插值和嵌入双引号的单行字符串,使用%()(它是%Q的简写)。对于多行字符串,建议使用heredoc。

# bad (no interpolation needed)
%(<div class="text">Some text</div>)
# should be '<div class="text">Some text</div>'

# bad (no double-quotes)
%(This is #{quality} style)
# should be "This is #{quality} style"

# bad (multiple lines)
%(<div>\n<span class="big">#{exclamation}</span>\n</div>)
# should be a heredoc.

# good (requires interpolation, has quotes, single line)
%(<tr><td class="name">#{name}</td>)

%q

除非你的字符串中同时包含'",否则避免使用%()或等效的%q()。常规字符串字面量更易读,除非需要转义大量字符,否则应该优先使用它们。

# bad
name = %q(Bruce Wayne)
time = %q(8 o'clock)
question = %q("What did you say?")

# good
name = 'Bruce Wayne'
time = "8 o'clock"
question = '"What did you say?"'
quote = %q(<p class='quote'>"What did you say?"</p>)

%r

仅当正则表达式匹配至少一个/字符时,才使用%r

# bad
%r{\s+}

# good
%r{^/(.*)$}
%r{^/blog/2011/(.*)$}

%x

除非你要使用反引号执行命令(这种情况不太可能),否则避免使用%x

# bad
date = %x(date)

# good
date = `date`
echo = %x(echo `date`)

%s

避免使用%s。社区似乎已经决定:"some string"是创建带空格符号的首选方式。

百分比字面量大括号

使用最适合各种百分比字面量的括号。

  • () 用于字符串字面量 (%q, %Q)。

  • [] 用于数组字面量 (%w, %i, %W, %I),因为它与标准数组字面量保持一致。

  • {} 用于正则表达式字面量 (%r),因为括号经常出现在正则表达式中。这就是为什么使用 { 的不太常见的字符通常是 %r 字面量的最佳分隔符。

  • () 用于所有其他字面量(例如 %s, %x

# bad
%q{"Test's king!", John said.}

# good
%q("Test's king!", John said.)

# bad
%w(one two three)
%i(one two three)

# good
%w[one two three]
%i[one two three]

# bad
%r((\w+)-(\d+))
%r{\w{1,2}\d{2,5}}

# good
%r{(\w+)-(\d+)}
%r|\w{1,2}\d{2,5}|

元编程

避免不必要的元编程

避免不必要的元编程。

不要猴子补丁

编写库时不要在核心类中乱搞(不要猴子补丁)。

class_eval

class_eval 的块形式优于字符串插值形式。

提供位置

当您使用字符串插值形式时,始终提供 __FILE____LINE__,以便您的回溯有意义

class_eval 'def use_relative_model_naming?; true; end', __FILE__, __LINE__

define_method

define_method 优于 class_eval { def …​ }

eval 注释文档

当使用 class_eval(或其他 eval)进行字符串插值时,添加一个注释块,显示其插值后的外观(Rails 代码中使用的做法)

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

避免使用 method_missing 进行元编程,因为回溯会变得混乱,行为不会列在 #methods 中,并且拼写错误的方法调用可能会静默地工作,例如 nukes.luanch_state = false。考虑使用委托、代理或 define_method 代替。如果您必须使用 method_missing

  • 请务必 也定义 respond_to_missing?

  • 只捕获具有明确定义前缀的方法,例如 find_by_* - 使您的代码尽可能地断言。

  • 在您的语句末尾调用 super

  • 委托给断言的、非魔术方法

# bad
def method_missing(meth, *params, &block)
  if /^find_by_(?<prop>.*)/ =~ meth
    # ... lots of code to do a find_by
  else
    super
  end
end

# good
def method_missing(meth, *params, &block)
  if /^find_by_(?<prop>.*)/ =~ meth
    find_by(prop, *params, &block)
  else
    super
  end
end

# best of all, though, would to define_method as each findable attribute is declared

优先使用 public_send

优先使用 public_send 而不是 send,以避免绕过 private/protected 可见性。

# We have an ActiveModel Organization that includes concern Activatable
module Activatable
  extend ActiveSupport::Concern

  included do
    before_create :create_token
  end

  private

  def reset_token
    # some code
  end

  def create_token
    # some code
  end

  def activate!
    # some code
  end
end

class Organization < ActiveRecord::Base
  include Activatable
end

linux_organization = Organization.find(...)

# bad - violates privacy
linux_organization.send(:reset_token)
# good - should throw an exception
linux_organization.public_send(:reset_token)

优先使用 __send__

优先使用 __send__ 而不是 send,因为 send 可能与现有方法重叠。

require 'socket'

u1 = UDPSocket.new
u1.bind('127.0.0.1', 4913)
u2 = UDPSocket.new
u2.connect('127.0.0.1', 4913)

# bad - Won't send a message to the receiver object. Instead it will send a message via UDP socket.
u2.send :sleep, 0
# good - Will actually send a message to the receiver object.
u2.__send__ ...

API 文档

YARD

使用 YARD 及其约定进行 API 文档。

RD(块)注释

不要使用块注释。它们不能以空格开头,而且不像普通注释那样容易发现。

# bad
=begin
comment line
another comment line
=end

# good
# comment line
# another comment line
从 Perl 的 POD 到 RD

这并不是真正的块注释语法,而是更像是试图模拟 Perl 的 POD 文档系统。

有一个用于 Ruby 的 rdtool,它与 POD 非常相似。基本上,rdtool 会扫描文件以查找 =begin=end 对,并提取它们之间的所有文本。假设此文本是 RD 格式 的文档。你可以 在这里 阅读更多相关信息。

RD 早于 RDoc 和 YARD 的兴起,实际上已被它们取代。3

Gemfile 和 Gemspec

Gemspec 中不包含 RUBY_VERSION

Gemspec 不应包含 RUBY_VERSION 作为切换依赖项的条件。RUBY_VERSIONrake release 确定,因此用户最终可能会使用错误的依赖项。

# bad
Gem::Specification.new do |s|
  if RUBY_VERSION >= '2.5'
    s.add_runtime_dependency 'gem_a'
  else
    s.add_runtime_dependency 'gem_b'
  end
end

通过以下方式修复:

  • 安装后消息。

  • 将两个 gem 都添加为依赖项(如果允许)。

  • 如果是开发依赖项,则将其移至 Gemfile。

其他

不要检查非 nil

除非处理布尔值,否则不要进行显式的非nil检查。

# bad
do_something if !something.nil?
do_something if something != nil

# good
do_something if something

# good - dealing with a boolean
def value_set?
  !@some_boolean.nil?
end

全局输入/输出流

使用$stdout/$stderr/$stdin而不是STDOUT/STDERR/STDINSTDOUT/STDERR/STDIN是常量,虽然你实际上可以在 Ruby 中重新赋值(可能重定向一些流)常量,但如果你这样做,你会得到解释器的警告。

# bad
STDOUT.puts('hello')

hash = { out: STDOUT, key: value }

def m(out = STDOUT)
  out.puts('hello')
end

# good
$stdout.puts('hello')

hash = { out: $stdout, key: value }

def m(out = $stdout)
  out.puts('hello')
end
注意
流常量的唯一有效用例是获取对原始流的引用(假设你已经重定向了一些全局变量)。

警告

使用warn而不是$stderr.puts。除了更简洁和清晰之外,warn允许你在需要时抑制警告(通过将警告级别设置为 0,方法是使用-W0)。

# bad
$stderr.puts 'This is a warning!'

# good
warn 'This is a warning!'

Array#join

优先使用Array#join,而不是使用带有字符串参数的相当神秘的Array#*

# bad
%w[one two three] * ', '
# => 'one, two, three'

# good
%w[one two three].join(', ')
# => 'one, two, three'

数组强制转换

当处理你想作为数组处理的变量时,但你不确定它是否是数组,使用Array()而不是显式的Array检查或[*var]

# bad
paths = [paths] unless paths.is_a?(Array)
paths.each { |path| do_something(path) }

# bad (always creates a new Array instance)
[*paths].each { |path| do_something(path) }

# good (and a bit more readable)
Array(paths).each { |path| do_something(path) }

范围或between

尽可能使用范围或Comparable#between?,而不是复杂的比较逻辑。

# bad
do_something if x >= 1000 && x <= 2000

# good
do_something if (1000..2000).include?(x)

# good
do_something if x.between?(1000, 2000)

谓词方法

优先使用谓词方法,而不是使用==进行显式比较。数字比较是可以的。

# bad
if x % 2 == 0
end

if x % 2 == 1
end

if x == nil
end

# good
if x.even?
end

if x.odd?
end

if x.nil?
end

if x.zero?
end

if x == 0
end

没有神秘的 Perl 式

避免使用 Perl 风格的特殊变量(如$:$;等)。它们非常神秘,除了单行脚本之外,不建议在任何地方使用它们。

# bad
$:.unshift File.dirname(__FILE__)

# good
$LOAD_PATH.unshift File.dirname(__FILE__)

如果需要,使用English库提供的用户友好的别名。

# bad
print $', $$

# good
require 'English'
print $POSTMATCH, $PID

尽可能使用require_relative

对于所有内部依赖项,你应该使用require_relativerequire的使用应该保留给外部依赖项。

# bad
require 'set'
require 'my_gem/spec/helper'
require 'my_gem/lib/something'

# good
require 'set'
require_relative 'helper'
require_relative '../lib/something'

这种方式更具表达性(明确哪些依赖项是内部的,哪些不是),并且更高效(因为require_relative不需要尝试所有$LOAD_PATH,与require相反)。

始终警告

编写ruby -w安全代码。

无可选哈希参数

避免使用哈希作为可选参数。方法是否做得太多?(对象初始化器是此规则的例外)。

实例变量

使用模块实例变量而不是全局变量。

# bad
$foo_bar = 1

# good
module Foo
  class << self
    attr_accessor :bar
  end
end

Foo.bar = 1

OptionParser

使用OptionParser解析复杂的命令行选项,使用ruby -s解析简单的命令行选项。

无参数变异

不要修改参数,除非这是方法的目的。

三是你要数的数字

避免超过三层的块嵌套。

函数式代码

以函数式方式编写代码,在有意义的情况下避免变异。

a = []; [1, 2, 3].each { |i| a << i * 2 }   # bad
a = [1, 2, 3].map { |i| i * 2 }             # good

a = {}; [1, 2, 3].each { |i| a[i] = i * 17 }                # bad
a = [1, 2, 3].reduce({}) { |h, i| h[i] = i * 17; h }        # good
a = [1, 2, 3].each_with_object({}) { |i, h| h[i] = i * 17 } # good

不要显式使用.rbrequire

省略传递给requirerequire_relative的文件名的.rb扩展名。

注意
如果省略了扩展名,Ruby 会尝试在名称中添加 '.rb'、'.so' 等,直到找到为止。如果找不到名为的文件,将引发LoadError。存在一个边缘情况,如果foo.so文件存在,则会加载foo.so文件而不是LoadError,如果require 'foo.rb'将更改为require 'foo',但这似乎无害。
# bad
require 'foo.rb'
require_relative '../foo.rb'

# good
require 'foo'
require 'foo.so'
require_relative '../foo'
require_relative '../foo.so'

避免使用tap

tap方法可以帮助调试,但不要将其保留在生产代码中。

# bad
Config.new(hash, path).tap do |config|
  config.check if check
end

# good
config = Config.new(hash, path)
config.check if check
config

这更简单、更高效。

工具

以下是一些工具,可以帮助您根据本指南自动检查 Ruby 代码。

RuboCop

RuboCop 是一个基于此风格指南的 Ruby 静态代码分析器和格式化程序。RuboCop 已经涵盖了指南的很大一部分,并且为大多数流行的 Ruby 编辑器和 IDE 提供了 插件

提示
RuboCop 的 cops(代码检查)在其元数据中包含指向它们所基于的指南的链接。

RubyMine

RubyMine 的代码检查 部分基于 此指南。

历史

此指南最初于 2011 年作为内部公司 Ruby 编码指南(由 Bozhidar Batsov 编写)开始。Bozhidar 作为一名 Ruby 开发人员,一直对一件事感到困扰 - Python 开发人员有一个很棒的编程风格参考(PEP-8),而 Ruby 开发人员从未获得过官方指南,记录 Ruby 编码风格和最佳实践。Bozhidar 坚信风格很重要。他还相信,像 Ruby 这样优秀的黑客社区,应该能够制作出这份梦寐以求的文档。其余的都是历史了……

在某个时候,Bozhidar 决定他正在做的工作可能对 Ruby 社区的成员来说很有趣,而且世界并不需要另一个内部公司指南。但世界肯定可以从社区驱动的、社区认可的 Ruby 编程实践、习语和风格规范中受益。

Bozhidar 在项目过渡到 RuboCop 总部之前,担任了该指南的唯一编辑几年,之后组建了一个编辑团队。

自从指南问世以来,我们收到了来自世界各地优秀的 Ruby 社区成员的大量反馈。感谢所有建议和支持!我们可以共同创建一个对每个 Ruby 开发人员都有益的资源。

灵感来源

许多人、书籍、演示文稿、文章和其他风格指南影响了社区 Ruby 风格指南。以下是一些例子:

贡献

本指南仍在不断完善中 - 一些指南缺乏示例,一些指南的示例不足以清晰地说明它们。改进这些指南是帮助 Ruby 社区的一个很棒(且简单)的方式!

这些问题将在适当的时候(希望)得到解决 - 现在请记住它们。

本指南中没有内容是不可更改的。我们希望与所有对 Ruby 编码风格感兴趣的人一起合作,以便最终创建一个对整个 Ruby 社区都有益的资源。

欢迎您随时提交问题或发送包含改进内容的拉取请求。感谢您的帮助!

您也可以通过以下平台之一为项目(和 RuboCop)提供财务支持

如何贡献?

很简单,只需按照以下贡献指南操作即可

题辞

本指南使用 AsciiDoc 编写,并使用 AsciiDoctor 发布为 HTML。指南的 HTML 版本托管在 GitHub Pages 上。

指南最初使用 Markdown 编写,但在 2019 年转换为 AsciiDoc。

传播

社区驱动的风格指南对于不知道其存在的社区来说毫无用处。在 Twitter 上发布指南,与您的朋友和同事分享。我们收到的每条评论、建议或意见都会让指南变得更好一点。我们想要拥有最好的指南,不是吗?


1. 不过,我们偶尔可能会建议读者考虑一些替代方案。
2. *BSD/Solaris/Linux/macOS 用户默认覆盖,Windows 用户需要格外小心。
3. 根据这篇 维基百科文章,这种格式曾经流行,直到 2000 年代初被 RDoc 取代。