Ruby でメモ化するときに defined? を使う
さて、Ruby でなにかの値をキャッシュして、その値を使い回すときに ||=
をよく利用すると思います。
# 最初の1回のみ CSV.read が呼ばれる
def csv
@csv ||= CSV.read("sample.csv")
end
# csv メソッドを何回呼び出しても読み込み処理が呼ばれるのは1回のみ
csv
csv
csv
便利ですねー。
このようにメソッドの結果をキャッシュ化することを メモ化
と呼びます。
||=
だと意図する動作がしない可能性がある
||=
でお手軽にメモ化することができるんですが、一部のケースで意図しない動作になることがあります。
例えば次のように Rails でレコードの値をメモ化したいとします。
class User
def comment
# 自身の id と紐づく Comment のレコードを保存しておく
@comment ||= Comment.find_by(user_id: id)
end
end
上記のコードでは『 #comment
を何回呼び出しても .find_by
が1回しか呼ばれないようにしたい』という風に書いたんですが残念ながらこれは意図する動きにならない可能性があります。
なぜかというと
@comment ||= Comment.find_by(user_id: id)
は
@comment = @comment || Comment.find_by(user_id: id)
のショートハンドなのですが、これは『 @comment
が偽の場合に Comment.find_by(user_id: id)
が呼ばれる』という意味になります。
なので『 .find_by
が nil
を返す 』と『 @comment = nil
』となり2回目に #comment
が呼ばれた場合に『再度 .find_by
が呼ばれる』という挙動になります。
『 .find_by
でみつからなかった場合には再度 .find_by
を呼び出す』という挙動にしたいのであれば問題ないんですが、そうでない場合は不要に .find_by
が呼ばれてしまいます。
これは nil
ではなくて『キャッシュしたい結果が false
の場合』でも同様になります。
||=
の変わりに defined?
を使う
このような場合には『 @comment
が偽かどうか』で判定するのではなくて『 @comment
が定義されているかどうか』で判定する必要があります。
それを実現するのが defined?
になります。
これを利用することで『任意のインスタンス変数が定義されているかどうか』を判定します。
# 定義されていない場合は nil を返す
pp defined? @comment
# => nil
# 定義されている場合は『引数のカテゴリーの文字列』を返す
@comment = "hoge"
pp defined? @comment
# => "instance-variable"
# これは値がどうなっているのかは関係ない
@comment = nil
pp defined? @comment
これを利用すると #comment
メソッドは以下のように記述する事ができます。
class User
def comment
if defined? @comment
# インスタンス変数が定義済みであればそれを返す
@comment
else
# そうでない場合は自身の id と紐づく Comment のレコードを保存しておく
@comment = Comment.find_by(user_id: id)
end
end
end
こんな感じで ||=
を使う場合には注意しましょう。
余談
ちなみに #remove_instance_variable
を利用することで意図的に『インスタンス変数を削除』することができます。
@comment = "hoge"
pp defined? @comment
# => "instance-variable"
remove_instance_variable :@comment
pp defined? @comment
# => nil
Discussion