Rubyに身体を慣らす
Rubyは何を真と考えているかを正確に理解しよう
- false, nil以外の全ての値は真である。
- 多くの言語とは異なり、Rubyでは、数値ゼロは真である。
- falseとnilを区別しなければならない時には、
nil?
メソッドを使うか、falseを左被演算子とする==
演算子を使おう。
オブジェクト扱うときにはnilかもしれないということを忘れないようにしよう
- Rubyの型システムの構造上、すべてのオブジェクトがnilになり得る。
-
nil?
メソッドは、レシーバがnilならtrue、そうでなければtrueを返す。 - 適切なら、
to_s
、to_i
などの変換メソッドを使ってnilオブジェクトを強制的に型変換しよう。 -
Array#compact
メソッドは、レシーバのコピーからすべてのnil要素を取り除いた形のものを返す。
Rubyの記号めいたPerl風機能を避けよう
-
String#=~
ではなく、String#match
を使おう。String#match
なら、複数の特殊グローバル変数ではなく、MatchDataオブジェクトにすべてのマッチ情報を返す。 - 短い暗号めいた名前のグローバル変数ではなく、長くて意味のわかる別名の方を使おう。
- ほとんどの長い名前は、Englishライブラリをロードしなければ使えない
- 暗黙のうちに$_グローバル変数を読み書きするメソッドを使うのを避けよう
-
Kernel#print
,Regexp#~
, ... - コマンドライン上で使われるワンライナーという、短くて単純なスクリプトを書く時のために上記メソッドは存在している。
-
定数がミュータブルなことに注意しよう
- 定数は書き換えられないようにするために必ずフリーズしよう。
- 定数が配列やハッシュなどのコレクションオブジェクトを参照する場合、コレクションとその要素をフリーズしよう。
- 配列をフリーズした場合、配列自体はフリーズされているが、その要素は依然としてミュータブル。
- 既存の定数に新しい値が代入されるのを防ぐためには、定数が定義されているモジュールをフリーズしよう。
- 定数が参照するオブジェクトをフリーズしても、既存の定数に新しい値を代入することができてしまう。
実行時の警告に注意しよう。
- Rubyのインタープリタのコマンドラインオプション
-w
を使ってコンパイル時、実行時警告を有効にしよう。- 構文解析時にコードについての警告を生成できる。
-
-w
オプションを付けられない場合は、RUBYOPT環境変数に-w
を設定することでもできる。
- 実行時警告を無効にしなければならない場合には、$VERBOSEグローバル変数に一時的にnilをセットしよう。
クラス、オブジェクト、モジュール
Rubyが継承階層をどのように組み立てるかを頭に入れよう
- Rubyは、クラス階層をサーチするだけでメソッドを見つけられる。探しているメソッドが見つからない時には、method_missingを探して再びサーチを開始する。
- モジュールをインクルードすると、暗黙のうちに特異クラスが作られ、その特異クラスはクラス階層のインクルードしたクラスの上に挿入される。
- 特異メソッドはクラス階層に挿入される特異クラスに格納される。
superのふるまいが一通りではないことに注意しよう
- 継承階層の上位に有るメソッドをオーバーライドするときには、superキーワードを使ってオーバーライドされるメソッドを呼び出すことができる。
- 引数もかっこも付けずにsuperを呼び出すと、呼び出し元のメソッドに渡されたすべての引数を渡してオーバーライドされるメソッドを呼び出すのと同じ意味になる。
- オーバーライドされるメソッドに引数を渡さずにsuperを使いたい場合には、super()のように空のかっこを使わなければならない。
- method_missingメソッドを定義してしまうと、super呼び出しが失敗した時に得られる便利な情報が失われてしまう。
サブクラスを初期化するときにはsuperを呼びだそう
- Rubyは、サブクラスのオブジェクトを作るときに、スーパークラスのinitializeメソッドを自動的に呼び出したりはしない。initializeにも通常のメソッドルックアップの規則が適用され、最初に見つかったバーションが実行される。
- 明示的に継承を使うクラスでinitializeメソッドを書くときには、superを使って親クラスを初期化しなければならない。initialize_copyメソッドを定義するときにも、同じ規則が当てはまる。
Rubyの最悪に紛らわしい構文に注意しよう
- セッターメソッドは、明示的にレシーバを指定しなければ呼び出せない。レシーバがなければ、変数への代入と解釈されてしまう。
- インスタンスメソッドからセッターメソッドを呼び出すときには、レシーバとしてselfを使う。
- セッター以外のメソッドを呼び出すときには、明示的にレシーバを指定する必要はない。selfでコードを汚さないようにしよう。
class Counter
attr_accessor(:counter)
def initialize
self.counter = 0
end
end
構造化データの表現にはHashではなくStructを使おう
- 新しいクラスを作るほどでもない構造化データを扱うときには、HashではなくStructを使うようにしよう。
- Struct::newの戻り値を定数に代入し、その定数をクラスのように扱おう。
Reading = Struct.new(:date, :high, :low) do
def mean
...
end
end
モジュールにコードをネストして名前空間を作ろう
- モジュール内に定義をネストして名前空間を作ろう。
- 名前空間の構造は、ディレクトリ構造と同じにしよう。
- トップレベル定数を修飾なしで使うと曖昧になるときには、
::
を使ってフルに修飾しよう (e.g. ::Array)
さまざまな等値の違いを理解しよう
-
equal?
メソッドをオーバーライドしてはならない。このメソッドは、厳格にオブジェクトを比較し、両方がメモリ内の同じオブジェクトを指すポインタでない限りtrueを返さないという動作をすべきものと考えれられている。 - Hashクラスは、衝突が起きたときに、キーとして使われているオブジェクトを比較するために
eql?
メソッドを使っている。デフォルト実装は、おそらく望ましい動作をしない。eql?
を==
の別名にして、適切なhashメソッドを書こう。 - 2つのオブジェクトが同じ値を表すかどうかをテストするときには
==
演算子を使う。数値を表すクラスなど、一部のクラスは型変換を実行する緩やかな等値演算子を持っている。 - case式は、個々のwhen節をテストするために
==
演算子を使っている。左被演算子はwhenに与えられる引数、右被演算子はcaseに与えられる引数である。
<=>
とComparableモジュールで比較を実装しよう
- オブジェクトの順序は、
<=>
演算子を定義し、Comparableモジュールをインクルードして実装しよう。 -
<=>
演算子は、左被演算子が右被演算子と比較できないものならnilを返す。 - クラスのために
<=>
を実装した場合、特にインスタンスをハッシュキーとして使うつもりなら、eql?
を==
の別名にすることを検討しよう。別名にする場合には、hashメソッドもオーバーライドしなければならない。
protectedメソッドを使ってプライベートな状態を共有しよう
- プライベートな状態はprotectedメソッドで共有する。
- protectdメソッドは、関連するクラスの間でプライベートな情報を共有するために作られている。呼び出し元の継承階層にあるオブジェクトをレシーバとして指定できる。
- privateメソッドに加えられている制限は、明示的なレシーバを指定して呼び出すことはできないということ。
- レシーバーを明示してprotectedメソッドを呼び出せるのは、同じクラスのオブジェクトか共通のスーパークラスからprotectedメソッドを継承しているオブジェクトだけだ。
クラス変数よりもインスタンス変数を使うようにしよう
- クラス変数よりもクラスインスタンス変数を使うようにしよう。
- クラス変数を用いるのは、グローバル変数を用いるのと同じ。クラスインスタンス変数を用いることでカプセル化しよう。
- クラスはオブジェクトなので、専用のプライベートなインスタンス変数セットを持っている。
コレクション
コレクションを書き換える前に引数として渡すコレクションのコピーを作っておこう
- Rubyのメソッド引数は値渡しではなく参照渡しである。ただし、この規則には、Fixnumオブジェクトという顕著な例外がある。
- 引数として渡されたコレクションは、書き換える前にコピーを作ろう。
-
clone
、dup
メソッドは、シャローコピーしか作らない。-
clone
: オリジナルのオブジェクトの状態を残す。 -
dup
: オリジナルのオブジェクトの状態を残さない。フリーズされた状態、特異メソッド・クラスをコピーしない。 - 殆どの場合は、オブジェクトを書き換えることができるため、
dup
メソッドを使った方が良い。 - ただしどちらもシャローコピーであるため、コンテナのコピーは作られるが、要素のコピーは作られないため、要素を書き換えると外部からもそれが見えてしまう。
-
- ほとんどのオブジェクトでは、Marshalを使えば必要なときにディープコピーを作れる。
- メモリを大食いし、また全てのオブジェクトがシリアライズできるとは限らない (IO, Fileクラスなど)
- オブジェクトのコピーを作るためにMarshalが必要になることは少ないが、ディープコピーが必要な場合に使う。
コピーを作る。
class Tuner
def initialize (presets)
@presets = presets.dup
clean # コピーを書き換える
end
end
Marshalを用いてディープコピーを作る。
a = ["Monkey", "Brains"]
b = Marshal.load(Marshal.dump(a))
nil、スカラーオブジェクトを配列に変換するには、Arrayメソッドを使おう
- nilとスカラーオブジェクトを配列に変換するには、Arrayメソッドを使おう。
- ArrayメソッドにHashを渡してはならない。Hashは一連のネストされた配列に変換されてしまう。
要素が含まれているかどうかの処理を効率よく行うために集合を使うことを検討しよう
- 要素が含まれているかどうかの高速チェックではSetを使うことを検討しよう。
- Setは集合を扱うのに便利なコレクションクラス。
- Setに挿入されるオブジェクトは、ハッシュキーとしても使えなければならない。
- Setを使う前に、
requiere('set')
を実行しよう。
require('set')
class Role
def initialize (name, permissions)
@name, @permissions = name, Set.new(permissions)
end
def can? (permission)
@permissions.include?(permission)
end
end
reduceを使ってコレクションを畳み込む方法を身に付けよう
- アキュムレータの初期値は必ず使おう。
- reduceのブロックは、かならずアキュムレータを返すようにする。現在のアキュムレータを書き換えるのは問題ないが、それをブロックから返すのを忘れない。
users.reduce([]) do |names, user|
names << user.name if user.age >= 21
names
end
ハッシュのデフォルト値を利用することを検討しよう
- Hashのデフォルト値を利用することを検討しよう。
-
hash[element] ||= 0
のようなコードが多く有るのは、デフォルト値としてnilが使われているため。
-
- ハッシュがキーを含んでいるかどうかをチェックするときには、
has_key?
またはその別名を使おう。つまり、存在しないキーにアクセスしたらnilが返されることを前提としてコードを書いてはならない。 - 無効なキーを渡すとnilを返すことを前提としているコードにハッシュを渡さなければならないときには、デフォルト値を使ってはならない。
- デフォルト値を使うよりも
Hash#fetch
を使った方が安全な場合がある。
h1 = Hash.new(0)
h2 = Hash.new([])
h3 = Hash.new { [] } # 存在しないキーでアクセスしたときに新しい配列を返す
コレクションクラスからの継承よりも委譲を使うようにしよう
- コレクションクラスからの継承よりも委譲を使うようにしよう。
- コアクラスからの継承よりも委譲を使うようにしよう。
- 委譲のターゲットのコピーを作る
initialize_copy
メソッドを忘れずに書こう。 - 委譲ターゲットに対応するメッセージを送ってからsuperを呼び出すという形で
freeze
、taint
、untaint
メソッドを書こう。
LikeArray#reverseはLikeArrayではなくArrayを返してしまう。
class LikeArray < Array; end
x = LikeArray.new([1, 2, 3])
y = x.reverse
y.class # --> Array
インスタンス変数に対して委譲を使う。
require('forwardable')
class RaisingHash
extend(Forwardable)
include(Enumrable)
# 存在しないキーにアクセスされたときの対策
def initialize
@hash = Hash.new do |hash, key|
raise(KeyError, "invalid key '#{key}'!")
end
end
# dup, cloneが正しく動作するようにする
def initialize_copy (other)
@hash = @hash.dup
end
# freeze, taint, untaintメソッドそれぞれ同じように動作するようにする
def freeze
@hash.freeze
super
end
# 移譲
def_delegators(:@hash, :[], :[]=, :delete, :each, :keys, :values, :length, :empty?, :has_key?)
end
例外
raiseにはただの文字列ではなくカスタム例外を渡そう
- 例外としてraiseに文字列を渡すのは避けよう。この場合汎用のRuntimeErrorオブジェクトが使われる。そうではなく、カスタム例外クラスを作ろう。
- カスタム例外クラスはStanderdErrorを継承し、クラス名がErrorで終わるようにしよう。
- 1つのプロジェクトのために複数の例外クラスを作るときには、まずStandardErrorを継承する基底クラスを作り、ほかの例外クラスはそのカスタム基底クラスを継承するように構成しよう。
- カスタム例外クラスのためにinitializeメソッドを書くときには、superを呼び出すようにしよう。super呼び出しにエラーメッセージを渡せばなお良い。
- initializeでエラーメッセージを設定するときには、raiseでエラーメッセージを設定すると、initializeのメッセージが上書きされてしまうことに注意しよう。
できる限りもっとも対象の狭い例外を処理するようにしよう
- 修復方法がわかっている特定の例外だけをrescueで捕まえよう。
- 例外を捕まえるときには、もっとも限定されたタイプのmのを最初に処理しよう。例外の階層構造の上位にあるものほど、rescue節は下流に作る。
- StandardErrorのような汎用例外クラスをrescue節で捕まえるのは避けよう。もしそうしたいと思う場合には、ほんとうに必要なものはensure節なのではないかということを考えるべきだ。
- rescue節で例外を生成すると、新しい例外が現在の例外を押しのけ、現在のスコープを抜けて例外処理を最初からやり直す。
リソースはブロックとensureで管理しよう
- 確保したリソースを開放するためにensure節を書こう。
- 例外が発生するかもしれないときのリソース管理では、ensure節を使うのが最も良い。
- リソース管理を抽象化するために、クラスメソッドでblockとensureパターンを使おう。
- ensure節で変数を使うときには、その前に変数が初期化されているかどうかを確かめよう。
# ブロックを付けた時には、メソッドはリソースを所有しリソースを開放する
# ブロックがなければメソッドは単純にリソースを返す
class Lock
def self.acquire
lock = new # リソースを初期化する
lock.exclusive_lock!
if block_given?
yield(lock)
else
lock # Lock::newのように動作する
end
ensure
if block_given?
lock.unlock if lock
end
end
end
ensure節は最後まで実行して抜けるように作ろう
- ensure節のなかでreturn文を明示的に使うのは避けなければならない。これが必要だと感じられる場合には、メソッド本体のロジックに何かしら問題があるはずだ。
- ensure節のなかでreturn文を使うと、すべての例外を捨ててしまう。
- 同様に、ensureのなかで直接throwを使ってはならない。throwはメソッド本体で使うべきものだ。
- throwも例外を捨ててしまう。
- 反復処理では、ensure節のなかでnextやbreakを使ってはならない。反復処理のなかに本当にbeginブロックが必要かよく考えよう。たいていの場合、逆にbeginのなかに反復処理を配置するほうが正しいものだ。
- nextやbreakも例外を捨ててしまう。
- より一般的に、ensure節のなかで制御フローを変更してはならない。制御フローの変更は、rescue節で行うべきだ。その方が、あなたの意図がはっきりと伝わる。
retryでは回数の上限を設け、頻度を変化させ、オーディットトレイルを残そう
- 無条件のretryを使ってはならない。retryはコード内の暗黙のループとして扱うようにしよう。beginブロックの外側のスコープに境界変数を作り、上限に達するまで例外を再生成させる。
- retryを使うときには、オーディットトレイルを作ろう。問題のあるコードを再試行しても上手くいかない時には、最終的なエラーまでのイベントの連鎖を知りたいと思うはずだ。
- 再試行の前にディレイに入れるときには、問題を
- 悪化させないようにrescue節のなかで値を増やしていくことを検討しよう。
retries = 0
begin
service.update(record)
rescue VendorDeadlockError => e
raise if retries >= 3
retries += 1
logger.warn("API failure: #{e}, retrying...")
sleep(5 ** retries)
retry
end
スコープから飛び出したいときにはraiseではなくthrowを使おう
- 複雑な制御フローが必要なときには、raiseではなく、throwを使うようにしよう。throwを使うと、ボーナスとしてスタックの上位にオブジェクトを送ることができる。catchの戻り値はそのオブジェクトだ。
- できる限り単純な制御構造を使おう。catchとthrowの組み合わせは、単純にretrunでスコープから抜け出すメソッドとそれに対する呼び出しで置き換えられることが多い。