상속
상속 (Inheritance)
계층의 루트인 Object를 제외한 모든 클래스는 다른 클래스(그것의 슈퍼클래스)를 상속해요. 슈퍼클래스를 지정하지 않으면 클래스는 Reference를, struct는 Struct를 기본으로 상속해요.
클래스는 슈퍼클래스의 모든 인스턴스 변수와 모든 인스턴스·클래스 메서드를 상속받아요. 생성자(new와 initialize)도 포함되죠.
class Person
def initialize(@name : String)
end
def greet
puts "Hi, I'm #{@name}"
end
end
class Employee < Person
end
employee = Employee.new "John"
employee.greet # "Hi, I'm John"
클래스가 new나 initialize를 정의하면, 그 슈퍼클래스의 생성자는 상속되지 않아요:
class Person
def initialize(@name : String)
end
end
class Employee < Person
def initialize(@name : String, @company_name : String)
end
end
Employee.new "John", "Acme" # OK
Employee.new "Peter" # Error: wrong number of arguments for 'Employee:Class#new' (1 for 2)
파생 클래스에서 메서드를 오버라이드할 수 있어요:
class Person
def greet(msg)
puts "Hi, #{msg}"
end
end
class Employee < Person
def greet(msg)
puts "Hello, #{msg}"
end
end
p = Person.new
p.greet "everyone" # "Hi, everyone"
e = Employee.new
e.greet "everyone" # "Hello, everyone"
오버라이드 대신 타입 제한을 사용해 특수화된 메서드를 정의할 수도 있어요:
class Person
def greet(msg)
puts "Hi, #{msg}"
end
end
class Employee < Person
def greet(msg : Int32)
puts "Hi, this is a number: #{msg}"
end
end
e = Employee.new
e.greet "everyone" # "Hi, everyone"
e.greet 1 # "Hi, this is a number: 1"
출처: Crystal 공식 문서
본문
super
super로 슈퍼클래스의 메서드를 호출할 수 있어요:
class Person
def greet(msg)
puts "Hello, #{msg}"
end
end
class Employee < Person
def greet(msg)
super # Same as: super(msg)
super("another message")
end
end
인자나 괄호 없이 쓰면 super는 메서드의 모든 파라미터를 인자로 받아요. 그렇지 않으면 넘겨주는 인자를 받아요.
공변성과 반공변성 (Covariance and Contravariance)
상속이 조금 까다로워질 수 있는 지점 중 하나가 배열이에요. 상속이 사용되는 객체 배열을 선언할 때는 조심해야 해요. 예를 들어 다음을 생각해 봐요:
class Foo
end
class Bar < Foo
end
foo_arr = [Bar.new] of Foo # => [#<Bar:0x10215bfe0>] : Array(Foo)
bar_arr = [Bar.new] # => [#<Bar:0x10215bfd0>] : Array(Bar)
bar_arr2 = [Foo.new] of Bar # compiler error
Foo 배열은 Foo와 Bar를 모두 담을 수 있지만, Bar 배열은 Bar와 그 서브클래스만 담을 수 있어요.
자동 캐스팅(automatic casting)이 개입할 때 여기서 헷갈릴 수 있어요. 예를 들어 다음은 작동하지 않아요:
class Foo
end
class Bar < Foo
end
class Test
@arr : Array(Foo)
def initialize
@arr = [Bar.new]
end
end
@arr을 Array(Foo) 타입으로 선언했으니 Bar들을 넣기 시작할 수 있을 거라고 생각하기 쉬워요. 그렇지 않아요. initialize에서 [Bar.new] 표현식의 타입은 단지 Array(Bar)예요. 그리고 Array(Bar)는 Array(Foo) 인스턴스 변수에 할당될 수 없어요.
그렇다면 올바른 방법은 뭘까요? 표현식이 올바른 타입(Array(Foo))이 되도록 바꾸는 거예요 (위 예시 참고).
class Foo
end
class Bar < Foo
end
class Test
@arr : Array(Foo)
def initialize
@arr = [Bar.new] of Foo
end
end
이것은 하나의 타입(Array)과 하나의 연산(할당)에 대한 예시일 뿐이에요. 다른 타입과 할당에는 이 로직이 다르게 적용돼요. 일반적으로 공변성과 반공변성은 완전히 지원되지 않아요.
더 알아보기
- 모든 클래스는
Object를 제외하고 하나의 슈퍼클래스를 상속하며, 지정하지 않으면 클래스는Reference, struct는Struct를 기본으로 해요. - 슈퍼클래스의 생성자는 자신이
new/initialize를 정의하면 상속되지 않아요. super로 슈퍼클래스 메서드를 호출하고, 타입 제한으로 특수화된 메서드를 만들 수 있어요.- 배열에서 상속 타입을 다룰 때는
Array(슈퍼)에Array(서브)를 넣을 수 없다는 점에 주의하세요. 표현식을 올바른 타입으로 만들어야 해요.