これは
- Ruby Gold を受験するにあたって学んでいること等を写経したり, メモったりしています
- 「Ruby 技術者認定試験合格教本」を主に参考にしており, 今回は第四章「オブジェクト指向」を写経していきます
- オブジェクト指向、苦手分野だし, 学ばないといけない事が多いので 2 回くらいに分けて記載予定です
そして, この記事は
既に 2018 年だけど...
初老丸 Advent Calendar 2017 25 日目の記事です.
クラス定義
class 式
Foo クラスの定義.
class Foo def initialize(a) @a = a end def method1 @a end end foo1 = Foo.new(1) foo2 = Foo.new(2)
- クラス名は大文字で始める(クラス名は定数)
- 小文字で始めると構文エラー
以下のように, Ruby のクラス定義は, 内部がスキップされることなく順に評価される.
$ cat 4-3.rb p 1 class Hoge p 2 end p 3 $ ruby 4-3.rb 1 2 3
インスタンスメソッドと初期化メソッド
インスタンスから生成元のクラスオブジェクトを参照する.
[1] pry(main)> class Foo [1] pry(main)* def initialize(a) [1] pry(main)* @a = a [1] pry(main)* end [1] pry(main)* def method1 [1] pry(main)* @a [1] pry(main)* end [1] pry(main)* end => :method1 [2] pry(main)> foo1 = Foo.new(1) => #<Foo:0x00557d56e89e68 @a=1> [3] pry(main)> foo1.class => Foo [4] pry(main)> foo1.class == Foo => true
クラス継承
class FooExt < Foo def initialize(a, b) @b = b super a end def method2(c) @a + @b + c end end fooExt = FooExt.new(3, 4) p fooExt.method1 p fooExt.method2(5)
実行例.
[9] pry(main)> class FooExt < Foo [9] pry(main)* def initialize(a, b) [9] pry(main)* @b = b [9] pry(main)* super a [9] pry(main)* end [9] pry(main)* def method2(c) [9] pry(main)* @a + @b + c [9] pry(main)* end [9] pry(main)* end => :method2 [10] pry(main)> fooExt = FooExt.new(3, 4) => #<FooExt:0x00557d5726aed8 @a=3, @b=4> [11] pry(main)> p fooExt.method1 3 => 3 [12] pry(main)> p fooExt.method2(5) 12 => 12
クラスオブジェクトからスーパークラスを取得する.
[13] pry(main)> FooExt.superclass => Foo
super
スーパークラスの同名メソッドを呼び出す場合, 以下のように super を用いる.
... def initialize(a, b) @b = b super a end ...
super に対して引数を省略すると, メソッドが受け取った引数を「そのまま」スーパークラスの同名メソッドに渡して実行する.
インスタンスメソッド
クラスオブジェクト
継承したクラスオブジェクト
- foo1 と fooExt は Foo クラスで定義された性質を持つ
- さらに fooExt は FooExt クラスで性質を持つ
継承関係は横軸で表現される. 右に行くほどより一般的な継承の上位に位置するクラスになる.
メソッドの探索経路
インスタンスメソッドを実行すると...
- クラスオブジェクトに指定されたメソッドが存在するかどうかを判定する(foo1 の method1 を実行すると, 抽象度を 1 つあげて Foo クラスオブジェクトに method1 が存在するか判定する)
- 同様に fooExt1 の method2 を実行すると, 1 つ上の FooExt に method2 が存在するかを判定する
メソッドが存在しない場合...
- そのクラスオブジェクト、次にスーパークラスのクラスオブジェクトを順に辿ってメソッドを探す
- 最後まで見つからなかった場合には, 例外 NoMethodError が発生する
[14] pry(main)> foo1 = Foo.new(1) => #<Foo:0x00557d56f7f930 @a=1> [15] pry(main)> p foo1.method2 NoMethodError: undefined method `method2' for #<Foo:0x00557d56f7f930 @a=1> [16] pry(main)> fooExt1 = FooExt.new(3, 4) => #<FooExt:0x00557d56ec1250 @a=3, @b=4> [17] pry(main)> p fooExt1.method1 3 => 3
継承チェーンと method_missing
継承チェーン
スーパークラスを省略してクラスを定義すると, 自動的に Object クラスを継承する.
[19] pry(main)> Foo.superclass => Object
Ruby 1.9 以降では, Object クラスは BasicObject クラスを継承するように変更されている.
[20] pry(main)> Object.superclass => BasicObject
クラスの継承チェーンを参照するには Module#ancestors を実行する.
[21] pry(main)> p Foo.ancestors [Foo, Object, PP::ObjectMixin, Kernel, BasicObject] => [Foo, Object, PP::ObjectMixin, Kernel, BasicObject] [22] pry(main)> p FooExt.ancestors [FooExt, Foo, Object, PP::ObjectMixin, Kernel, BasicObject] => [FooExt, Foo, Object, PP::ObjectMixin, Kernel, BasicObject]
継承チェーンに任意のクラスが含まれているかを判定するには, 比較演算子を用いる.
[24] pry(main)> Foo < Object => true [25] pry(main)> Foo > Object => false
オブジェクトが持つインスタンスメソッドを調べるには instance_methods メソッドを用いる. オブジェクトが持つインスタンス変数を調べるには instance_valiables メソッドを用いる.
[29] pry(main)> Foo.instance_methods(false) => [:method1] [30] pry(main)> FooExt.instance_methods(false) => [:method2] [32] pry(main)> foo1.instance_variables => [:@a] [33] pry(main)> fooExt1.instance_variables => [:@b, :@a]
尚, instance_methods に false を指定すると, スーパークラスを辿らない.
[46] pry(main)> FooExt.instance_methods.grep(/method\d/) => [:method2, :method1]
alias と undef
alias を用いて継承チェーンにあるメソッドに別名を付ける.
alias <新メソッド名> <旧メソッド名> alias <新グローバル変数名> <旧グローバル変数名>
- alias 式はメソッドではないので, 間にはカンマを指定しない
- メソッド名は識別子かシンボルで指定する(文字列では指定出来ない!)
undef を用いて指定されたメソッド定義を取り消す.
undef <メソッド名> undef <メソッド名>,<メソッド名>
- alias と同様に識別子かシンボルでメソッドを指定する
- メソッド名をカンマで区切って複数指定することも可能
以下, 実行例.
[47] pry(main)> class Hoge [47] pry(main)* def huga1;end [47] pry(main)* def huga2;end [47] pry(main)* alias :huga3 :huga1 [47] pry(main)* undef :huga2 [47] pry(main)* end => nil [48] pry(main)> Hoge.instance_methods(false) => [:huga1, :huga3] [50] pry(main)> h = Hoge.new => #<Hoge:0x00557d57032378> [51] pry(main)> h.huga1 => nil [52] pry(main)> h.huga2 NoMethodError: undefined method `huga2' for #<Hoge:0x00557d57032378> [53] pry(main)> h.huga3 => nil
- alias メソッドを用いて huga1 メソッドに huga3 という別名を付与している
- undef メソッドを用いて huga2 メソッドを取り消している
- huga2 メソッドを実行すると例外が発生する
method_missing
- メソッドが見つからない場合, NoMethodError が発生する
- 厳密には Object クラスの method_missing メソッドが呼び出される
- 第一引数に指定されたメソッド名, 第二引数以降に指定された引数が渡される
以下のように method_missing を上書きすることで, メソッドが見つからない場合の動作をフックすることが出来る.
class Hoge def method_missing(m, *args) p "called: " + m.to_s end super # 例外が発生するようにスーパークラスの method_missing を呼び出す end
以下, 実行例.
[57] pry(main)> class Hoge [57] pry(main)* def method_missing(m, *args) [57] pry(main)* p "called: " + m.to_s [57] pry(main)* super [57] pry(main)* end [57] pry(main)* end => :method_missing [58] pry(main)> Hoge.new.no_method "called: no_method" NoMethodError: undefined method `no_method' for #<Hoge:0x00557d56f71a10>
オープンクラス
- class 式はクラスオブジェクトが存在しない場合に生成する
- 既に暮らすオブジェクトが存在する場合には, そのオブジェクトをもう一度開いて評価する
class Hoge def huga1;end end class Hoge def huga2;end alias :huga3 :huga1 undef :huga2, :method_missing end Hoge.instance_methods(false)
以下, 実行例.
[64] pry(main)> class Hoge [64] pry(main)* def huga1;end [64] pry(main)* end => :huga1 [65] pry(main)> [66] pry(main)> class Hoge [66] pry(main)* def huga2;end [66] pry(main)* alias :huga3 :huga1 [66] pry(main)* undef :huga2, :method_missing [66] pry(main)* end => nil [67] pry(main)> Hoge.instance_methods(false) => [:huga1, :huga3]
定義済みのクラスを再定義出来るクラスのことをオープンクラスと呼ぶ. 再オープン出来るのは, 標準提供されている組み込みクラスも再オープン出来る.
class String def huga; 1; end end p "hoge".huga1
以下, 実行例.
[69] pry(main)> class String [69] pry(main)* def huga; 1; end [69] pry(main)* end => :huga [71] pry(main)> p "hoge".huga 1 => 1
スーパークラスを指定して再オープンする場合, スーパークラスはオープンする前のクラスと同じであること.
class Foo; end class Bar; end class Baz < Foo end class Baz < Bar end class Baz < Foo end class Baz end
以下, 実行例.
[72] pry(main)> class Foo; end => nil [73] pry(main)> class Bar; end => nil [74] pry(main)> class Baz < Foo [74] pry(main)* end => nil [75] pry(main)> class Baz < Bar [75] pry(main)* end TypeError: superclass mismatch for class Baz [76] pry(main)> class Baz < Foo [76] pry(main)* end => nil [77] pry(main)> class Baz [77] pry(main)* end => nil
Mix-in
Mix-in とは
モジュールの定義と include
モジュールとクラスの相違点は以下の通り.
- モジュールは, 単独ではインスタンス化出来ない(new 出来ない)
- モジュールは継承出来ない
- モジュールはクラスや他のモジュールを取り込むことが出来る
以下, モジュール定義例.
module Bar def methodA @a end end
Module オブジェクトの操作
[1] pry(main)> module Bar [1] pry(main)* def methodA [1] pry(main)* @a [1] pry(main)* end [1] pry(main)* end => :methodA [2] pry(main)> Bar.ancestors => [Bar] [4] pry(main)> Bar.instance_methods => [:methodA] [5] pry(main)> Bar.new NoMethodError: undefined method `new' for Bar:Module
生成した Module オブジェクトは, include メソッドを使用してクラスに取り込むことが出来る.
class FooExt < Foo include Bar end fooExt1 = FooExt.new(3, 4) p fooExt1.methodA
以下, 実行例.
[6] pry(main)> class Foo [6] pry(main)* def initialize(a) [6] pry(main)* @a = a [6] pry(main)* end [6] pry(main)* def method1 [6] pry(main)* @a [6] pry(main)* end [6] pry(main)* end => :method1 [7] pry(main)> class FooExt < Foo [7] pry(main)* def initialize(a, b) [7] pry(main)* @b = b [7] pry(main)* super a [7] pry(main)* end [7] pry(main)* def method2(c) [7] pry(main)* @a + @b + c [7] pry(main)* end [7] pry(main)* end => :method2 [8] pry(main)> class FooExt < Foo [8] pry(main)* include Bar [8] pry(main)* end => FooExt [9] pry(main)> fooExt1 = FooExt.new(3, 4) => #<FooExt:0x00559833bc11c0 @a=3, @b=4> [10] pry(main)> p fooExt1.methodA 3 => 3
FooExt インスタンスから Bar モジュールで定義された methodA を実行出来る.
モジュールのメソッド探索
methodA がどのように探索されるのか.
[11] pry(main)> FooExt.ancestors => [FooExt, Bar, Foo, Object, PP::ObjectMixin, Kernel, BasicObject] [12] pry(main)> FooExt.superclass => Foo [13] pry(main)> FooExt.instance_methods(false) => [:method2]
- include が実行されると, インタプリタは指定されたモジュールに対応する無名クラスを作成して, スーパークラスとの間に組み込む
- 無名クラスは include を実行したクラスのおやすに, 順に挿入される
methodA を持つ 2 つのモジュール Bar と Bar2 を include した場合...
module Bar2 def methodA @a + 1 end end class FooExt < Foo include Bar include Bar2 end
以下, 実行例.
[14] pry(main)> module Bar2 [14] pry(main)* def methodA [14] pry(main)* @a + 1 [14] pry(main)* end [14] pry(main)* end => :methodA [15] pry(main)> class FooExt < Foo [15] pry(main)* include Bar [15] pry(main)* include Bar2 [15] pry(main)* end => FooExt [16] pry(main)> fooExt1 = FooExt.new(3, 4) => #<FooExt:0x00559832c97b40 @a=3, @b=4> [17] pry(main)> p fooExt1.methodA 4 => 4
後者の Bar2 の methodA が最初に見つかって実行される. つまり, 後から include したモジュールのメソッドが優先される.
[18] pry(main)> module M1 [18] pry(main)* def method1; 1; end [18] pry(main)* end => :method1 [19] pry(main)> class C1 [19] pry(main)* def method1; 2; end [19] pry(main)* include M1 [19] pry(main)* end => C1 [21] pry(main)> C1.new.method1 => 2 [22] pry(main)> C1.ancestors => [C1, M1, Object, PP::ObjectMixin, Kernel, BasicObject]
上記のようにモジュールと同名のメソッドを include メソッドの実行前に定義した場合, モジュールは継承チェーンに挿入さっるだけなので, 実行順序に関わらず常にそのクラス中のメソッド定義が優先される.
[1] pry(main)> module M1 [1] pry(main)* def method1; 1; end [1] pry(main)* end => :method1 [2] pry(main)> class C1 [2] pry(main)* include M1 [2] pry(main)* def method1; 2; end [2] pry(main)* end => :method1 [3] pry(main)> C1.new.method1 => 2
特異クラス
特異クラスの性質
def <オブジェクト名>.<新たに定義するメソッド名> ... end
特異メソッドの定義例.
foo1 = Foo.new(1) def foo1.methodB @a + 100 end p foo1.methodB foo2 = Foo.new(1) p foo2.methodB #=> NoMethodError
以下, 実行例.
[2] pry(main)> class Foo [2] pry(main)* def initialize(a) [2] pry(main)* @a = a [2] pry(main)* end [2] pry(main)* def methodA [2] pry(main)* @a [2] pry(main)* end [2] pry(main)* end => :methodA [3] pry(main)> foo1 = Foo.new(1) => #<Foo:0x0055bc095a9748 @a=1> [4] pry(main)> def foo1.methodB [4] pry(main)* @a + 100 [4] pry(main)* end => :methodB [5] pry(main)> p foo1.methodB 101 => 101 [6] pry(main)> p foo2.methodB NameError: undefined local variable or method `foo2' for main:Object
特異メソッドの仕組み
- 特異クラスと呼ばれるクラスを生成して継承チェーンに組み込む
- 特異クラスに対応するオブジェクトは, 指定されたインスタンスの生成元をスーパークラスとして指す
- 特異クラスは無名クラスと同様にインタプリタ内部で使用するクラスで, ユーザーに意識させたくないクラス
特異クラスの参照と再オープン
特異クラスは, 以下のような特殊な構文を使用することで参照することが出来る.
foo1 = Foo.new(1) singleton_class = class << foo1 self end p singleton_class
特異クラスの再オープン式.
class << 対象オブジェクト end
以下, 実行例.
[7] pry(main)> singleton_class = class << foo1 [7] pry(main)* self [7] pry(main)* end => #<Class:#<Foo:0x0055bc095a9748>> [8] pry(main)> p singleton_class #<Class:#<Foo:0x0055bc095a9748>> => #<Class:#<Foo:0x0055bc095a9748>>
上記では, 特異クラスを再オープンして self を指定しているが, これは通常のクラス内部の self と同様に, そのクラスを参照する. クラス式では最後に評価された式の結果を返り値として返すので, 返り値は特異クラスのオブジェクトになる.
以下のように特異クラスを再オープンして特異メソッドを定義することも可能.
foo1 = Foo.new(1) class << foo1 def methodC; @a + 200; end end p foo1.methodC
クラス式の内部で定義しているメソッドは, そのクラスのメソッドになっている. よって, プログラム内の methodC メソッドは <<
でオープンされた foo オブジェクトの特異クラスのメソッドである.
以下, 実行例.
[11] pry(main)> class << foo1 [11] pry(main)* def methodC; @a + 200; end [11] pry(main)* end => :methodC [12] pry(main)> p foo1.methodC 201 => 201
self
- self は自分自身を示す特別なオブジェクト
- クラス内部で使う場合はそのクラスを, メソッド内部ではそのメソッドを実行するオブジェクトを保持する
- メソッドが実行されるオブジェクトのことを, 「メソッドの受け手」という意味でレシーバと呼ぶ
class C1 p self def method1 self end end c1 = C1.new p c1 == c1.method1
以下, 実行例.
[1] pry(main)> class C1 [1] pry(main)* p self [1] pry(main)* def method1 [1] pry(main)* self [1] pry(main)* end [1] pry(main)* end C1 => :method1 [2] pry(main)> c1 = C1.new => #<C1:0x005621083c1598> [3] pry(main)> p c1 == c1.method1 true => true
メソッドのネスト
- メソッドは, そのメソッドが定義したクラスに所属する
- メソッドがネストされた場合, 外側のメソッドが定義されたクラスに定義されるが, 内側のメソッドは外側のメソッドが実行されるまで定義されない
class C2 def method1 def method2 end end end C2.instance_methods(false) C2.new.method1 C2.instance_methods(false)
以下, 実行例.
[4] pry(main)> class C2 [4] pry(main)* def method1 [4] pry(main)* def method2 [4] pry(main)* end [4] pry(main)* end [4] pry(main)* end => :method1 [5] pry(main)> [6] pry(main)> C2.instance_methods(false) => [:method1] [7] pry(main)> C2.new => #<C2:0x005621085ae090> [8] pry(main)> C2.instance_methods(false) => [:method1] [10] pry(main)> C2.new.method1 => :method2 [11] pry(main)> C2.instance_methods(false) => [:method1, :method2]
extend メソッド
特異クラスに対して Mix-in を行う場合, 以下のように特異クラスを再オープンして include を実行する.
foo1 = Foo.new(1) class << foo1 include Bar end p foo1.methodA
以下, 実行例.
[4] pry(main)> class Foo [4] pry(main)* def initialize(a) [4] pry(main)* @a = a [4] pry(main)* end [4] pry(main)* end => :initialize [5] pry(main)> foo1 = Foo.new(1) => #<Foo:0x0055bbb54adfa0 @a=1> [6] pry(main)> class << foo1 [6] pry(main)* include Bar [6] pry(main)* end => #<Class:#<Foo:0x0055bbb54adfa0>> [7] pry(main)> p foo1.methodA 1 => 1
通常クラスと同じように無名クラスが挿入され, 別のインスタンスである foo2 には影響を与えない.
特異クラスへの Mix-in は良く利用される為, 同じ動作をするメソッドである extend メソッドが用意されている.
foo1 = Foo.new(1) foo1.extend(Bar) p foo1.methodA
以下, 実行例.
[8] pry(main)> foo1 = Foo.new(1) => #<Foo:0x0055bbb55c0a78 @a=1> [9] pry(main)> foo1.extend(Bar) => #<Foo:0x0055bbb55c0a78 @a=1> [10] pry(main)> p foo1.methodA 1 => 1
include を利用するコードと比較して簡潔に書けている.
prepend メソッド
- include と同様にモジュールのメソッドをクラスに取り込む為のメソッド
- prepend を呼び出したクラスよりも先にモジュールに対して探索を行う為, 同名メソッドが存在する場合に include と振る舞い異なる
module M1 def method1; "m1"; end end class C1 prepend M1 def method1; "c1"; end end class C2 include M1 def method1; "c2"; end end p C1.new.method1 p C2.new.method1
以下, 実行例.
[1] pry(main)> module M1 [1] pry(main)* def method1; "m1"; end [1] pry(main)* end => :method1 [2] pry(main)> [3] pry(main)> class C1 [3] pry(main)* prepend M1 [3] pry(main)* def method1; "c1"; end [3] pry(main)* end => :method1 [4] pry(main)> [5] pry(main)> class C2 [5] pry(main)* include M1 [5] pry(main)* def method1; "c2"; end [5] pry(main)* end => :method1 [6] pry(main)> p C1.new.method1 "m1" => "m1" [7] pry(main)> p C2.new.method1 "c2" => "c2" [8] pry(main)> C1.ancestors => [M1, C1, Object, PP::ObjectMixin, Kernel, BasicObject] [9] pry(main)> C2.ancestors => [C2, M1, Object, PP::ObjectMixin, Kernel, BasicObject]
- prepend(先頭に追加する) を使った C1 では, M1 が先にメソッド探索される
- include を使った C2 では, C2 の方が先にメソッド探索される
prepend メソッド(2)
- prepend メソッド使うとモジュールがクラスよりも先に呼び出される
- prepend されたモジュール内で super を呼び出すと, 継承チェーン上のクラスから同名のメソッドを探索し呼び出すことが出来る
- 継承チェーン上のクラスに存在しなければ NoMethodError となる
module M1 def method1 super puts "m1" end end class C1 prepend M1 def method1 puts "c1" end end C1.new.method1
以下, 実行例.
[14] pry(main)> module M1 [14] pry(main)* def method1 [14] pry(main)* super [14] pry(main)* puts "m1" [14] pry(main)* end [14] pry(main)* end => :method1 [15] pry(main)> [16] pry(main)> class C1 [16] pry(main)* prepend M1 [16] pry(main)* def method1 [16] pry(main)* puts "c1" [16] pry(main)* end [16] pry(main)* end => :method1 [17] pry(main)> C1.new.method1 c1 m1 => nil [18] pry(main)> C1.ancestors => [M1, C1, Object, PP::ObjectMixin, Kernel, BasicObject]
Refinements
- refine メソッドで変更を加えるクラスを宣言し, using メソッドを呼び出した以降から変更したメソッドの呼び出しが有効になる
- using が有効になる範囲は, クラス・モジュール定義の外で using を呼び出すとファイルの末尾まで
- クラス・モジュール定義の中で using を呼び出すとクラス・モジュール定義の末尾まで
class C def foo puts "C#foo" end def bar foo end end module M refine C do def foo puts "C#foo in M" end end end using M x = C.new x.foo x.bar
以下, 実行例.
$ ruby 4-31.rb C#foo in M C#foo
- refine 内で定義された変更内容は using メソッドを呼び出した後に有効になる
- bar メソッドは内部で foo メソッドの呼び出しを行っているが, using が呼び出される前に定義されているので, 書き換え前の foo メソッドを呼び出す
以上
写経でした.
引き続き, 頑張るぞ.