普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
mattr_reader
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると
3.2.3 モジュール属性
mattr_reader、mattr_writer、mattr_accessorという3つのマクロは、クラス用に定義されるcattr_*マクロと同じです。実際、cattr_*マクロは単なるmattr_*マクロの別名です。クラス属性も参照してください。
たとえば、これらのマクロは以下のDependenciesモジュールで使用されています。
module ActiveSupport module Dependencies mattr_accessor :warnings_on_first_load mattr_accessor :history mattr_accessor :loaded mattr_accessor :mechanism mattr_accessor :load_paths mattr_accessor :load_once_paths mattr_accessor :autoloaded_constants mattr_accessor :explicitly_unloadable_constants mattr_accessor :logger mattr_accessor :log_activity mattr_accessor :constant_watch_stack mattr_accessor :constant_watch_stack_mutex end end
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def mattr_reader
を探してみます
2. 該当箇所が1箇所あったので、みてみます
1. activesupport > lib > active_support > core_ext > module > attribute_accessors.rb
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym) class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} def self.#{sym} @@#{sym} end EOS if instance_reader && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} @@#{sym} end EOS end sym_default_value = (block_given? && default.nil?) ? yield : default class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? end end alias :cattr_reader :mattr_reader
まずここまでの処理を見てみます
syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
symsはsymbolsを短縮した名前で、変数に入る内容は、シンボルの配列ではないかと推測できます
unless
以下の正規表現 /\A[_A-Za-z]\w*\z/
を見てみると
\A
は文字列の先頭にマッチ
[_A-Za-z]
は指定された文字のどれかに一致
\w
は英単語を構成する文字(a~z,A~Z,_,1~9)
*
は0回以上の繰り返しにマッチ
\z
は文字列の末尾にマッチ
上記の条件に当てはまるシンボルにマッチするかを検査しています
例えば、_hoge
, Huga1
などがマッチします
これにマッチしない場合は NameError
例外を発生させるということになります
※ 正規表現について詳しくは以下を参照
マッチした場合は、以下の処理が実行されます
class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} def self.#{sym} @@#{sym} end EOS
class_evalにより引数のヒアドキュメントの内容をクラス内でそのまま実行することになります
モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。
モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。
ただし、ローカル変数は module_eval/class_eval の外側のスコープと共有します。
文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。
class C end a = 1 C.class_eval %Q{ def m # メソッドを動的に定義できる。 return :m, #{a} end } p C.new.m #=> [:m, 1]
ヒアドキュメントの処理をそれぞれ見ていきます
@@#{sym} = nil unless defined? @@#{sym}
では変数symのクラス変数が自クラスに定義されているかを確認しています
定義されていない場合は、クラス変数にnilを代入しています
そして、こちらではクラス変数のアクセッサ(クラスメソッド)を定義しています
def self.#{sym} @@#{sym} end
次にこちらを見ていきます
if instance_reader && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} @@#{sym} end EOS end
instance_reader && instance_accessor
こちらはメソッドの引数の値によって、判定されます。(デフォルトはどちらもtrueの為、実行されます)
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)
こちらの記述は先程のクラス変数のアクセッサとほぼ同一ですが、こちらはクラスメソッドではなく、インスタンスメソッドになります
class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} @@#{sym} end EOS
※クラスメソッド、インスタンスメソッドの定義の方法については以下参照
次にこちらを見ていきます
sym_default_value = (block_given? && default.nil?) ? yield : default class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?
ブロックが与えられているかつメソッドの引数defaultがnilの場合に、ブロックの返り値をsym_default_value変数に代入しています
それ以外の場合に、メソッドの仮引数のdefaultの値を代入しています
sym_default_valueがnilでないときに、クラス変数に値を代入しています
mattr_readerにはエイリアスも設定されており、cattr_readerも同じ処理を行うことができます
alias :cattr_reader :mattr_reader