Ruby のメモ化では ||= に要注意!

Ruby on Rails

パフォーマンス向上のためにメモ化を利用するケースがあります。
メモ化とは、処理結果を保存しておき、同じ処理を再実行しないようにするテクニックです。
例えば以下のようなインスタンスメソッドを定義することで、処理の実行回数を1回におさえることができます。

def mails
  @mails ||= fetch_mails
end

@mailsnil または false の場合に fetch_mails を実行し、その結果を @mails に代入します。
@mails はインスタンス変数なので、宣言していなくてもアクセスができ、nil が返されます。
よって mails メソッドは以下のように動作し、何回呼び出しても fetch_mails は1回しか呼び出されず、パフォーマンス向上がはかれます。

  • 初回: @mailsnil を返すことから fetch_mails が実行される
  • 2回目以降: @mails には値があるので、その値を返し fetch_mails は実行されない

一見シンプルで読みやすいこの実装ですが、実は落とし穴があります。

||= の落とし穴

それはfetch_mails の結果が nil false である場合です。
@mails が falsy な値になってしまうので、||= では毎回再評価されてしまいます。

なので、メモ化したい場合は defined?instance_variable_defined? を使うようにしましょう。

defined?

defined? は Ruby のキーワードであり、定義されていればその種別の文字列を返します。

defined? @hoge
=> nil

@hoge = nil
=> nil

defined? @hoge # ? がついてますが、定義されていたら文字列を返します!
=> "instance-variable"

そのため、以下のようにメモ化できます。

def mails
  return @mails if defined? @mails

  @mails = fetch_mails
end

参考: https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#defined

instance_variable_defined?

こちらは Object クラスに定義されているメソッドで、インスタンス変数として宣言されているかを bool 値で返します。

instance_variable_defined? :@hoge
=> false

@hoge = nil
=> nil

instance_variable_defined? :@hoge
=> true

よってこちらも以下のようにメモ化できます。

def mails
  return @mails if instance_variable_defined? :@mails

  @mails = fetch_mails
end

参考: https://docs.ruby-lang.org/ja/latest/method/Object/i/instance_variable_defined=3f.html

どちらを使うべきか

どちらでも ||= この問題は解決できます。
強いて挙げるとするならば defined? の方が個人的にはおすすめです。

  • よりシンプルに書ける
  • キーワードなのでメソッド呼び出しのオーバーヘッドがない(大差ないと思いますが…)
タイトルとURLをコピーしました