Menu

C# 상속: base, virtual, override, sealed와 다형성

C#에서 클래스 상속이 동작하는 방식: 콜론으로 파생하기, 상속되는 것과 상속되지 않는 것, base로 기본 클래스 호출하기, virtual과 override, new로 숨기기(모두를 놀라게 하는 출력과 함께), sealed, 다형성, 계층 구조에서의 업캐스트와 다운캐스트를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

상속은 한 클래스가 다른 클래스를 기반으로 만들어지게 합니다. 파생 클래스는 기본 클래스의 멤버를 얻고, 자기 멤버를 추가하며, 기본 클래스가 허락한 동작을 바꿀 수 있습니다. virtual 메서드와 결합하면 다형성이 생깁니다. 기본 클래스를 대상으로 작성한 코드가 어떤 파생 클래스를 가졌는지 모르고도 올바른 파생 동작을 실행합니다.

클래스 파생하기

콜론 뒤에 기본 클래스를 둡니다. 파생 클래스는 기본 클래스가 가진 모든 것과 자신이 선언한 것을 가집니다:

출력:

TX-19: 20 km, 2 fares
True

파생 클래스가 얻는 것과 얻지 못하는 것:

  • 상속됨: 필드, 속성, 메서드, 이벤트, 중첩 타입. 모든 것이 물리적으로 객체 안에 있습니다.
  • 접근 가능: 기본 클래스가 허락한 것만, 즉 public, protected, internal 멤버입니다. Vehicle의 private 멤버는 모든 Taxi 안에 존재하지만 Taxi의 코드는 그 이름을 쓸 수 없습니다. 그래서 Km에 private set이 있습니다. Taxi는 읽을 수 있고, Drive를 통해서만 바꿉니다.
  • 상속되지 않음: 생성자. Taxi는 자기 생성자를 선언하고 : base(plate)로 Vehicle의 생성자 중 하나에 연결해야 합니다. 둘이 실행되는 순서는 생성자 페이지에서 보여 줍니다.

모든 클래스는 결국 object에서 파생되며, 그래서 모든 객체에 ToString(), Equals(), GetHashCode()가 있습니다.

virtual과 override

기본 클래스는 "파생 클래스가 자기 버전을 제공해도 된다"는 뜻으로 메서드를 virtual로 표시합니다. 파생 클래스는 override로 이를 대체합니다. 재정의 안에서 base.Method()는 기본 클래스의 버전을 호출합니다.

출력:

[email] to lea@example.com: Your order 1042 has shipped today
  (unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today

루프 변수의 타입은 Notification인데도 각 객체는 자기 방식으로 렌더링합니다. 이것이 다형성입니다. n.Render(...) 호출은 실행 시점에 객체의 실제 타입으로 결정됩니다. 또 Email의 base.Render가 그 자체로 가상인 Channel을 쓰므로, 기본 메서드가 generic이 아니라 email을 출력한다는 점에도 주목하세요. 기본 클래스 안의 가상 호출도 재정의에 도달합니다.

Channel이 보여 주듯 속성도 가상일 수 있습니다. 필드는 불가능합니다.

컴파일러는 두 키워드를 모두 요구합니다. virtual이 아닌 메서드에 override를 쓰면 CS0506 오류("cannot override inherited member ... because it is not marked virtual, abstract, or override")입니다. 기본 메서드가 가상인데 override를 빠뜨리면 경고일 뿐이지만, 다음 절에서 보듯 의미가 완전히 바뀝니다.

new와 override: 재정의 대신 숨기기

파생 클래스가 override를 쓰지 않고 기본 메서드와 시그니처가 같은 메서드를 선언하면 기본 메서드를 숨깁니다. 컴파일러가 경고하고(가상 기본 메서드면 CS0114, 아니면 CS0108) new 키워드를 제안하는데, 이는 경고를 없애 줄 뿐 숨기는 동작은 그대로 둡니다:

출력:

Sales report
Report
Draft report

b와 c는 같은 종류의 객체인데 다른 제목을 출력합니다. new를 쓰면 어떤 메서드가 선택되는지가 컴파일 시점에 변수의 타입으로 결정됩니다. 보고서를 Report로 다루는 코드(리스트, 메서드 매개변수, 프레임워크 콜백)는 DraftReport.Title을 절대 보지 못합니다. 원하는 동작인 경우는 거의 없습니다. 다형성에는 override를 쓰세요. new는 주로 제어할 수 없는 기본 클래스가 이미 가진 이름과 겹치는 멤버를 추가한 경우를 위해 존재합니다.

sealed

클래스의 sealed는 상속을 금지합니다:

sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'

string이 sealed이고, 많은 프레임워크 타입도 그렇습니다. 재정의에서 sealed는 그 단계에서 연결을 멈춥니다:

class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed

상속을 위한 클래스 설계에는 노력이 듭니다. 무엇을 가상으로 할지, 파생 클래스가 무엇에 의존해도 되는지, 일이 어떤 순서로 일어나는지 정해야 합니다. 그렇게 설계하지 않은 클래스는 sealed로 두는 편이 안전하며, sealed는 나중에 아무도 깨뜨리지 않고 풀 수 있지만, 봉인 해제는 다른 사람들이 상속하기 시작하면 되돌릴 수 없습니다. sealed 클래스의 멤버 호출은 재정의가 없음을 런타임이 알기 때문에 조금 더 빠를 수도 있습니다.

기본 클래스 하나, 인터페이스 여러 개

C# 클래스는 기본 클래스를 정확히 하나 가집니다. class Admin : User, Employee는 CS1721 오류("cannot have multiple base classes")입니다. 하지만 클래스는 기본 클래스와 함께 인터페이스를 몇 개든 구현할 수 있습니다:

class Admin : User, IAuditable, IComparable<Admin>
{
    // base class first, then interfaces, in any order
}

"~의 한 종류이고 구현을 공유한다"에는 기본 클래스를, "~을 할 수 있다"에는 인터페이스를 쓰세요. 기본 클래스가 파생 클래스에게 일부 메서드를 채우도록 강제하기 위해서만 존재한다면 추상 클래스가 맞는 도구입니다.

계층 구조에서 위아래로 캐스트하기

파생 객체는 기본 타입이 기대되는 곳에 언제나 쓸 수 있습니다. 이 업캐스트는 암시적이며 실패할 수 없습니다. 반대 방향인 다운캐스트는 명시적 캐스트가 필요하며, 객체가 그 타입이 아니면 실행 시점에 실패합니다:

출력:

Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball

캐스트 없이 Dog d = pet;이라고 쓰면 컴파일러는 pet이 어떤 Animal이라는 것만 알므로 컴파일 오류(CS0266: "An explicit conversion exists (are you missing a cast?)")입니다. 객체가 다른 타입일 수 있다면 변수를 붙인 is를 우선 쓰고, 다른 타입이 오는 것이 버그일 때만 일반 캐스트를 쓰세요. 다운캐스트가 잦다면 설계의 문제를 알리는 신호입니다. 보통 그 동작은 기본 클래스의 가상 메서드에 있어야 합니다. is 형태 전체는 패턴 매칭 페이지에서 다룹니다.

상속과 컬렉션

다형성은 요소 단위로 동작하지만, 파생 타입의 제네릭 컬렉션은 기본 타입의 컬렉션이 아닙니다. List<Animal> animals = new List<Dog>();는 컴파일되지 않습니다. 그러면 리스트가 Cat을 받아들이게 되기 때문입니다. 읽기 전용 뷰는 공변적이므로 IEnumerable<Animal> animals = new List<Dog>();은 괜찮습니다. 그 이유는 제네릭 페이지에서 설명합니다.

흔한 실수

  • override를 잊기. 메서드가 경고와 함께 컴파일되고 재정의 대신 조용히 숨깁니다. CS0114를 오류로 취급하세요.
  • 모든 것을 virtual로 만들기. 가상 멤버 하나하나가 언제 호출되고 무엇을 가정해도 되는지에 대해 파생 클래스에 하는 약속입니다. 의도한 확장 지점만 표시하세요.
  • 깊은 계층 구조. 상속이 서너 단계가 되면 어느 버전의 메서드가 실행되는지 알기 어렵습니다. 클래스가 다른 객체를 담고 호출하는 합성이 새 단계를 만드는 것보다 단순한 경우가 많습니다.
  • 생성자에서 가상 메서드 호출하기. 재정의가 파생 생성자의 본문보다 먼저 실행되므로, 그 본문이 대입하는 필드는 여전히 기본값을 담고 있습니다.
  • 코드 재사용만을 위해 상속하기. Stack이 List를 상속하면 호출하는 쪽이 스택 중간에 Insert할 수 있습니다. 대신 private 필드에 List를 두세요.

자주 묻는 질문

C#에서 상속은 어떻게 동작하나요?

클래스는 콜론 뒤에 기본 클래스 하나를 적습니다: class Dog : Animal. 파생 클래스는 기본 클래스의 모든 멤버(필드, 속성, 메서드, 이벤트)를 얻고, 자기 멤버를 추가할 수 있으며, 기본 클래스가 virtual로 표시한 멤버를 재정의할 수 있습니다. 생성자는 상속되지 않고, private 멤버는 객체 안에 있지만 파생 클래스에서 접근할 수 없습니다.

C#에서 virtual과 override의 차이는 무엇인가요?

virtual은 기본 클래스 메서드에 붙어 "파생 클래스가 이것을 바꿔도 된다"고 말합니다. override는 파생 클래스 메서드에 붙어 실제로 바꿉니다. 둘 다 필요합니다. virtual, abstract, override가 아닌 메서드를 재정의하면 CS0506 오류입니다. 가상 메서드가 호출되면 런타임은 변수의 타입이 아니라 객체의 실제 타입에 해당하는 버전을 실행합니다.

C#에서 new와 override의 차이는 무엇인가요?

override는 기본 클래스 변수로 객체를 들고 있는 코드를 포함해 모든 호출하는 쪽에 대해 기본 메서드를 대체합니다. new는 숨기기만 합니다. 객체를 파생 타입으로 보는 코드는 새 메서드를 호출하지만, 기본 타입으로 보는 코드는 여전히 기본 메서드를 호출합니다. 그래서 Base b = new Derived(); b.M();은 override면 Derived.M을, new면 Base.M을 실행합니다.

C#은 다중 상속을 지원하나요?

클래스는 지원하지 않습니다. 클래스는 기본 클래스를 정확히 하나 가지며, 두 개를 나열하면 CS1721 오류입니다. 클래스는 인터페이스를 몇 개든 구현할 수 있으며, C#은 이것으로 "이 타입은 여러 가지를 할 수 있다"를 표현합니다. C# 8부터 인터페이스도 기본 메서드 구현을 가질 수 있습니다.

C#에서 sealed는 무슨 뜻인가요?

sealed 클래스는 기본 클래스로 쓸 수 없으며, 이를 상속하면 CS0509 오류입니다. 예를 들어 string이 sealed입니다. 메서드에서는 sealed override가 계층 구조 아래쪽 클래스들이 다시 재정의하지 못하게 막습니다(CS0239). 상속을 염두에 두고 설계하지 않은 클래스를 sealed로 만드는 것은 합리적인 기본값입니다.

C#에서 기본 클래스 메서드는 어떻게 호출하나요?

파생 클래스 안에서, 보통 재정의 안에서 base.MethodName(...)을 씁니다: public override string Describe() => base.Describe() + " with GPS";. 생성자에서는 매개변수 목록 뒤에 : base(...)를 써서 어느 기본 생성자를 실행할지 고릅니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기