普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
Rails書いているとよく使う try
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
RailsのAPIリファレンスを見てみると
try(*a, &b)
Invokes the public method whose name goes as first argument just like public_send does, except that if the receiver does not respond to it the call returns nil rather than raising an exception.
This method is defined to be able to write@person.try(:name)
instead of
@person.name if @person
レシーバ(@person)がnilならnilを返し、別のオブジェクトならnameメソッドを実行するというかんじです
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能なので、activesupportディレクトリのlib配下で def try
を探してみます
2. 該当箇所が2個ほどあったので、それぞれみてみます
1. activesupport > lib > active_support > core_ext > object > try.rb
ActiveSupport::Tryable.try
# frozen_string_literal: true require "delegate" module ActiveSupport module Tryable #:nodoc: def try(*a, &b) try!(*a, &b) if a.empty? || respond_to?(a.first) end
まず def try(*a, &b)
を見てみます
引数の *a
ですが、rubyは仮引数に*をつけることで引数をすべて配列として受け取ることができます(可変長引数といいます)
&b
はブロックを引数として受け取ってます
ブロックとは
eachメソッドなど、Rubyでは「ブロック」という仕組みを活用して、メソッドに「処理そのものを引数として渡す」というパターンが頻出します。
引用:Rubyの面白さを理解するためのメソッド、ブロック、Proc、lambda、クロージャの基本
次に try!(*a, &b) if a.empty?
を見てみると 仮引数のaが空の場合は try!(*a, &b)
を実行するようです
try!を見てみます 同じmodule内にtry!メソッドがありました
def try!(*a, &b) if a.empty? && block_given? if b.arity == 0 instance_eval(&b) else yield self end else public_send(*a, &b) end end
if a.empty? && block_given?
は配列aに対して empty?
をしています
Array.empty?を調べてみると
empty?メソッドは、配列が空であればtrue、1つ以上の要素があればfalseを返します。
arr = [1, 2, 3] puts arr.empty? arr = [] puts arr.empty?
なので、配列の中身がnilでもfalseになります
以下が配列の中身がnilの場合の挙動です
block_given?
はメソッドにブロックが与えられているか確認するメソッドです
メソッドにブロックが与えられていれば真を返します。
このメソッドはカレントコンテキストにブロックが与えられているかを調べるので、 メソッド内部以外で使っても単に false を返します。
iterator? は (ブロックが必ずイテレートするとはいえないので)推奨されていないの で block_given? を使ってください。
Ruby2.4.0リファレンスマニュアル
つまり、仮引数aの要素が空かつブロックが渡されているときがこちらの判定式の内容です
※ ブロックについては@kidach1氏のこちらの記事が分かりやすかったです
qiita.com
次にif b.arity == 0
を見てみます
arity
を調べてみると
メソッドが受け付ける引数の数を返します。
ただし、メソッドが可変長引数を受け付ける場合、負の整数
-(必要とされる引数の数 + 1)
を返します。C 言語レベルで実装されたメソッドが可変長引数を 受け付ける場合、-1 を返します。
引用:Ruby2.4.0リファレンスマニュアル
ブロックの引数が無い場合に真となります
その場合には instance_eval
にブロックを渡すということになります
instance_evalメソッドは、渡されたブロックをレシーバのインスタンスの元で実行します。ブロックの戻り値がメソッドの戻り値になります。
ブロック内では、インスタンスメソッド内でコードを実行するときと同じことができます。ブロック内でのselfはレシーバのオブジェクトを指します。なお、ブロックの外側のローカル変数はブロック内でも使えます。
Ruby 1.9 Ruby 1.9では、instance_evalメソッドはBasicObjectに移されました(この変更はRuby 1.8用に書いたプログラムに特に影響はありません)。
次の例では、instance_evalメソッドに渡したブロック内でインスタンス変数やprivateメソッドを利用しています。
class Cat def initialize(name) @name = name end private def hello "meow..." end end cat = Cat.new("Piko") puts cat.instance_eval { @name + ": " + hello }
yield self
はブロックが引数を受け取る場合を想定しています
ブロックが引数を受け取るのはどんな記述かというとeachメソッドであればこのような書き方です
each do |i| puts i end
次に if a.empty? && block_given?
がfalseだった場合を見てみます
public_send(*a, &b)
を調べると
public_sendメソッドは、レシーバの持っているpublicなメソッドを呼び出します。引数や戻り値については、sendメソッドの説明をご覧ください。
引用:Rubyリファレンス:public_send
レシーバのpublicなメソッドに対してtry!で受け取った引数をそのまま渡しています
try!メソッドは以上です
tryメソッドに戻ります
respond_to?(a.first)
を見てみます
aは可変長引数の為、配列です。 a.firstで配列の先頭の値を取得しています
そして、respond_to?でレシーバに配列の先頭の値がメソッドとして実装されているかを確認しています
先程、tryの使い方を調べた際に出てきた例person.try(:name)
でいうとaには[:name]
が入っており、a.first
で :name
が取れるので、
respond_to?(:name)
という感じです
NilClass.try
class NilClass # Calling +try+ on +nil+ always returns +nil+. # It becomes especially helpful when navigating through associations that may return +nil+. # # nil.try(:name) # => nil # # Without +try+ # @person && @person.children.any? && @person.children.first.name # # With +try+ # @person.try(:children).try(:first).try(:name) def try(*args) nil end
nilに対してtryした場合は、常にnilを返すという動きです
ついでにnil.try!も見てみると
# Calling +try!+ on +nil+ always returns +nil+. # # nil.try!(:name) # => nil def try!(*args) nil end
tryと同じ動きです
tryが色々なオブジェクトで使えるのは、ActiveSupport::TryableモジュールをObjectにincludeしてからです
該当の箇所は下記になります
class Object include ActiveSupport::Tryable class Delegator include ActiveSupport::Tryable
読んでみて
ブロックを引き受けるメソッドを作る場合の書き方が非常に参考になったので、ここらへんは活かしていきたいと思います