가상 타입과 추상 타입

한 변수의 타입이 같은 계층 아래의 여러 타입을 조합한 것이라면, 그 타입은 가상 타입이 됩니다. 이는 Reference, Value, Int, Float을 제외한 모든 클래스와 구조체에 적용됩니다.

class Animal
end

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

class Cat < Animal
  def talk
    "냥"
  end
end

class Person
  getter pet

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

cholsu = Person.new "철수", Dog.new
minsu = Person.new "민수", Cat.new

위의 프로그램을 tool hierarchy 명령어로 컴파일한다면 Person에 대해 다음을 볼 수 있습니다.

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

@petAnimal+이라는 것을 볼 수 있습니다. + 기호는 가상 타입을 나타냅니다. "Animal을 포함해 Animal을 상속하는 임의의 타입"을 의미합니다.

컴파일러는 같은 위계에 있는 타입이라면 언제나 타입 공용체를 가상 타입으로 해석합니다.

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

# pet : Animal+

같은 위계에 있는 구조체와 클래스에 대해서는 항상 공용체가 가상 타입이 됩니다. 컴파일러는 항상 (Reference, Value, Int, Float을 제외하고) 가장 가까운 공통 상위 클래스를 찾을 것입니다. 찾을 수 없는 경우에는 타입 공용체가 유지됩니다.

이런 과정이 일어나는 진짜 이유는, 비슷하지만 다른 온갖 종류의 공용체를 만들지 않으므로 컴파일이 더욱 빨라질 뿐 아니라 코드의 용량 또한 줄어들기 때문입니다. 굳이 그런 이유가 아니더라도 가상 타입을 만드는 쪽이 더 말이 되기도 합니다. 같은 위계 하의 클래스는 비슷하게 동작하기 때문입니다.

철수의 애완동물한테 말을 걸어볼까요.

cholsu.pet.talk # 오류: Animal에 'talk' 메서드 정의되지 않음

이때 @petAnimal을 포함하는 Animal+로 취급되기 때문에 오류가 발생합니다. Animal에서는 talk 메서드를 찾을 수 없기 때문에 오류가 되는 것입니다.

컴파일러가 모르는 것은, Animal의 인스턴스를 만든다는 것은 말이 안 되고 절대로 일어나지 않는단 사실입니다. 컴파일러에게 이를 알려주는 방법은 클래스를 abstract로 표시하는 것입니다.

abstract class Animal
end

이제 코드는 정상적으로 컴파일됩니다.

cholsu.pet.talk #=> "멍!"

클래스를 추상 클래스로 지정하면 인스턴스를 실수로 만드는 것을 방지할 수도 있습니다.

Animal.new # 오류: 추상 클래스 Animal의 인스턴스를 만들 수 없음

Animaltalk 메서드를 정의해야 한다는 것을 명시적으로 나타내기 위해 추상 메서드를 추가할 수 있습니다.

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

메서드를 abstract로 지정하면 컴파일러는 모든 하위 클래스에 대해 이 메서드가 설령 사용되지 않는다고 해도 구현되어 있는지 점검합니다.

추상 메서드는 모듈에도 정의될 수 있으며, 이 경우 컴파일러는 그 모듈을 포함하는 타입이 해당 추상 메서드를 구현하는지 검사합니다.

results matching ""

    No results matching ""