상속은 물려받아 덧붙이는 것이다
게임에 전사와 마법사를 만든다고 해 봅시다.
class Warrior
{
private string name;
private int hp;
public void TakeDamage(int amount) { ... }
public bool IsAlive() { ... }
public void Slash() { ... }
}
class Mage
{
private string name;
private int hp;
public void TakeDamage(int amount) { ... }
public bool IsAlive() { ... }
public void Fireball() { ... }
}
name, hp, TakeDamage, IsAlive 가 똑같습니다. 다른 것은 공격 방법뿐입니다.
앞에서 같은 코드가 두 번 보이면 메서드로 묶을 때라고 했습니다. 여기는 코드 덩어리가 아니라 클래스가 통째로 겹칩니다.
겹치는 부분을 부모에 두고, 자식은 다른 것만 적습니다.
상속은 이렇게 생겼다
class Character
{
protected string name;
protected int hp;
public void TakeDamage(int amount) { ... }
public bool IsAlive() { ... }
}
class Warrior : Character
{
public void Slash() { ... }
}
class Mage : Character
{
public void Fireball() { ... }
}
콜론 뒤에 적은 것이 부모입니다. Warrior 는 Character 를 물려받았습니다.
Warrior w = new Warrior();
w.TakeDamage(30); // 물려받은 것
w.Slash(); // 자기 것
Warrior 안에 TakeDamage 를 안 적었는데 씁니다. 물려받았으니까요.
protected 가 여기서 쓰입니다. private 이면 자식도 못 씁니다.
부모의 필드를 자식에서 써야 하면 protected 입니다.
물려받은 것은 사라지지 않는다
상속을 “복사해서 붙인다” 로 생각하면 안 됩니다.
Warrior 객체 안에는 Character 부분이 실제로 들어 있습니다.
그래서 Warrior 는 Character 이기도 합니다. 이 사실이 다음 이야기의 전부입니다.
부모의 생성자가 먼저 실행된다
자식 객체를 만들면 부모 부분부터 만들어집니다.
class Character
{
public Character(string name)
{
this.name = name;
}
}
class Warrior : Character
{
public Warrior(string name) : base(name)
{
}
}
: base(name) 은 부모 생성자를 먼저 부른다는 뜻입니다.
앞에서 본 : this(...) 와 같은 자리인데, 가리키는 곳이 부모입니다.
부모에 매개변수 없는 생성자가 있으면 base(...) 를 안 적어도 그게 자동으로 불립니다.
없으면 반드시 적어야 합니다. 부모 부분을 만들지 않고 자식만 만들 수는 없습니다.
위로 올라가는 것은 언제나 안전하다
Warrior 는 Character 이기도 하니, Character 자리에 넣을 수 있습니다.
Character c = new Warrior("전사");
이걸 업캐스팅이라고 합니다. 언제나 됩니다. 아무것도 안 적어도 됩니다.
전사는 캐릭터의 조건을 전부 갖추고 있습니다. name 도 hp 도 TakeDamage 도 있습니다.
캐릭터로 다뤄도 부족한 것이 없습니다.
이게 되면 목록으로 묶을 수 있습니다.
List<Character> party = new List<Character>();
party.Add(new Warrior("전사"));
party.Add(new Mage("마법사"));
for (int i = 0; i < party.Count; i++)
{
party[i].TakeDamage(10);
}
전사와 마법사를 한 목록에 담고 같은 코드로 다룹니다. 부모 타입은 여러 자식을 한 이름으로 부르는 방법입니다.
대신 Character 로 보고 있으니 Character 에 있는 것만 쓸 수 있습니다.
Character c = new Warrior("전사");
c.Slash(); // 컴파일 에러
담긴 것은 전사가 맞습니다. 그런데 보고 있는 타입이 Character 입니다.
컴파일러는 그 자리에 마법사가 들어올 수도 있다고 봅니다.
내려가는 것은 확인해야 한다
다시 Warrior 로 보려면 캐스팅합니다. 다운캐스팅입니다.
Character c = new Warrior("전사");
Warrior w = (Warrior)c;
w.Slash();
앞에서 캐스팅은 “내가 책임진다” 는 표시라고 했습니다. 여기서도 같습니다. 틀리면 실행 중에 터집니다.
Character c = new Mage("마법사");
Warrior w = (Warrior)c; // InvalidCastException
그래서 확인하고 내려갑니다. 두 가지 방법이 있습니다.
if (c is Warrior)
{
Warrior w = (Warrior)c;
w.Slash();
}
Warrior w = c as Warrior;
if (w != null)
{
w.Slash();
}
is 는 맞는지 묻고, as 는 시도해 보고 아니면 null 을 줍니다.
as 는 실패해도 안 터집니다. 대신 null 검사를 해야 합니다.
둘을 합친 짧은 문법도 있습니다.
if (c is Warrior w)
{
w.Slash();
}
맞으면 w 에 담아 주고 참이 됩니다. 요즘은 이걸 많이 씁니다.
다만 다운캐스팅이 자주 나오면 설계를 의심해야 합니다.
is 로 타입을 물어보고 갈라지는 코드가 늘어난다면,
타입마다 다르게 동작하도록 만드는 쪽이 대부분 더 낫습니다.
부모는 하나뿐이다
C# 에서 클래스는 부모를 하나만 가질 수 있습니다.
class Duck : Animal, Flyable // 컴파일 에러
부모가 둘이면 양쪽에 같은 이름의 메서드가 있을 때 어느 쪽인지 정할 수가 없습니다. 그 복잡함을 아예 막았습니다.
여러 갈래가 필요하면 인터페이스를 씁니다. 뒤에서 다룹니다.
상속이 늘 답은 아니다
상속은 “무엇인가” 관계일 때만 씁니다.
- 전사는 캐릭터이다 → 상속
- 자동차는 엔진을 가진다 → 상속이 아니라 필드로 가짐
class Car
{
private Engine engine; // 이게 맞습니다
}
class Car : Engine // 자동차는 엔진이 아닙니다
코드를 재사용하고 싶다는 이유만으로 상속하면 이상해집니다. 문장으로 읽어 보면 대체로 답이 나옵니다.
상속이 깊어지는 것도 조심해야 합니다. 부모의 부모의 부모까지 올라가야 뭐가 있는지 알 수 있으면 읽기 어려워집니다.
정리
- 콜론 뒤에 부모를 적으면 그 클래스의 것을 물려받습니다.
- 물려받은 부분은 자식 객체 안에 실제로 들어 있습니다.
protected는 자식까지 쓸 수 있는 표시입니다.private은 자식도 못 씁니다.- 자식을 만들면 부모 생성자가 먼저 실행됩니다.
: base(...)로 넘깁니다. - 자식을 부모 타입으로 보는 것은 언제나 안전합니다. 여러 자식을 한 목록에 담을 수 있습니다.
- 부모로 보고 있으면 부모에 있는 것만 쓸 수 있습니다.
- 다시 내려가려면 캐스팅합니다.
is나as로 확인하고 내려갑니다. - 부모는 하나뿐입니다. 여러 갈래가 필요하면 인터페이스를 씁니다.
- “무엇인가” 일 때만 상속합니다. “가진다” 면 필드로 둡니다.