Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyスタイルガイドを読む: コレクション(Array、Hash、Setなど)

こんにちは、hachi8833です。Rubyスタイルガイドを読むシリーズの「コレクション」編は配列やハッシュのスタイルや利用法を扱います。

コレクション

7-01【統一】配列やハッシュの作成は[]{}によるリテラル表記が望ましい

Prefer literal array and hash creation notation (unless you need to pass parameters to their constructors, that is).

Array#newHash.newによる作成は、コンストラクタにパラメータを渡す必要がある場合のみとします。タイプ量が少ないという点も含めて、リテラル表記に揃えるスタイルにしたということですね。

# 不可
arr = Array.new
hash = Hash.new

# 良好
arr = []
hash = {}

7-02【統一】配列のリテラル表記で語の配列が必要な場合は%wが望ましい

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.

スペースや特殊文字を含まない限りは%wが推奨されます。

# 不可
STATES = ['draft', 'open', 'closed']

# 良好
STATES = %w[draft open closed]

ただし要素が1つしかない場合はこの限りではありません。

STATES = %w[draft] # これは冗長
STATES = ['draft'] # これはOK

要素が1つしかない場合に%wを使うと語のリストではないものに見えてしまいそうですし、タイプ量的にもメリットがないからと思われます。

参考: %のリテラル表記の囲みは1種類ではない

Rubyでは、%w%iといったリテラル表記の囲みに[ ]以外も使えます。開始記号と終了記号が一貫していればよいので、以下のような書き方もできます。

%w[draft open closed]
%w(draft open closed)
%w|draft open closed|
%w<draft open closed>

実際のコードでは( )を使うことも多いようです。プロジェクトで統一しておけばよいでしょう。

  • Rubyリファレンスマニュアル: %記法

7-03【統一】配列のリテラル表記でシンボルの配列が必要な場合は%iが望ましい

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.

%iはRuby 1.9以前では利用できないので、後方互換性の必要なコードはこの限りではありません。

# 不可
STATES = [:draft, :open, :closed]

# 良好
STATES = %i[draft open closed]

語の場合と同様、要素が1つしかない場合もこの限りではありません。

STATES = %i[draft] これは冗長
STATES = [:draft] これはOK

7-04【統一】配列のリテラルやハッシュのリテラルで最後の項目直後のカンマは避ける

Avoid comma after the last item of an Array or Hash literal, especially when the items are not on separate lines.

特に、項目が行で区切られずに1行の中で書かれている場合はカンマを置かないようにします。

# 不可 - 項目の移動/追加/削除がやりやすいのは認めるが、それでも推奨しない
VALUES = [
           1001,
           2020,
           3333,
         ]

# 不可
VALUES = [1001, 2020, 3333, ]

# 良好
VALUES = [1001, 2020, 3333]

最終項目にカンマを置かないスタイルはRubyスタイルガイドを読む: ソースコードレイアウト(2)インデント、記号でも言及されていました。
これは良し悪しの問題というよりスタイル統一のためと理解しました。

7-05【統一】大きな配列を途中から作成することは避ける

Avoid the creation of huge gaps in arrays.

arr = []
arr[100] = 1 # 99より前の配列がすべてnilで埋まってしまう

コメントのとおりですね。

7-06【統一】配列の最初の要素には#first、最後の要素には#lastでアクセスするのが望ましい

When accessing the first or last element from an array, prefer first or last over [0] or [-1].

Rubyでは全般に、数値や文字列をコード内でなるべく直接書かないスタイルが好まれます。Rubyに限りませんが、コード内でリテラルを書き散らすと一括変更やリファクタリングが面倒になるので、リテラルはなるべく1箇所にまとめるのが常道です。

このスタイルガイドでも、こうした操作をメソッド呼び出しで行うことを全般に推奨していますが、そうすることで自然とコード内リテラルを減らせるという意図があると思います。

7-07【統一】要素を重複させたくない場合はArrayではなくSetを利用する

Use Set instead of Array when dealing with unique elements.
Set implements a collection of unordered values with no duplicates.
This is a hybrid of Array's intuitive inter-operation facilities and Hash's fast lookup.

Setクラスは、重複要素を持てない点を除けばArrayと少し似ています。Arrayの直感的な操作と、Hashの高速参照を合わせ持つような感じです。

このSetは、もちろん集合論(set theory)のsetですね。利用にはrequire 'set'が必要です。

SetクラスはEnumerableをincludeしているので#eachなどの操作はひととおり行えます。SetクラスはObjectクラスの直下であり、Arrayを継承していませんので、操作方法はArrayと同じでないことがあります。

require 'set'
s1 = Set.new [1, 2]                   # -> #<Set: {1, 2}>
s2 = [1, 2].to_set                    # -> #<Set: {1, 2}>
s1 == s2                              # -> true
s1.add('foo')                         # -> #<Set: {1, 2, "foo"}>
s1.merge([2, 6])                      # -> #<Set: {1, 2, "foo", 6}>
s1.subset? s2                         # -> false
s2.subset? s1                         # -> true

Setの要素は数字以外のものであっても順序が保存されていますね。

7-08【統一】ハッシュのキーは文字列よりもシンボルが望ましい

Prefer symbols instead of strings as hash keys.

Rubyのシンボルは文字列よりも高速でメモリ効率が高く、かつタイプ量も少なくて表記上の意味も明確になるので、積極的に利用したいですね。

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

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

同じシンボルでも、コード中の文脈によってコロンの位置が変わります。それさえ理解すればシンボルは問題なく使いこなせると思います。

hash = { one: 1, two: 2, three: 3 } # ハッシュのキー表記ではコロンが後ろになる
hash[:one] = 1.0                    # その他はコロンが前になる

def my_method(arg1: arg:2)          # キーワード引数(シンボルではない)
  pp arg1, arg2
end

シンボルは、ハッシュのキーとしての利用やキーワード引数の表記と整合しているのが設計としてうまいと思います。私だけかもしれませんが、他の言語を使っているとRubyのシンボルがとても欲しくなります。

以下のように引用符で囲むことで、スペースやハイフンを含むシンボルを無理やり作ることもできます。

:'hello world'
#=> :"hello world"
:'hello-world'
#=> :"hello-world"

なお、数値や数値で始まっていると、引用符なしではシンボルにできません。

:1
#=> SyntaxError: unexpected tINTEGER, expecting tSTRING_CONTENT or tSTRING_DBEG or tSTRING_DVAR or tSTRING_END

:1.000
#=> SyntaxError: unexpected tFLOAT, expecting tSTRING_CONTENT or tSTRING_DBEG or tSTRING_DVAR or tSTRING_END

7-09【統一】ミュータブルなオブジェクトをハッシュのキーにすることは避ける

Avoid the use of mutable objects as hash keys.

ミュータブル(mutable)なオブジェクトをハッシュのキーにすると、ハッシュの意味やメリットが失われてしまいますし、最悪の場合正常に機能しなくなってしまいます。

原文にサンプルコードがないので作ってみました。

a, b = 1, 2
hash = { a => 'hello', b => 'world' }
a = 2
hash[a]
#=> 'world' (aが指す内容が変わってしまった)

シンボルは不変(immutable)なので、ハッシュのキーとして理想です。

7-10【統一】ハッシュのキーがシンボルならシンボル:で表記する

Use the Ruby 1.9 hash literal syntax when your hash keys are symbols.

このハッシュリテラル記法はRuby 1.9で導入されました。旧来の=>記法はロケット演算子などと呼ばれたりします。

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

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

7-11【統一】Ruby 1.9記法とロケット演算子記法を同一ハッシュリテラル内で混ぜないこと

Don't mix the Ruby 1.9 hash syntax with hash rockets in the same hash literal.
When you've got keys that are not symbols stick to the hash rockets syntax.

キーがシンボルでない場合はロケット演算子で統一すべきとのことです。

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

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

7-12【統一】ハッシュではHash#key?Hash#value?を使うこと

  • Use Hash#key? instead of Hash#has_key? and Hash#value? instead of
    Hash#has_value?.

動詞含みのHash#has_key?Hash#has_value?は推奨されません。

# 不可
hash.has_key?(:test)
hash.has_value?(value)

# 良好
hash.key?(:test)
hash.value?(value)

7-13【統一】ハッシュではHash#each_keyHash#values_eachを使うこと

Use Hash#each_key instead of Hash#keys.each and Hash#each_value
instead of Hash#values.each.

Hash#keys.eachHash#values.eachは推奨されません。

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

# 良好
hash.each_key { |k| p k }
hash.each_value { |v| p v }

このスタイルの理由が書かれていませんが、morimorihogeさんから「hash.keys.eachhash.values.each だと一度#keysや#valuesを通してArrayオブジェクトが生成されてしまう」と教えていただきました。

7-14【統一】ハッシュのキーの存在を前提にする場合はHash#fetchを使うこと

通常の[]でハッシュにアクセスすると、無効なキーではnilが返ります。

Use Hash#fetch when dealing with hash keys that should be present.

heroes = { batman: 'Bruce Wayne', superman: 'Clark Kent' }
# 不可 - キーが無効でもエラーにならない
heroes[:batman] # => 'Bruce Wayne'
heroes[:supermann] # => nil

# 良好 - KeyErrorでキーがないことを検出できる
heroes.fetch(:supermann)

7-15【統一】ハッシュ値のデフォルト値が欲しい場合はHash#fetchでデフォルト値を与えること

Introduce default values for hash keys via Hash#fetch as opposed to using custom logic.

Hash#fetchなら外部のロジックを使わなくてもハッシュ値にデフォルト値を設定できます。コード例にもあるように、||演算子だと正常に処理できないことがあります。

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

# 不可 - ||演算子ではハッシュの値がfalseと同値の場合に正しく処理されない
batman[:is_evil] || true # => true

# 良好 - ハッシュの値がfalseと同値であっても正常に処理できる
batman.fetch(:is_evil, true) # => false

7-16【ヒント】Hash#fetchで取る値を評価するコードに副作用がある場合やコストが高い場合は、コードをデフォルト値で与えるよりもコードをブロックで与える方が望ましい

Prefer the use of the block instead of the default value in Hash#fetch if the code that has to be evaluated may have side effects or be expensive.

スタイルというよりは、Hash#fetchの効果的な使い方の指示ですね。

batman = { name: 'Bruce Wayne' }

# 不可 - `Hash#fetch`のデフォルト値にコードを設定すると毎回評価されてしまう
#       このため繰り返しが多いと速度が低下する
batman.fetch(:powers, obtain_batman_powers) # obtain_batman_powersでは重たい処理を行うとする

# 良好 - ブロックで与えたコードは遅延評価されるので、KeyError発生時以外は評価されない
batman.fetch(:powers) { obtain_batman_powers }

7-17【統一】ハッシュから複数の値を一度に取り出す場合はHash#values_atを使うこと

Use Hash#values_at when you need to retrieve several values consecutively from a hash.

これもハッシュの効果的な使い方の指示ですね。なお、配列にも同様のArray#values_atメソッドがあります。

# 不可
email = data['email']
username = data['nickname']

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

7-18【統一】ハッシュの要素の順序が保持されることを前提としてコードを書くこと

Rely on the fact that as of Ruby 1.9 hashes are ordered.

一般にハッシュは要素の順序を保証しないものであり、以前のRubyもそのように実装されていましたが、Ruby 1.9からはハッシュの要素の順序が保存されるようになりました。

7-19【統一】コレクションの全要素をスキャン中に要素を変更しないこと

Do not modify a collection while traversing it.

コレクションのスキャン中の要素変更は事故りやすいですね。

参考: traverseについて

traverseは登山用語でも見かけますね。最短の急な登攀ルートではなく、左右に進路を変えて緩やかに登攀する手法を指します。距離で損する代わりに角度が緩くなるので登山中の安全性が高まります。山岳鉄道の線路でよく使われるスイッチバックと同じ考え方ですね。

この登攀ルートでは斜面全体をなめるように進むので、コンピュータ方面での「ツリーのノードを隅々までスキャンする」という意味に通じます。セキュリティ情報でもスキャンの意味でときどきtraverseという語が登場しますね。


trekandmountain.comより

ついでながら、u-ichiさんから「NATトラバーサルというのもありますね」と教わりました。通称「NAT越え」だそうです。

7-20【統一】コレクションにはできるだけ[n]以外の読み出しメソッドでアクセスすること

When accessing elements of a collection, avoid direct access via [n] by using an alternate form of the reader method if it is supplied. This guards you from calling [] on nil.

[]でコレクションにアクセスすると、nilで呼び出そうとした場合の処理が別途必要になるので、nilに対応できるメソッドが推奨されます。

# 不可
Regexp.last_match[1]

# 良好
Regexp.last_match(1)

7-21【統一】コレクションにアクセサを実装する場合はnilチェックすること

When providing an accessor for a collection, provide an alternate form to save users from checking for nil before accessing an element in the collection.

コレクションにnilでアクセスしないよう、メソッド側で対応しましょう。

# 不可
def awesome_things
  @awesome_things
end

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

今回は以上です。次回「数値・文字列編」にご期待ください。

関連記事



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。