가상 타입과 추상 타입
가상 타입과 추상 타입 (Virtual and abstract types)
변수의 타입이 같은 클래스 계층 아래의 여러 타입을 합칠 때, 그 타입은 가상 타입(virtual type) 이 돼요. 이 규칙은 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에서 상속받은 모든 클래스"를 의미해요.
출처: Crystal 공식 문서
본문
타입 유니온이 같은 계층 아래에 있다면 컴파일러는 항상 그 유니온을 가상 타입으로 해석해요.
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이 포함되기 때문이에요. 그런데 Animal에서 talk 메서드를 찾을 수 없으니 오류가 나는 거예요.
컴파일러가 모르는 사실은 우리 입장에서는 Animal을 인스턴스화할 일이 없다는 거예요. 인스턴스화하는 게 말이 안 되니까요. 클래스를 abstract로 표시하면 컴파일러에게 그 사실을 알려 줄 수 있어요.
abstract class Animal
end
이제 코드가 컴파일돼요.
john.pet.talk # => "Woof!"
클래스를 abstract로 표시하면 그 클래스의 인스턴스를 만드는 것도 막아 줘요.
Animal.new # Error: can't instantiate abstract class Animal
Animal이 반드시 talk 메서드를 정의해야 한다는 걸 더 명확히 하고 싶다면, Animal에 추상 메서드로 추가할 수 있어요.
abstract class Animal
# Makes this animal talk
abstract def talk
end
메서드를 abstract로 표시하면, 프로그램이 그 메서드를 실제로 쓰지 않더라도 컴파일러가 모든 서브클래스가 이 메서드를 구현하는지(매개변수 타입과 이름까지 일치하는지) 검사해요.
추상 메서드는 모듈에도 정의할 수 있고, 컴파일러가 include하는 타입들이 이를 구현하는지 검사해요.
더 알아보기
- modules: 추상 메서드를 정의할 수 있는 또 다른 장소
- 클래스 계층: 가상 타입이 만들어지는 배경