クラス(もしくは構造体,以下同様)の継承とモジュールのmix-inは,多くの面でよく似た様な挙動を示します。
例えば,あるクラスAを継承したクラスBのインスタンスb
に対してb.is_a?(A)
はtrue
を返しますし,あるモジュールCをmix-inしたクラスDのインスタンスd
に対してd.is_a?(C)
もやはりtrue
を返します。
class A
end
class B < A
end
b = B.new
p b.is_a?(A)
#=> true
module C
end
class D
include C
end
d = D.new
p d.is_a?(C)
#=> true
また,親クラスで定義されたメソッドをオーバーライドした際も,mix-inしたモジュールで定義されたメソッドをオーバーライドした際も,super
でオーバーライド元のメソッドを呼び出すことができます。
class A
def x
puts "A"
end
end
class B < A
def x
puts "B"
super()
end
end
B.new.x
#=> B
# A
module C
def x
puts "C"
end
end
class D
include C
def x
puts "D"
super()
end
end
D.new.x
#=> D
# C
クラスもモジュールもジェネリクスの型引数として指定することができます。配列の要素の型としてクラスやモジュールを指定した場合,その配列には,要素の型として指定したクラスを継承したサブクラスや,モジュールをmix-inしたクラスのインスタンスを収めることができます。
class A
end
class B < A
end
p Array(A).new << B.new
#=> [#<B:0x5644ae6d9fe0>]
module C
end
class D
include C
end
p Array(C).new << D.new
#=> [#<D:0x5644ae6d9fd0>]
しかし,クラスの継承とモジュールのmix-inとで明確に異なる挙動を示す場合もあります。Crystalには複数の型を合成したユニオン型という考え方があります。例えば,(String|Symbol)
はString
型とSymbol
型からなるユニオン型です。
ユニオン型を定義した場合,それらの型が共通の祖先を持っていると,コンパイラはそのユニオン型を共通の祖先の型としてみなします。
class A
end
class B < A
end
class C < A
end
p (B|C)
#=> A
一方,共通のモジュールをmix-inした複数のクラスでユニオン型を構成した場合には,そのような集約は行われません。
module A
end
class B
include A
end
class C
include A
end
p (B|C)
#=> (B|C)
こうした挙動の違いが問題になることはほとんどありませんが。それでもいくつかケースで直感的でない挙動に遭遇することになります。例えば,以下の例は問題なく動作します。
class A
end
class B < A
end
class C < A
end
class D
@array : Array(A)
def initialize(*elements)
@array = elements.to_a
end
end
p D.new(B.new, C.new)
#=> #<D:0x55acf7b7ff00 @array=[#<B:0x55acf7b7efe0>, #<C:0x55acf7b7efd0>]>
一方,同様のコードをモジュールのmix-inで実装した場合には,エラーが発生します。
module A
end
class B
include A
end
class C
include A
end
class D
@array : Array(A)
def initialize(*elements)
@array = elements.to_a
end
end
p D.new(B.new, C.new)
#=> Error in line 20: instantiating 'D:Class#new(B, C)'
#
# in line 16: instance variable '@array' of D must be Array(A), not Array(B | C)
クラスの継承1では親子関係が生じるので共通の先祖を指定できるが,モジュールのmix-inでは親子関係は生じないためそれができない,といったところでしょうか。このこと自体は,クラス継承とモジュールmix-inの目的からしても納得できる動作だと思います。
ちなみに,クラス継承においてユニオン型が共通の先祖の型に置き換えられることの副作用としては,以下の様な場合に,DogとCatだけを受け入れる配列を作ることができません。
class Animal
end
class Dog < Animal
end
class Cat < Animal
end
class Bear < Animal
end
pets = Array(Dog|Cat).new
p pets << Bear.new
#=> [#<Bear:0x557cf88c1fe0>]
もっとも,こうした場合はDogとCatに対してBearとは継承ツリーの別れた共通の先祖を設定すべきなんでしょうね。
-
モジュールと同様にそれ自体ではインスタンス化できない抽象型(abstract classやabstract struct)を継承した場合も含む ↩