普段仕事で使っている Ruby on Rails ですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
class_attribute
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
RAILS GUIDES の日本語ドキュメントを見てみると
4.1.1 class_attribute
class_attributeメソッドは、1つ以上の継承可能なクラスの属性を宣言します。そのクラス属性は、その下のどの階層でも上書き可能です。
class A class_attribute :x end class B < A; end class C < B; end A.x = :a B.x # => :a C.x # => :a B.x = :b A.x # => :a C.x # => :b C.x = :c A.x # => :a B.x # => :b
ソースコードを読んでみる
1. rails プロジェクトの activesupport にある機能ですので、activesupport ディレクトリの lib 配下で def class_attibute
を探してみます
2. 該当箇所が 1 箇所あったので、みてみます
1. activesupport > lib > active_support > core_ext > class > attribute.rb
# frozen_string_literal: true require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/array/extract_options" class Class 省略 def class_attribute( *attrs, instance_accessor: true, instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil ) attrs.each do |name| singleton_class.silence_redefinition_of_method(name) define_singleton_method(name) { default } singleton_class.silence_redefinition_of_method("#{name}?") define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate ivar = "@#{name}".to_sym singleton_class.silence_redefinition_of_method("#{name}=") define_singleton_method("#{name}=") do |val| redefine_singleton_method(name) { val } if singleton_class? class_eval do redefine_method(name) do if instance_variable_defined? ivar instance_variable_get ivar else singleton_class.send name end end end end val end if instance_reader redefine_method(name) do if instance_variable_defined?(ivar) instance_variable_get ivar else self.class.public_send name end end redefine_method("#{name}?") { !!public_send(name) } if instance_predicate end if instance_writer redefine_method("#{name}=") do |val| instance_variable_set ivar, val end end end end
まず class_attribute の引数からみてみます
def class_attribute( *attrs, instance_accessor: true, instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil )
attrs
は可変長引数で instance_accessor
などの引数はキーワード引数です
可変長引数は引数を配列として受け取れ、キーワード引数はハッシュとして引数で宣言できます
それぞれ以下のように挙動します
class A def self.hoge(*attrs, huga: 'a') p '==attrs==' p attrs p '==huga==' p huga end end => A.hoge(1,2,3, huga: 'b') "==attrs==" [1, 2, 3] "==huga==" "b"
それではメソッドの中身を見ていきます
attrs.each do |name| singleton_class.silence_redefinition_of_method(name) define_singleton_method(name) { default } singleton_class.silence_redefinition_of_method("#{name}?") define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate ivar = "@#{name}".to_sym
attrs
は先程の可変長引数で配列になります。配列の要素を each
で1つずつ取り出して処理しています
each
の中の処理を見ていきます
singleton_class.silence_redefinition_of_method(name)
singleton_class
メソッドでレシーバの特異クラスを戻り値とします
参照:instance method Object#singleton_class (Ruby 2.6.0)
silence_redefinition_of_method
は active_support > core_ext > module > redefine_method.rb
に定義されているメソッドです
処理内容と経緯については @y-yagi
さんのブログに記載がありましたので、こちらを引用させて頂きます
普通にメソッドを再定義しようとすると既にメソッドがある為Rubyのwarningが発生します。それを避ける為、今まではremove_possible_methodメソッドを先に呼び出してメソッドを未定義にするようにしていました(redefine_methodメソッド内でもそのような挙動になっています)。が、メソッドを再定義する、という事をする為にメソッドを未定義にする、というのは目的がわかりにくいのでは、という事で新たにremove_possible_methodを使用せずに再定義するメソッドが追加されました。
実装は下記の通りです(Ruby 2.3以降の場合)。
# Marks the named method as intended to be redefined, if it exists. # Suppresses the Ruby method redefinition warning. Prefer # #redefine_method where possible. def silence_redefinition_of_method(method) if method_defined?(method) || private_method_defined?(method) # This suppresses the "method redefined" warning; the self-alias # looks odd, but means we don't need to generate a unique name alias_method method, method end endalias_methodを使用して対応しているんですねえ。
処理自体はメソッドの再定義をRubyのエラーを表示させず、実行できるものです
次の行を見てみます
define_singleton_method(name) { default }
define_singleton_method
は、レシーバにシングルトンのメソッドを追加できるメソッドです
使い方は以下の通りです
define_singleton_method(symbol, method) → symbol define_singleton_method(symbol) { block } → symbol
define_singleton_method
の引数とブロック内の default
にそれぞれ何が入るかは
name
は、可変長引数 attrs
の各要素で、ブロック内の default
は引数のdefaultが入ります
def class_attribute( *attrs, instance_accessor: true, instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil ) attrs.each do |name| singleton_class.silence_redefinition_of_method(name) define_singleton_method(name) { default }
次の行をみます
先程見てきた処理とほとんど一緒です
singleton_class.silence_redefinition_of_method("#{name}?")
違いは name?
にしているところです
rubyで ?
が付くメソッドは、booleanを返すメソッドです
次の行をみます
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
instance_predicate
はメソッドの引数の値でデフォルト値が true
のものです
define_singleton_method("#{name}?")
は、先ほどと一緒でシングルトンメソッドを定義している処理です
ブロック内の処理 { !!public_send(name) }
を見ていきます
public_send
がどんな処理をするか見てみると、引数のパブリックメソッドを呼び出す処理です
オブジェクトの public メソッド name を args を引数にして呼び出し、メソッ ドの実行結果を返します。
1.public_send(:+, 2) # => 3
public_sendの前にある !!
はdouble bangという方法で 戻り値を booleanにする方法です
参照:Rubyの否定演算子2つ重ね「!!」(double-bang)でtrue/falseを返す
ちなみにrubocopではチェック対象になります
Examples:
# bad !!something # good !something.nil?
次の行をみます
インスタンス変数名をシンボルにしています
例えば、name が hoge
な場合だと、 :@hoge
となります
ivar = "@#{name}".to_sym
次の行をみます
singleton_class.silence_redefinition_of_method("#{name}=")
今までと同じようにこんどはsetterのメソッド定義しています
まずは、次の処理の全体像をみます
define_singleton_method("#{name}=") do |val| redefine_singleton_method(name) { val } if singleton_class? class_eval do redefine_method(name) do if instance_variable_defined? ivar instance_variable_get ivar else singleton_class.send name end end end end val end
define_singleton_method("#{name}=")
は今までと同じでシングルトンメソッドの定義です
次の行をみます
redefine_singleton_method(name) { val }
redefine_singleton_method
は既に定義されているメソッドを上書きするRailsのメソッドです
参照:Module
次のif文内の全体像をみます
if singleton_class? class_eval do redefine_method(name) do if instance_variable_defined? ivar instance_variable_get ivar else singleton_class.send name end end end end
最初の行は、レシーバがシングルトンのクラスかどうか判定しています
if singleton_class?
次の行をみます
class_eval do 省略 end
class_eval
のブロック内の処理をレシーバに動的に実装します
モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。 モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。
class_eval
のブロック内を見ていきます
redefine_method(name) do if instance_variable_defined? ivar instance_variable_get ivar else singleton_class.send name end end
redefine_method
は activesupport > lib > active_support > core_ext > class > redefine_method.rb
の17行目に定義されているメソッドで定義済みのメソッドを書き換える処理を行います
instance_variable_defined?
は引数のインスタンス変数名が定義されていれば、真を返すメソッドです
インスタンス変数 var が定義されていたら真を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#instance_variable_defined?
ivar
は class_attribute
メソッドの引数 *attrs
の要素の1つになります
def class_attribute( *attrs, instance_accessor: true, instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil ) attrs.each do |name| 省略 ivar = "@#{name}".to_sym
if instance_variable_defined? ivar
が真の場合は、instance_variable_get ivar
でレシーバのインスタンス変数を取り出します
オブジェクトのインスタンス変数の値を取得して返します。 インスタンス変数が定義されていなければ nil を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#instance_variable_get
偽の場合は、singleton_class.send name
でレシーバから特異クラスを返し、特異クラスに対して send name
で name
メソッドを呼び出します
レシーバの特異クラスを返します。 まだ特異クラスがなければ、新しく作成します。 レシーバが nil か true か false なら、それぞれ NilClass, TrueClass, FalseClass を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#singleton_class
次にこちらをみます
今まで見てきた実装とほぼ同じです
ことなる部分は、 if instance_variable_defined?(ivar)
が偽の場合に、 self.class.public_send name
でレシーバのクラスメソッドを呼び出す処理になっている部分です
instance method Object#public_send (Ruby 2.6.0)
if instance_reader redefine_method(name) do if instance_variable_defined?(ivar) instance_variable_get ivar else self.class.public_send name end end redefine_method("#{name}?") { !!public_send(name) } if instance_predicate end
if instance_predicate
が真の場合は、レシーバのpublicなメソッドとして定義されている name
を定義しています
例えば、 name
が hoge
である場合は、 hoge?
というメソッドが定義され、 戻り値が true, false
になります
次をみていきます
instance_writer
が真の場合に、redefine_method
でセッターを定義しています
if instance_writer redefine_method("#{name}=") do |val| instance_variable_set ivar, val end end
参照:instance method Object#instance_variable_set (Ruby 2.6.0)