風格是從偉大事物中萃取出的美好事物。
-- Bozhidar Batsov
作為 Ruby 開發者,有一件總是令我煩心的事 — Python 開發者有一份好的程式風格參考指南(PEP-8) 而我們永遠沒有一份官方指南,一份記錄 Ruby 程式風格及最佳實踐的指南。而我們確信風格很重要。我也相信這些好傢伙們,像我們是 Ruby 開發者,應該可以自己產生一份這夢寐以求的文件。
這份指南開始是作為我們公司內部 Ruby 程式指南(由我所寫的)。進行到某個部分時,我決定要把我的成果貢獻給廣大的 Ruby 社群,而且這個世界需要來自另一個公司內部的一點幫助。然而這個世界也可以從由社群制定及驅動的一系列 Ruby 程式慣例、實踐及風格中受益。
在開始寫這份指南時,我收到世界上很多優秀 Ruby 社群用戶們的反饋。感謝所有的建議及幫助!我們同心協力創造一個能夠讓每一個 Ruby 開發者受益的資源。
順道一提,如果你對 Rails 有興趣,你可以看看這份與之互補的 Ruby on Rails 3 風格指南。
這份 Ruby 風格指南向你推薦現實世界中的最佳實踐,Ruby 程式設計師如何寫出可被別的 Ruby 程式設計師維護的程式碼。一份風格指南反映出現實世界中的用法,並帶有一個理想,避免已經公認是危險的事物被人繼續使用 –– 不管看起來是多麼的好。
本指南依照相關規則分成數個小節。我試著在每個規則後面說明理由(如果省略的話,我相信理由是顯而易見的)。
我沒有想到所有的規則 — 他們大致上是基於,我作為一個專業軟體工程師的廣泛生涯,從 Ruby 社群成員所得到的反饋及建議,和數個高度評價的 Ruby 程式設計資源,像是 "Programming Ruby 1.9" 以及 "The Ruby Programming Language"。
本指南仍在進行完善中 –– 某些規則缺乏實例,某些規則沒有例子來清楚地展示它們。在完稿時,將會解決這些議題 — 現在就先把他們記在心理吧。
你可以使用 Transmuter 來產生本指南的一份 PDF 或 HTML 副本。
rubocop 專案會自動檢查你的 Ruby 程式碼是否符合這份 Ruby 風格指南。目前這個專案尚有許多功能缺漏,不足以被正式地使用,歡迎有志之士協助改進。
本指南被翻譯成下列語言:
幾乎每人都深信,每一個除了自己的風格都又醜又難讀。把 "除了自己的" 拿掉,他們或許是對的...
-- Jerry Coffin (論縮排)
-
使用
UTF-8
作為原始檔案的編碼。 -
每個縮排層級使用兩個空格。不要使用 Hard Tabs。
# 不好 - 四個空格 def some_method do_something end # 好 def some_method do_something end
-
使用 Unix 風格的行編碼 (BSD/Solaris/Linux/OSX 的使用者不用擔心,Windows 使用者要特別小心。)
-
如果你使用 Git ,你也許會想加入下面這個配置設定,來保護你的專案被 Windows 的行編碼侵入:
$ git config --global core.autocrlf true
-
-
Don't use
;
to separate statements and expressions. As a corollary - use one expression per line.# bad puts 'foobar'; # superfluous semicolon puts 'foo'; puts 'bar' # two expression on the same line # good puts 'foobar' puts 'foo' puts 'bar' puts 'foo', 'bar' # this applies to puts in particular
-
Prefer a single-line format for class definitions with no body.
# bad class FooError < StandardError end # good class FooError < StandardError; end
-
Avoid single-line methods. Although they are somewhat popular in the wild, there are a few peculiarities about their definition syntax that make their use undesirable. At any rate - there should no more than one expression in a single-line method.
# 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 ; make it kind of hard to read def some_method() body end # good def some_method body end
One exception to the rule are empty-body methods.
# good def no_op; end
-
使用空格來圍繞運算元,逗點
,
、冒號:
及分號;
之後,圍繞{
和}
之前。 空格可能對(大部分)Ruby 直譯器來說是無關緊要的,但正確的使用是寫出可讀性高的程式碼的關鍵。sum = 1 + 2 a, b = 1, 2 1 > 2 ? true : false; puts 'Hi' [1, 2, 3].each { |e| puts e }
(針對運算子)唯一的例外是當使用指數運算子時:
# 不好 e = M * c ** 2 # 好 e = M * c**2
{
and}
deserve a bit of clarification, since they are used for block and hash literals, as well as embedded expressions in strings. For hash literals two styles are considered acceptable.# good - space after { and before } { one: 1, two: 2 } # good - no space after { and before } {one: 1, two: 2}
The first variant is slightly more readable (and arguably more popular in the Ruby community in general). The second variant has the advantage of adding visual difference between block and hash literals. Whichever one you pick - apply it consistently.
As far as embedded expressions go, there are also two acceptable options:
# good - no spaces "string#{expr}" # ok - arguably more readable "string#{ expr }"
The first style is extremely more popular and you're generally advised to stick with it. The second, on the other hand, is (arguably) a bit more readable. As with hashes - pick one style and apply it consistently.
-
不要有空格在
(
、[
之後,或]
、)
之前。some(arg).other [1, 2, 3].length
-
把
when
跟case
縮排在同一層。我知道很多人不同意這一點,但這是 "The Ruby Programming Language" 及 "Programming Ruby" 所設立的風格。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 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
-
在
def
之間使用空行,並且把方法分成合乎邏輯的段落。def some_method data = initialize(options) data.manipulate! data.result end def some_method result end
-
Use spaces around the
=
operator when assigning default values to method parameters:# bad def some_method(arg1=:default, arg2=nil, arg3=[]) # do something... end # good def some_method(arg1 = :default, arg2 = nil, arg3 = []) # do something... end
While several Ruby books suggest the first style, the second is much more prominent in practice (and arguably a bit more readable).
-
Avoid line continuation (\) where not required. In practice, avoid using line continuations at all.
# bad result = 1 - \ 2 # good (but still ugly as hell) result = 1 \ - 2
-
When continuing a chained method invocation on another line keep the
.
on the second line.# 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
-
當一個方法呼叫的參數擴展超過一行時,排列它們。
# 一開始(一行太長) def send_mail(source) Mailer.deliver(to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text) end # 不好(一般的縮排) def send_mail(source) Mailer.deliver( to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text) end # 不好(兩倍縮排) def send_mail(source) Mailer.deliver( to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text) end # 好 def send_mail(source) Mailer.deliver(to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text) end
-
Add underscores to big numeric literals to improve their readability.
# 差勁 - 到底是有幾個零? num = 1000000 # 良好 - 容易被人腦解讀。 num = 1_000_000
-
使用 RDoc 以及它的慣例來撰寫 API 文件。不要在註解區塊及
def
之前放一個空行。 -
將每一行限制在最多 80 個字元。
-
避免尾隨的空白(trailing whitesapce)。
-
Don't use block comments. They cannot be preceded by whitespace and are not as easy to spot as regular comments.
# bad == begin comment line another comment line == end # good # comment line # another comment line
-
Use
::
only to reference constants(this includes classes and modules). Never use::
for method invocation.# bad SomeClass::some_method some_object::some_method # good SomeClass.some_method some_object.some_method SomeModule::SomeClass::SOME_CONST
-
使用
def
時,當有參數時使用括號。當方法不接受任何參數時,省略括號。def some_method # 省略主體 end def some_method_with_arguments(arg1, arg2) # 省略主體 end
-
永遠不要使用
for
,除非你很清楚為什麼。大部分情況應該使用迭代器來取代。for
是由each
所實作的(所以你加入了一層的迂迴),但出乎意料的是 —for
並沒有包含一個新的視野 (不像是each
)而在這個區塊中定義的變數將會被外部所看到。arr = [1, 2, 3] # 不好 for elem in arr do puts elem end # 好 arr.each { |elem| puts elem }
-
永遠不要在多行的
if/unless
使用then
# 不好 if some_condition then # 省略主體 end # 好 if some_condition # 省略主體 end
-
偏愛三元運算元
? :
勝於if/then/else/end
結構 -
它更為常見及更精準。
# 不好 result = if some_condition then something else something_else end # 好 result = some_condition ? something : something_else
-
使用一個表達式給一個三元運算元的分支。這也意味著三元運算符不要寫成巢狀式。巢狀情況使用
if/else
結構。# 不好 some_condition ? (nested_condition ? nested_something : nested_something_else) : something_else # 好 if some_condition nested_condition ? nested_something : nested_something_else else something_else end
-
永遠不要使用
if x: ...
— 它已經在 Ruby 1.9 被移除了。使用三元運算元來取代。# 不好 result = if some_condition: something else something_else end # 好 result = some_condition ? something : something_else
-
永遠不要使用
if x; ...
使用三元運算元來取代。 -
一行的情況使用
when x then ...
。替代方案的語法when x: ...
已經在 Ruby 1.9 被移除了。 -
永遠不要使用
when x; ...
。參考前一個規則。 -
使用
!
而不是not
.# 差 - 因為運算子有優先序,需要用括號。 x = (not something) # 好 x = !something
-
布林表達式使用
&&/||
,控制流程使用and/or
。(經驗法則:如果你需要使用外部括號,你正在使用錯誤的運算元。)# 布林表達式 if some_condition && some_other_condition do_something end # 控制流程 document.saved? or document.save!
-
避免多行的
? :
(三元運算元);使用if/unless
來取代。 -
當你有單行的主體時,偏愛
if/unless
修飾符。另一個好的方法是使用控制流程的and/or
。# 不好 if some_condition do_something end # 好 do_something if some_condition # 另一個好方法 some_condition and do_something
-
否定條件偏愛
unless
優於if
(或是控制流程or
)。# 不好 do_something if !some_condition # 好 do_something unless some_condition # 另一個好方法 some_condition or do_something
-
永遠不要使用
unless
搭配else
。 將它們改寫成肯定條件。# 不好 unless success? puts 'failure' else puts 'success' end # 好 if success? puts 'success' else puts 'failure' end
-
不要使用括號圍繞
if/unless/while
的條件式,除非這條件包含了一個賦值(見下面使用=
(一個賦值)的回傳值)。# 不好 if (x > 10) # 省略主體 end # 好 if x > 10 # 省略主體 end # 好 if (x = self.next_value) # 省略主體 end
-
當你有一個單行的主體時,偏愛使用
while/until
修飾子。# bad while some_condition do_something end # good do_something while some_condition
-
負面條件偏愛
until
勝於while
。# bad do_something while !some_condition # good do_something until some_condition
-
忽略圍繞方法參數的括號,如內部 DSL (如:Rake, Rails, RSpec),Ruby 中帶有 "關鍵字" 狀態的方法(如:
attr_reader
,puts
)以及屬性存取方法。所有其他的方法呼叫,使用括號圍繞參數。class Person attr_reader :name, :age # 忽略 end temperance = Person.new('Temperance', 30) temperance.name puts temperance.age x = Math.sin(y) array.delete(e)
-
單行區塊喜好
{...}
勝於do..end
。多行區塊避免使用{...}
(多行串連總是醜陋)。在do...end
、 "控制流程" 及"方法定義",永遠使用do...end
(如 Rakefile 及某些 DSL)。串連時避免使用do...end
。names = ['Bozhidar', 'Steve', 'Sarah'] # 不好 names.each do |name| puts name end # 好 names.each { |name| puts name } # 不好 names.select do |name| name.start_with?('S') end.map { |name| name.upcase } # 好 names.select { |name| name.start_with?('S') }.map { |name| name.upcase }
某些人會爭論多行串連時,使用
{...}
看起來還可以,但他們應該捫心自問 — 這樣程式碼真的可讀嗎?難道不能把區塊內容取出來放到絕妙的方法裡嗎? -
避免在不需要控制流程的場合時使用
return
。# 不好 def some_method(some_arr) return some_arr.size end # 好 def some_method(some_arr) some_arr.size end
-
避免在不需要的情況使用
self
。(只有在呼叫一個 self write 存取器時會需要用到。)# 不好 def ready? if self.last_reviewed_at > self.last_updated_at self.worker.update(self.content, self.options) self.status = :in_progress end self.status == :verified end # 好 def ready? if last_reviewed_at > last_updated_at worker.update(content, options) status = :in_progress end status == :verified end
-
避免使用帶有區域變數的 shadowing 方法,除非它們彼此相等。
class Foo attr_accessor :options # ok def initialize(options) self.options = options # both options and self.options are equivalent here end # 不好 def do_something(options = {}) unless options[:when] == :later output(self.options[:message]) end end # 好 def do_something(params = {}) unless params[:when] == :later output(options[:message]) end end end
-
當賦予預設值給方法參數時,使用空格圍繞
=
運算元。# 不好 def some_method(arg1=:default, arg2=nil, arg3=[]) # 做些事情... end # 好 def some_method(arg1 = :default, arg2 = nil, arg3 = []) # 做些事情... end
然而幾本 Ruby 書建議第一個風格,第二個風格在實踐中更為常見(並可爭議地可讀性更高一點)。
-
避免在不需要的場合使用續行
\
。在實踐中,盡量避免使用續行。# 不好 result = 1 - \ 2 # 好 (但仍然醜的跟地獄一樣) result = 1 \ - 2
-
不要在條件表達式裡使用
=
(賦值)的回傳值。# bad (+ a warning) if (v = array.grep(/foo/)) do_something(v) ... end # bad (+ a warning) if v = array.grep(/foo/) do_something(v) ... end # good v = array.grep(/foo/) if v do_something(v) ... end
-
隨意使用
||=
來初始化變數# 僅在name為nil或false時,把名字設為 Bozhidar。 name ||= 'Bozhidar'
-
不要使用
||=
來初始化布林變數。(想看看如果現在的值剛好是false
時會發生什麼。)# 不好 — 會把 enabled 設成真,即便它本來是假。 enabled ||= true # 好 enabled = true if enabled.nil?
-
避免使用 Perl 風格的特別變數(像是
$0-9
,$
, 等等)。它們看起來非常神祕以及不鼓勵使用一行的腳本。 -
避免在方法名與左括號之間放一個空格。
# 不好 f (3 + 2) + 1 # 好 f(3 + 2) + 1
-
如果方法的第一個參數由左括號開始,永遠在這個方法呼叫裡使用括號。舉個例子,寫
f((3+2) + 1)
。 -
總是使用
-w
來執行 Ruby 直譯器,如果你忘了某個上述的規則,它就會警告你! -
使用新的 lambda 字面語法。
# 不好 lambda = lambda { |a, b| a + b } lambda.call(1, 2) # 好 lambda = ->(a, b) { a + b } lambda.(1, 2)
-
未使用的區塊參數使用
_
。# 不好 result = hash.map { |k, v| v + 1 } # 好 result = hash.map { |_, v| v + 1 }
-
Use
$stdout/$stderr/$stdin
instead ofSTDOUT/STDERR/STDIN
.STDOUT/STDERR/STDIN
are constants, and while you can actually reassign (possibly to redirect some stream) constants in Ruby, you'll get an interpreter warning if you do so. -
Use
warn
instead of$stderr.puts
. Apart from being more concise and clear,warn
allows you to suppress warnings if you need to (by setting the warn level to 0 via-W0
). -
Favor the use of
sprintf
over the fairly crypticString#%
method.# bad '%d %d' % [20, 10] # => '20 10' # good sprintf('%d %d', 20, 10) # => '20 10'
-
Favor the use of
Array#join
over the fairly crypticArray#*
with a string argument.# bad %w(one two three) * ', ' # => 'one, two, three' # good %w(one two three).join(', ') # => 'one, two, three'
-
Use
[*var]
orArray()
instead of explicitArray
check, when dealing with a variable you want to treat as an Array, but you're not certain it's an array.# bad paths = [paths] unless paths.is_a? Array paths.each { |path| do_something(path) } # good [*paths].each { |path| do_something(path) } # good (and a bit more readable) Array(paths).each { |path| do_something(path) }
-
Use ranges instead of complex comparison logic when possible.
# bad do_something if x >= 1000 && x < 2000 # good do_something if (1000...2000).include?(x)
程式設計的真正難題是替事物命名及無效的快取。
-- Phil Karlton
-
識別字用英語命名。
# bad - variable name written in Bulgarian with latin characters zaplata = 1_000 # good salary = 1_000
-
符號、方法與變數使用蛇底式小寫(snake_case)。
# 差 :'some symbol' :SomeSymbol :someSymbol someVar = 5 def someMethod ... end def SomeMethod ... end # 好 :some_symbol def some_method ... end
-
類別與模組使用駝峰式大小寫(CamelCase)。(保留像是 HTTP、RFC、XML 這種縮寫為大寫)
# 差 class Someclass ... end class Some_Class ... end class SomeXml ... end # 好 class SomeClass ... end class SomeXML ... end
-
其他常數使用尖叫蛇底式大寫(SCREAMING_SNAKE_CASE)。
# 差 SomeConst = 5 # 好 SOME_CONST = 5
-
判斷式方法的名字(回傳布林值的方法)應以問號結尾。(即
Array#empty?
) -
有潛在“危險性”的方法,若此 危險 方法有安全版本存在時,應以安全版本名加上驚嘆號結尾(即:改動
self
或參數、exit!
等等方法)。# 不好 - 沒有對應的安全方法 class Person def update! end end # 好 class Person def update end end # 好 class Person def update! end def update end end
-
如果可能的話,從危險方法(bang)的角度來定義對應的安全方法(non-bang)。
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
-
在短的區塊使用
reduce
時,把參數命名為|a, e|
(累加器,元素) -
在定義二元操作符時,把參數命名為
other
(<<
與[]
是這條規則的例外,因為它們的語義不同)。def +(other) # 省略主體 end
-
偏好
map
勝於collect
,find
勝於detect
,select
勝於find_all
,reduce
勝於inject
以及size
勝於length
。這不是一個硬性要求;如果使用別名增加了可讀性,使用它沒關係。這些有押韻的方法名是從 Smalltalk 繼承而來,在別的語言不常見。鼓勵使用select
而不是find_all
的理由是它跟reject
搭配起來是一目了然的。 -
使用
flat_map
勝於使用map
+flatten
的組合。 這並不適用於深度大於 2 的陣列,舉個例子,如果users.first.songs == ['a', ['b', 'c']]
,則使用map + flatten
的組合,而不是使用flat_map
。flat_map
將陣列變平坦一個層級,而flatten
會將整個陣列變平坦。# bad all_songs = users.map(&:songs).flatten.uniq # good all_songs = users.flat_map(&:songs).uniq
良好的程式碼是最佳的文件。當你要加一個註解時,捫心自問,
"如何改善程式碼讓它不需要註解?" 改善程式碼然後記錄下來使它更簡潔。
-- Steve McConnell
-
撰寫自我記錄的程式碼並忽略之後的小節。我是認真的!
-
用英文寫註解。
-
在註解的
#
與註解文字之間使用一個空格。 -
比一個單字長的註解要大寫及使用標點符號。句號後使用一個空格。
-
避免多餘的註解
# 不好 counter += 1 # 把計數器加一
-
保持現有的註解是最新的。過時的註解比沒有註解還差。
好的程式碼就像是好的笑話 -- 它不需要解釋
-- Russ Olsen
- 避免替爛程式碼寫註解。重構程式碼讓它們看起來一目了然。(要嘛就做,要嘛不做 ― 不要只是試試看。-- Yoda)
-
註釋應該直接寫在相關程式碼那行之後。
-
註釋關鍵字後方伴隨著一個冒號及空白,接著一個描述問題的記錄。
-
如果需要用多行來描述問題,之後的行要放在
#
號後面並縮排兩個空白。def bar # FIXME: 這在 v3.2.1 版本之後會異常當掉,或許與 # BarBazUtil 的版本更新有關 baz(:quux) end
-
在問題是顯而易見的情況下,任何的文件會是多餘的,註釋應該要留在可能有問題的那行。這個用法是例外而不是規則。
def bar sleep 100 # OPTIMIZE end
-
使用
TODO
來標記之後應被加入的未實現功能或特色。 -
使用
FIXME
來標記一個需要修復的程式碼。 -
使用
OPTIMIZE
來標記可能影響效能的緩慢或效率低落的程式碼。 -
使用
HACK
來標記代碼異味,其中包含了可疑的編碼實踐以及應該需要重構。 -
使用
REVIEW
來標記任何需要審視及確認正常動作的地方。舉例來說:REVIEW: 我們確定用戶現在是這麼做的嗎?
-
如果你覺得適當的話,使用其他你習慣的註釋關鍵字,但記得把它們記錄在專案的
README
或類似的地方。
-
在類別定義裡使用一致的結構。
class Person # 首先是 extend 與 include extend SomeModule include AnotherModule # 接著是常數 SOME_CONSTANT = 20 # 接下來是屬性巨集 attr_reader :name # 跟著是其它的巨集(如果有的話) validates :name # 公開的類別方法接在下一行 def self.some_method end # 跟著是公開的實體方法 def some_method end # 受保護及私有的方法,一起放在接近結尾的地方 protected def some_protected_method end private def some_private_method end end
-
偏好模組,勝過只有類別方法的類。類別應該只在產生實例是合理的時候使用。
# 差 class SomeClass def self.some_method # body omitted end def self.some_other_method end end # 好 module SomeClass module_function def some_method # body omitted end def some_other_method end end
-
當你想將模組的實例方法變成類別方法時,偏愛使用
module_function
勝過extend self
。# 差 module Utilities extend self def parse_something(string) # do stuff here end def other_utility_method(number, string) # do some more stuff end end # 好 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 代換原則。
-
盡可能讓你的類別越[堅固](http://en.wikipedia.org/wiki/SOLID_(object-oriented_design\))越好。
-
永遠替類別提供一個適當的
to_s
方法來表示領域模型(domain model)。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
這類函數來定義瑣碎的 accessor 或 mutators。# 不好 class Person def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end def first_name @first_name end def last_name @last_name end end # 好 class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end
-
考慮使用
Struct.new
,它替你定義了那些瑣碎的存取器(accessors),建構式(constructor)以及比較運算元(comparison operators)。# 好 class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end # 較佳 Person = Struct.new(:first_name, :last_name) do end
-
考慮加入工廠方法來提供額外合理的方式,來創造一個特定類別的實體。
class Person def self.create(options_hash) # 省略主體 end end
-
偏好鴨子類型勝於繼承。
# 不好 class Animal # 抽象方法 def speak end end # 繼承高層次的類別 (superclass) class Duck < Animal def speak puts 'Quack! Quack' end end # 繼承高層次的類別 (superclass) class Dog < Animal def speak puts 'Bau! Bau!' end end # 好 class Duck def speak puts 'Quack! Quack' end end class Dog def speak puts 'Bau! Bau!' end end
-
由於繼承中 "討厭的" 行為,避免使用類別變數 (
@@
)。class Parent @@class_var = 'parent' def self.print_class_var puts @@class_var end end class Child < Parent @@class_var = 'child' end Parent.print_class_var # => will print "child"
如同你所看到的,在類別階級中的所有類別其實都共享一個類別變數。應該通常偏好使用實體變數而不是類別變數。
-
依據方法的目的用途指定適當的可視層級 (
private
,protected
)。別把所有方法都設為public
(方法的預設值)。我們現在是在寫 Ruby ,不是 Python 。 -
將
public
,protected
,private
和被應用的方法定義保持一致的縮排。在上下各留一行,來強調在這些方法的特性(公有的、受保護的、私有的)。class SomeClass def public_method # ... end private def private_method # ... end def another_private_method # ... end end
-
使用
def self.method
來定義 singleton 方法。由於類別名稱不重複的關係,這使得代碼更容易重構。class TestClass # 不好 def TestClass.some_method # 省略主體 end # 好 def self.some_other_method # 省略主體 end # 也有可能且當你要定義多個 # singleton時的便利方法 class << self def first_method # 省略主體 end def second_method_etc # 省略主體 end end end
-
使用
fail
方法來捕捉異常。僅在捕捉到異常時使用raise
並重新拋出異常(因為沒有失敗,所以顯式地拋出異常)begin fail 'Oops'; rescue => error raise if error.message != 'Oops' end
-
永遠不要從
ensure
區塊返回。如果你顯式地從ensure
區塊中的一個方法返回,那麼這方法會如同沒有異常般的返回。實際上,異常會被默默地丟掉。def foo begin fail ensure return 'very bad idea' end end
-
盡可能使用隱式的
begin
區塊。# 不好 def foo begin # main logic goes here rescue # failure handling goes here end end # 好 def foo # main logic goes here rescue # failure handling goes here end
-
透過 contingency 方法 (一個由 Avdi Grimm 創造的詞)來減少
begin
區塊的使用。# 不好 begin something_that_might_fail rescue IOError # handle IOError end begin something_else_that_might_fail rescue IOError # handle IOError end # 好 def with_io_error_handling yield rescue # handle IOError end with_io_error_handling { something_that_might_fail } with_io_error_handling { something_else_that_might_fail }
-
不要封鎖異常。
begin # 這裡發生了一個異常 rescue SomeError # 救援子句完全沒有做事 end # bad do_something rescue nil
-
避免在 modifier 形式裡使用
rescue
。# 差勁 - 這捕捉了所有的 StandardError 異常。 do_something rescue nil
-
不要為了控制流程而使用異常。
# 不好 begin n / d rescue ZeroDivisionError puts 'Cannot divide by 0!' end # 好 if d.zero? puts 'Cannot divide by 0!' else n / d end
-
避免救援
Exception
類別。這會把信號困住,並呼叫exit
,導致你需要kill -9
進程。# 不好 begin # 呼叫 exit 及殺掉信號會被捕捉(除了 kill -9) exit rescue Exception puts "you didn't really want to exit, right?" # 異常處理 end # 好 begin # 從 StandardError 中救援一個救援子句, # 不是許多程式設計師所假定的異常。 rescue => e # 異常處理 end # 也很好 begin # 這裡發生一個異常 rescue StandardError => e # 異常處理 end
-
把較具體的異常放在救援串連的較上層,不然它們永遠不會被救援。
# 不好 begin # 一些程式碼 rescue Exception => e # 一些處理 rescue StandardError => e # 一些處理 end # 好 begin # 一些程式碼 rescue StandardError => e # 一些處理 rescue Exception => e # 一些處理 end
-
在 ensure 區塊中釋放你程式的外部資源。
f = File.open('testfile') begin # .. 處理 rescue # .. 錯誤處理 ensure f.close unless f.nil? end
-
偏愛使用標準函式庫的異常處理勝於導入新的異常類別。
-
偏好陣列及雜湊的字面表示法(除非你需要給建構子傳入參數)。
# 不好 arr = Array.new hash = Hash.new # 好 arr = [] hash = {}
-
Prefer
%w
to the literal array syntax when you need an array of words(non-empty strings without spaces and special characters in them). Apply this rule only to arrays with two or more elements.# 不好 STATES = ['draft', 'open', 'closed'] # 好 STATES = %w(draft open closed)
-
Prefer
%i
to the literal array syntax when you need an array of symbols(and you don't need to maintain Ruby 1.9 compatibility). Apply this rule only to arrays with two or more elements.# bad STATES = [:draft, :open, :closed] # good STATES = %i(draft open closed)
-
避免在陣列中創造巨大的間隔。
arr = [] arr[100] = 1 # 現在你有一個很多 nil 的陣列
-
當處理獨一無二的元素時,使用
Set
來替代Array
。Set
實現了不重複的無序數值集合。Set
是陣列直觀的內部操作功能與雜湊的快速存取的混合體。 -
偏好用符號取代字串作為雜湊的鍵。
# 不好 hash = { 'one' => 1, 'two' => 2, 'three' => 3 } # 好 hash = { one: 1, two: 2, three: 3 }
-
在處理需要出現的雜湊鍵時,使用
fetch
。heroes = { 蝙蝠俠: 'Bruce Wayne', 超人: 'Clark Kent' } # 差勁 - 如果我們打錯字的話,我們就無法找到對的英雄了 heroes[:蝙蝠俠] # => "Bruce Wayne" heroes[:超女] # => nil # 棒 - fetch 會拋出一個 KeyError 來體現這個問題 heroes.fetch(:supermann)
-
避免使用可變的物件作為鍵值。
-
當你的 hash 鍵為符號時,使用雜湊的字面語法。
# 不好 hash = { :one => 1, :two => 2, :three => 3 } # 好 hash = { one: 1, two: 2, three: 3 }
-
Use
fetch
when dealing with hash keys that should be present.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)
-
Use
fetch
with second argument to set a default valuebatman = { name: 'Bruce Wayne', is_evil: false } # bad - if we just use || operator with falsy value we won't get the expected result batman[:is_evil] || true # => true # good - fetch work correctly with falsy values batman.fetch(:is_evil, true) # => false
-
相信這個事實吧,Ruby 1.9 的雜湊是有序的。
-
在遍歷一個集合時,不要改動它。
-
偏好字串插值 (interpolation),而不是字串串接 (concatenation)。
# 不好 email_with_name = user.name + ' <' + user.email + '>' # 好 email_with_name = "#{user.name} <#{user.email}>"
-
考慮替字串插值留白。這使插值在字串裡看起來更清楚。
"#{ user.last_name }, #{ user.first_name }"
-
當你不需要插入特殊符號如
\t
,\n
,'
, 等等時,偏好單引號的字串。# 不好 name = 'Bozhidar' # 好 name = 'Bozhidar'
-
別忘了使用
{}
圍繞要被插入字串的實體與全域變數。class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end # 差 – 有效,但難看 def to_s "#@first_name #@last_name" end # 好 def to_s "#{@first_name} #{@last_name}" end end $global = 0 # 差 puts "$global = #$global" # 好 puts "$global = #{$global}"
-
當你需要建構龐大的資料區段(chunk)時,避免使用
String#+
。 使用String#<<
來替代。字串串接在對的地方改變字串實體,並且永遠比String#+
來得快,String#+
創造了一堆新的字串物件。# 好也比較快 html = '' html << '<h1>Page title</h1>' paragraphs.each do |paragraph| html << "<p>#{paragraph}</p>" end
有些人在面對問題時,不經大腦便認為,「我知道,這裡該用正規表示法」。現在問題反倒變成兩個了。
-- Jamie Zawinski
-
如果你只需要在字串中簡單的搜索文字,不要使用正規表示法:
string['text']
-
針對簡單的字串查詢,你可以直接在字串索引中直接使用正規表示法。
match = string[/regexp/] # 獲得匹配正規表示法的內容 first_group = string[/text(grp)/, 1] # 或得分組的內容 string[/text (grp)/, 1] = 'replace' # string => 'text replace'
-
當你不需要替結果分組時,使用非分組的群組。
/(first|second)/ # 不好 /(?:first|second)/ # 好
-
避免使用
$1-9
,因為它們很難追蹤它們包含什麼。可以使用命名群組來替代。# 不好 /(regexp)/ =~ string ... process $1 # 好 /(?<meaningful_var>regexp)/ =~ string ... process meaningful_var
-
字元類別只有幾個你需要關心的特殊字元:
^
,-
,\
,]
,所以你不用逃脫字元.
或在[]
的中括號。 -
小心使用
^
與$
,它們匹配的是一行的開始與結束,不是字串的開始與結束。如果你想要匹配整個字串,使用\A
與\z
。(譯註:\Z
實為/\n?\z/
,使用\z
才能匹配到有含新行的字串的結束)string = "some injection\nusername" string[/^username$/] # 匹配 string[/\Ausername\z/] # 無匹配
-
針對複雜的正規表示法,使用
x
修飾符。這讓它們的可讀性更高並且你可以加入有用的註解。只是要小心忽略的空白。regexp = %r{ start # 一些文字 \s # 空白字元 (group) # 第一組 (?:alt1|alt2) # 一些替代方案 end }x
-
針對複雜的替換,
sub
或gsub
可以與區塊或雜湊來使用。
-
使用
%()
給需要插值與嵌入雙引號的單行字串。多行字串,偏好使用 heredocs 。# 不好(不需要插值) %(<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>) # 應該是一個 heredoc # 好(需要插值、有雙引號以及單行) %(<tr><td class="name">#{name}</td>)
-
正規表示法要匹配多於一個的
/
字元時,使用%r
。# 不好 %r(\s+) # 仍不好 %r(^/(.*)$) # 應當是 /^\/(.*)$/ # 好 %r(^/blog/2011/(.*)$)
-
避免
%q
,%Q
,%x
,%s
以及%W
。 -
偏好
()
作為所有%
字面的分隔符。
-
避免無謂的元程式設計。
-
寫一個函式庫時不要在核心類別搗亂(不要替它們加 monkey patch。)
-
偏好區塊形式的
class_eval
勝於字串插值 (string-interpolated)的形式。-
當你使用字串插值形式時,總是提供
__FILE__
及__LINE__
,使你的 backtrace 看起來有意義:class_eval "def use_relative_model_naming?; true; end", __FILE__, __LINE__
-
偏好
define_method
勝於class_eval{ def ... }
-
-
當使用
class_eval
(或其它的eval
)搭配字串插值時,添加一個註解區塊,來顯示如果做了插值的樣子(我從 Rails 程式碼學來的一個實踐):# 從 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}(*args, &block) # def capitalize(*args, &block) to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block) end # end def #{unsafe_method}!(*args) # def capitalize!(*args) @dirty = true # @dirty = true super # super end # end EOT end end
-
元程式設計避免使用
method_missing
。會讓 Backtraces 變得很凌亂;行為沒有列在#methods
裡;拼錯的方法呼叫可能默默的工作(nukes.launch_state = false
)。考慮使用 delegation, proxy, 或是define_method
來取代。如果你必須使用method_missing
,-
僅捕捉字首定義良好的方法,像是
find_by_*
― 讓你的程式碼愈肯定(assertive)愈好。 -
在最後的敘述句(statement)呼叫
super
-
從 delegate 到 assertive, 不神奇的(non-magical)方法:
# 不好 def method_missing?(meth, *args, &block) if /^find_by_(?<prop>.*)/ =~ meth # ... lots of code to do a find_by else super end end # 好 def method_missing?(meth, *args, &block) if /^find_by_(?<prop>.*)/ =~ meth find_by(prop, *args, &block) else super end end # 而最好是在每個可找到的屬性被宣告時,使用 define_method。
-
ruby -w
寫出安全的程式碼。 -
避免使用雜湊作為選擇性參數。這個方法是不是做太多事情了?(物件初始器是本規則的例外)
-
避免方法長於 10 行程式碼(LOC)。理想上,大部分的方法會小於5行。空行不算進 LOC 裡。
-
避免參數列表長於三或四個參數。
-
如果你真的需要全域方法,把它們加入到 Kernel 並設為私有的。
-
使用模組變數取代全域變數。
# 不好 $foo_bar = 1 # 好 module Foo class << self attr_accessor :bar end end Foo.bar = 1
-
當
alias_method
可以做到時,避免使用alias
。 -
使用
OptionParser
來解析複雜的命令行選項及ruby -s
來處理瑣碎的命令行選項。 -
用函數式的方法寫程式,在有意義的情況下避免賦值。
-
不要變動參數,除非那是方法的目的。
-
避免超過三行的巢狀區塊。
-
保持一致性。在理想的世界裡,遵循這些準則。
-
使用常識。
以下是一些工具,讓你自動檢查 Ruby 程式碼是否符合本指南。
RuboCop is a Ruby code style checker based on this style guide. RuboCop already covers a significant portion of the Guide, supports both MRI 1.9 and MRI 2.0 and has good Emacs integration.
RubyMine's code inspections are partially based on this guide.
在本指南所寫的每個東西都不是定案。這只是我渴望想與同樣對 Ruby 程式設計風格有興趣的大家一起工作,以致於最終我們可以替整個 Ruby 社群創造一個有益的資源。
歡迎開票或發送一個帶有改進的更新請求。在此提前感謝你的幫助!
This work is licensed under a Creative Commons Attribution 3.0 Unported License
一份社群策動的風格指南,對一個社群來說,只是讓人知道有這個社群。推特這個指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們想要擁有的是最好的指南,不是嗎?
共勉之,
Bozhidar