C# 상속
개념
상속이란 기존에 정의된 클래스의 모든 특성과 동작을 새로운 클래스가 물려받는 것을 말한다.
이때, 물려주는 상위 클래스를 부모 클래스(Parent Class)라 부르고,
물려받는 하위 클래스를 자식 클래스(Child Class)라 부른다.
상속을 통해 클래스 간의 계층 구조를 만들고 다형성을 지원함으로써, 코드의 재사용성과 구조화를 촉진할 수 있다.
유니티에서 스크립트를 생성할 때도 MonoBehaviour 클래스를 자동으로 상속받게 하여 컴포넌트, 코루틴 등 유니티에서 제공하는 여러 기능들을 쉽게 사용할 수 있게 해주고 있다.
// 부모 클래스
public class Unit
{
// 공통적으로 구현 될 코드
}
// 자식 클래스 - 아군
public class AgentUnit : Unit
{
// AgentUnit 클래스에서만 구현 될 코드
}
// 자식 클래스 - 적군
public class EnemyUnit : Unit
{
// EnemyUnit 클래스에서만 구현 될 코드
}
상속은 클래스 옆에 ' : '를 작성 후, 상속받을 부모 클래스를 작성한다.
상속에 대해 이어 설명하기 전에, 꼭 알아야 하는 개념이 더 있다.
Virtual, Abstract, Override
virtual은 가상 클래스, abstract는 추상 클래스라 하며, override는 재정의라는 뜻이다.
우선 다음의 코드를 통해 각각의 개념에 대해 알아보자.
// 추상 클래스를 사용하므로, abstract 사용
public abstract class Unit : MonoBehaviour
{
protected HealthAbility _healthAbility;
// 가상 클래스
public virtual void Start()
{
TryGetComponent(out _healthAbility);
}
public virtual void Defense(int damage)
{
_healthAbility.Damage(damage);
}
// 추상 클래스
public abstract void Attack();
}
public class AgentUnit : Unit
{
// 재정의
public override void Defense(int damage)
{
// 부모의 Defense 메소드 호출
base.Defense(damage);
Debug.Log($"{gameObject.name}가 {damage}만큼의 피해를 입었습니다.");
}
public override void Attack()
{
Debug.Log($"{gameObject.name}가 공격했습니다.");
AttackExcute();
}
// 일반 메소드
private void AttackExcute()
{
Debug.Log("공격 실행");
}
}
코드를 보면 가상 클래스와 추상 클래스의 차이를 알 수 있다.
가상 클래스는 부모 클래스에서 기능을 구현 후, 자식 클래스에서 재정의 할 수 있는 클래스이다.
할 수 있는 클래스이므로, 재정의를 안해도 된다면 Start 메소드 같이 자식 클래스에서 생략할 수 있다.
추상 클래스는 부모 클래스에서 기능을 구현할 수 없다.
대신 자식 클래스에서 반드시 재정의를 통한 기능 구현을 해주어야 한다.
또한 추상 클래스를 사용할 때에는 클래스를 선언할 때에도 abstract를 붙여 추상 클래스임을 알려야 한다.
재정의할 때에는 가상 메소드와 추상 메소드 모두 override를 통해 재정의를 해주어야 한다.
재정의를 하면 원래 부모 클래스의 메소드는 작동하지 않기에,
부모클래스의 메소드를 base.Defense()와 같이 호출해주어야 한다.
다시 말해, 재정의하지 않은 가상 메소드는 부모 클래스의 메소드가 실행되는 결과를 얻을 수 있다.
지금까지, 가상 클래스와 추성 클래스를 통한 상속에 대해 알아보았는데
이 두 방법을 사용한 상속은 다중상속을 지원하지 않는다.
하지만 다중 상속을 할 수 있도록 지원되는 기능이 있는데..
Interface
인터페이스는 속성, 메소드, 이벤트 이외의 것을 가질 수 없고, 다중 상속을 구현할 수 있다.
인터페이스가 하는 역할을 쉽게 보자면, 어떠한 클래스가 가져야 할 구조, 형태를 선언하고, 이를 구현하도록 강제하는 역할을 한다.
인터페이스의 이름은 네임 컨벤션에 의해 'I'로 시작해야한다.
한 번 코드를 보도록 하자.
public interface ISubSystem
{
void Initialize();
void Deinitialize();
}
public class BattleManage : Singleton<BattleManager>
{
private List<ISubSystem> _subSystems = new List<ISubSystem>();
void Awake()
{
var systems = GetComponentsInChildren<ISubSystem>(true);
_subSystems.AddRange(systems);
}
public void Initialize()
{
foreach (var system in _subSystems)
{
system.Initialize();
}
}
}
public class BattleStateSystem : MonoBehaviour, ISubSystem
{
public void Initialize()
{
Debug.Log("상태 시스템 초기화");
}
public void Deinitialize()
{
Debug.Log("상태 시스템 초기화 해제");
}
}
public class TimeSystem : MonoBehaviour, ISubSystem
{
public void Initialize()
{
Debug.Log("시간 시스템 초기화");
}
public void Deinitialize()
{
Debug.Log("시간 시스템 초기화 해제");
}
}
인터페이스는 public 선언을 안해도 무조건 public으로 취급되며,
이를 상속받아 구현한 필드, 메소드, 이벤트도 무조건 public으로 선언해야 한다.
또한 인터페이스는 override를 선언할 필요가 없다.