crystal
CrystalDay 18

[Crystal] クラス継承とモジュールmix-inの挙動の違い

クラス(もしくは構造体,以下同様)の継承とモジュールの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とは継承ツリーの別れた共通の先祖を設定すべきなんでしょうね。


  1. モジュールと同様にそれ自体ではインスタンス化できない抽象型(abstract classやabstract struct)を継承した場合も含む