상속
상속 (Inheritance)
상속은 객체 지향 프로그래밍에서 "is-a" 관계, 다시 말해 하위 유형(subtyping) 관계를 나타내는 데 쓰는 개념이에요. 부모 유형이 가진 특징을 자식 유형이 물려받는다고 생각하면 자연스럽게 다가와요.
출처
본문
상속 관계 지정하기
상속 관계는 클래스 선언에서 inherit 키워드로 지정해요. 기본 문법은 아래 예시와 같아요.
type MyDerived(...) =
inherit MyBase(...)
클래스는 직접적인 기본 클래스(base class)를 최대 하나까지만 가질 수 있어요. inherit 키워드로 기본 클래스를 지정하지 않으면, 그 클래스는 암시적으로 System.Object를 상속받아요.
상속된 멤버
어떤 클래스가 다른 클래스를 상속받으면, 기본 클래스의 메서드와 멤버들은 파생 클래스(derived class)를 사용하는 쪽에서 마치 파생 클래스 자체의 멤버인 것처럼 바로 쓸 수 있어요.
다만 let 바인딩과 생성자 매개변수는 해당 클래스 내부에서만 접근할 수 있는 private 멤버라서, 파생 클래스에서는 접근할 수 없어요.
base 키워드는 파생 클래스 안에서 사용할 수 있고, 기본 클래스의 인스턴스를 가리켜요. 사용법은 self 식별자와 비슷해요.
가상 메서드와 오버라이드
가상 메서드(와 속성)는 다른 .NET 언어에 비해 F#에서 조금 다르게 동작해요. 새로운 가상 멤버를 선언할 때는 abstract 키워드를 사용해요. 그 메서드에 기본 구현을 제공하는지와는 상관없이 항상 이렇게 선언해요. 그래서 기본 클래스에서 가상 메서드를 완전히 정의하는 모습은 다음과 같은 패턴이 돼요.
abstract member [method-name] : [type]
default [self-identifier].[method-name] [argument-list] = [method-body]
그리고 파생 클래스에서 이 가상 메서드를 오버라이드하는 모습은 다음 패턴이에요.
override [self-identifier].[method-name] [argument-list] = [method-body]
기본 클래스에서 default 구현을 생략하면, 그 기본 클래스는 추상 클래스(abstract class)가 돼요.
아래 코드 예시는 기본 클래스에 새 가상 메서드 function1을 선언하고, 파생 클래스에서 이를 오버라이드하는 모습을 보여줘요.
type MyClassBase1() =
let mutable z = 0
abstract member function1: int -> int
default u.function1(a: int) =
z <- z + a
z
type MyClassDerived1() =
inherit MyClassBase1()
override u.function1(a: int) = a + 1
생성자와 상속
기본 클래스의 생성자는 파생 클래스에서 반드시 호출해야 해요. 기본 클래스 생성자에 넘길 인자들은 inherit 절의 인자 목록에 들어가요. 그리고 이때 사용하는 값들은 파생 클래스 생성자에 전달된 인자들로부터 정해져야 해요.
아래 코드는 기본 클래스와 파생 클래스를 보여주는데, 파생 클래스가 inherit 절에서 기본 클래스 생성자를 호출하고 있어요.
type MyClassBase2(x: int) =
let mutable z = x * x
do
for i in 1..z do
printf "%d " i
type MyClassDerived2(y: int) =
inherit MyClassBase2(y * 2)
do
for i in 1..y do
printf "%d " i
생성자가 여러 개인 경우에는 아래 코드를 사용할 수 있어요. 파생 클래스 생성자의 첫 줄이 inherit 절이고, 필드는 val 키워드로 선언된 명시적 필드(explicit field)로 나타나요. 자세한 내용은 Explicit Fields: The val Keyword 문서를 참고해요.
type BaseClass =
val string1 : string
new (str) = { string1 = str }
new () = { string1 = "" }
type DerivedClass =
inherit BaseClass
val string2 : string
new (str1, str2) = { inherit BaseClass(str1); string2 = str2 }
new (str2) = { inherit BaseClass(); string2 = str2 }
let obj1 = DerivedClass("A", "B")
let obj2 = DerivedClass("A")
상속의 대안
유형을 조금만 수정하면 되는 경우라면, 상속 대신 객체 식(object expression)을 쓰는 것을 고려해 볼 수 있어요. 아래 예시는 새 파생 유형을 만드는 대신 객체 식을 사용하는 모습을 보여줘요.
open System
let object1 =
{ new Object() with
override this.ToString() = "This overrides object.ToString()" }
printfn "%s" (object1.ToString())
객체 식에 대한 자세한 내용은 Object Expressions 문서를 참고해요.
객체 계층을 만들 때는 상속 대신 판별 유니온(discriminated union)을 쓰는 것도 고려해 봐요. 판별 유니온은 공통된 전체 유형을 공유하면서 서로 다른 동작을 가지는 객체들의 다양한 모습도 표현할 수 있어요. 서로 미세하게 다른 파생 클래스들이 여럿 필요한 상황이라면, 단일 판별 유니온이 그 필요성을 없애주는 경우가 많아요. 판별 유니온에 대한 정보는 Discriminated Unions 문서를 참고해요.