🎉

Ruby でメモ化するときに defined? を使う

2024/02/01に公開

さて、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_bynil を返す 』と『 @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
GitHubで編集を提案

Discussion