コンテンツにスキップ

仮想型と抽象型

変数の型が同じクラス階層下の異なる型を組み合わせている場合、その型は**仮想型**になります。これは、`Reference`、`Value`、`Int`、`Float`を除くすべてのクラスと構造体に適用されます。例:

class Animal
end

class Dog < Animal
  def talk
    "Woof!"
  end
end

class Cat < Animal
  def talk
    "Miau"
  end
end

class Person
  getter pet

  def initialize(@name : String, @pet : Animal)
  end
end

john = Person.new "John", Dog.new
peter = Person.new "Peter", Cat.new

上記のプログラムを `tool hierarchy` コマンドでコンパイルすると、`Person` に対して以下が表示されます。

- class Object
  |
  +- class Reference
     |
     +- class Person
            @name : String
            @pet : Animal+

`@pet` が `Animal+` であることがわかります。 `+` は仮想型であることを意味し、「`Animal` を含む、`Animal` から継承する任意のクラス」を意味します。

コンパイラは、型ユニオンが同じ階層下にある場合、常に仮想型に解決します。

if some_condition
  pet = Dog.new
else
  pet = Cat.new
end

# pet : Animal+

コンパイラは、同じ階層下のクラスと構造体に対して常にこれを行います。つまり、すべての型が継承する最初のスーパークラスを見つけます(`Reference`、`Value`、`Int`、`Float` は除く)。見つからない場合、型ユニオンはそのまま残ります。

コンパイラがこれを行う本当の理由は、あらゆる種類の異なる類似ユニオンを作成しないことでプログラムのコンパイルを高速化し、生成されるコードのサイズを小さくするためです。しかし、一方で、これは理にかなっています。同じ階層下のクラスは、同様の動作をするはずです。

Johnのペットに話させましょう。

john.pet.talk # Error: undefined method 'talk' for Animal

コンパイラが `@pet` を `Animal+` (`Animal` を含む)として扱うため、エラーが発生します。そして、`talk` メソッドが見つからないため、エラーになります。

コンパイラが知らないのは、私たちにとって、`Animal` はインスタンス化されないということです。インスタンス化しても意味がないからです。クラスを `abstract` としてマークすることで、コンパイラにそれを伝える方法があります。

abstract class Animal
end

これでコードはコンパイルされます。

john.pet.talk # => "Woof!"

クラスを抽象としてマークすると、そのインスタンスを作成することもできなくなります。

Animal.new # Error: can't instantiate abstract class Animal

`Animal` が `talk` メソッドを定義する必要があることをより明確にするために、`Animal` に抽象メソッドとして追加できます。

abstract class Animal
  # Makes this animal talk
  abstract def talk
end

メソッドを `abstract` としてマークすることにより、コンパイラは、プログラムがそれらを使用しない場合でも、すべての下位クラスがこのメソッドを実装していることを確認します(パラメータの型と名前が一致していること)。

抽象メソッドはモジュールでも定義でき、コンパイラはインクルードする型がそれらを実装していることを確認します。