RailsのObject#tryのソースコードを読んでみた

はじめに

Misocaの開発チームでインターンをしているhmryuです。Misocaでは、Railsによる開発を行っているのですが、開発を進める中で検索しても、ドキュメントを読んでも、わからないことが時々あります。そんなときは、手探りながらRailsソースコードやコミットログを調べるようにしています。

先日は、Object#tryの挙動でわからないことがあり、いろいろ調べていました。そこで今回は、Object#tryの実装や注意点について書きたいと思います。

f:id:hmryu:20151203181154p:plain

Object#tryとは?

ActiveSupportObject#try*1は、「レシーバーがnilの場合でもメソッドを呼び出すことができる」「レシーバーがメソッドを持っていなくても呼び出すことができる」メソッドです。

これを使うと、例えば、

# @personがnilの場合、NoMethodErrorが発生してしまう
@person.name if @person 

@person.try(:name)

と書くことができます。

また、

# @personがnon_existing_methodを持っていない場合、NoMethodErrorが発生してしまう
@person.non_existing_method if @person.respond_to?(:non_existing_method)

を  

@person.try(:non_existing_method)

とかけたりして便利です。

tryの実装

tryの実装は、以下のようになっています(version:4.2.4)

class Object
  def try(*a, &b)
    try!(*a, &b) if a.empty? || respond_to?(a.first)
  end

  def try!(*a, &b)
    if a.empty? && block_given?
      if b.arity.zero?
        instance_eval(&b)
      else
        yield self
      end
    else
      public_send(*a, &b)
    end
  end
end

class NilClass
  def try(*args)
    nil
  end

  def try!(*args)
    nil
  end
end

NilClass#tryが定義されているため、初めの例のようにレシーバがnilのケースでは、nilが返されます。

また、レシーバのオブジェクトが引数のメソッドを持っていないケースでは、respond_to?falseになるため、NameNethod::Errorが発生しないようになっています。

trytry!の違いは、呼び出し元のオブジェクトが引数のメソッドを持っていない時、NameNethod::Errorを発生するかどうかになります。

ちなみに、Rails3では、tryで存在しないメソッドを呼び出した場合は、NameNethod::Errorを発生するようになっていました。なので、Rails3のtryは、Rails4のtry!に相当します。*2

Delegatorを使うときの注意

とても便利なtryですが、Delegatorを使ったオブジェクトを対象にする場合は注意が必要です。例えば、

class A < Delegator
  def initialize
  end

  def __getobj__
    B.new
  end

  def method_1
    "A"
  end
end

class B
  def method_1
    "B"
  end
end

このように、AクラスがBクラスに委譲しているようなケースだと、

> a = A.new
> a.method_1
=> A
> a.try(:method_1)
=> B

このようになります。a.method_1については意図した通りにAが帰ってくるのですが、a.try(:method_1)ではB#method_1が呼ばれてしまっていることがわかります。

DelegatorクラスはObjectクラスを継承していないため、tryメソッドを持っておらず、このような挙動になります。

> Delegator.superclass
=> BasicObject

ここで問題提起されており、こちらのコミットで解決されています。

Rubyのtry?

先日、2015年11月11日に、Ruby 2.3.0 の最初のプレビュー版が、リリースされました。

そこで新しく追加されたSafe navigation operatorは、ActiveSupportObject#try!のように動く演算子です。

a = 'a'
a&.upcase # => "A"
a = nil
a&.upcase # => nil

Ruby 2.3.0 の最初のプレビュー版については、Misoca開発チームのeitoballさんが解説記事:Ruby 2.3 プレビュー(言語・組み込みライブラリ編)を書いているので、興味のある方は読んでみてください。