仮想型と抽象型¶
変数の型が同じクラス階層下の異なる型を組み合わせている場合、その型は**仮想型**になります。これは、`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` としてマークすることにより、コンパイラは、プログラムがそれらを使用しない場合でも、すべての下位クラスがこのメソッドを実装していることを確認します(パラメータの型と名前が一致していること)。
抽象メソッドはモジュールでも定義でき、コンパイラはインクルードする型がそれらを実装していることを確認します。