숏컷 코드
public abstract class Shape
{
public string Color { get; set; } = "black";
public abstract double Area(); // 반드시 재정의해야 함
public virtual string Describe() // 선택적으로 재정의 가능
=> $"{Color} shape, area={Area():F2}";
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
public override double Area() => Math.PI * Radius * Radius;
}문법
virtual vs abstract
virtual | abstract | |
|---|---|---|
| 기본 구현 | 있음 | 없음 (본문 작성 불가) |
| 파생 클래스 재정의 | 선택 | 필수 |
| 클래스 요건 | 일반 클래스 가능 | abstract class 필요 |
abstract 메서드가 하나라도 있으면 클래스도 abstract여야 합니다. 추상 클래스는 new로 직접 인스턴스화할 수 없습니다.
base() — 부모 생성자와 멤버 접근
자식 생성자는 부모 생성자보다 나중에 실행됩니다. 부모에 기본 생성자(public Base())가 없다면 자식이 : base(...)로 명시해야 합니다.
public class Animal
{
public string Name { get; }
public Animal(string name) => Name = name;
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal
{
public Dog(string name) : base(name) { } // 부모 생성자 호출
public override void Speak()
{
base.Speak(); // 부모 구현 호출 후 확장
Console.WriteLine("Woof!");
}
}sealed — 상속/재정의 차단
클래스에 sealed를 붙이면 더 이상 상속할 수 없습니다. 메서드에 붙이면 파생 클래스가 더 이상 재정의할 수 없습니다.
public sealed class Singleton { } // 상속 불가
public class Base
{
public virtual void Run() { }
}
public class Child : Base
{
public sealed override void Run() { } // Child 이하에서 재정의 불가
}sealed는 성능 최적화(JIT devirtualization)에도 도움이 됩니다.
상속 vs 합성
상속은 "is-a" 관계가 명확할 때 잘 맞습니다. 단순히 기능 몇 개를 재사용하고 싶을 때는 합성(composition)이 더 유연합니다.
// ❌ "Player는 Logger다"는 is-a 관계가 아님
public class Player : Logger { ... }
// ✅ Player가 Logger를 갖는 has-a 관계
public class Player
{
private readonly ILogger _logger;
public Player(ILogger logger) => _logger = logger;
}상속 계층이 깊어지면 부모 클래스의 변경이 모든 자식에 영향을 주고, 의도치 않은 동작이 숨어들기 쉽습니다.
체크포인트
| 키워드 | 역할 |
|---|---|
virtual | 파생 클래스에서 선택적으로 재정의 가능 |
override | 부모의 virtual/abstract를 재정의 |
abstract | 구현 없이 계약만 선언, 반드시 재정의 |
abstract class | 직접 인스턴스화 불가, abstract 멤버 포함 가능 |
sealed | 상속 또는 재정의를 더 이상 허용하지 않음 |
base() | 부모 생성자 또는 멤버 호출 |
주의할 점
생성자 안에서 virtual 메서드를 호출하면 위험합니다. 부모 생성자 실행 시점에 자식 클래스는 아직 초기화되지 않았으므로, 자식에서 재정의한 메서드가 초기화되지 않은 필드에 접근할 수 있습니다.
인터페이스는 구현 없이 계약만 강제하고, 추상 클래스는 일부 구현을 공유할 수 있습니다. 공통 상태나 기본 동작을 물려줘야 한다면 추상 클래스가, 단순히 "이 기능을 제공해야 한다"는 계약만 필요하다면 인터페이스가 더 적합합니다.
참고 링크
2 sources