나만의 작은 도서관

[C#] 문법 정리 #5. 상속과 다형성(virtual, abstract 키워드) 본문

Unity/문법 및 메소드(System)

[C#] 문법 정리 #5. 상속과 다형성(virtual, abstract 키워드)

pledge24 2024. 11. 21. 17:07

상속(Inheritance)

상속은 기존의 클래스(부모 클래스 또는 상위 클래스)를 확장하거나 재사용하여 새로운 클래스를 정의하는 것을 의미한다. 상속받은 클래스는 부모 클래스와 동일한 멤버(필드, 메서드, 속성 등)를 상속받으며, 상속받은 멤버들을 사용할 수 있게된다. C#에서 상속은 상속받을 클래스 이름 오른쪽에 ': [부모_클래스_이름]'을 추가하여 상속한다.

 

선언부 예제

// 부모 클래스
public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void Eat()
    {
        Console.WriteLine("Animal is eating.");
    }

    public void Sleep()
    {
        Console.WriteLine("Animal is sleeping.");
    }
}

// 자식 클래스
public class Dog : Animal
{

}

public class Cat : Animal
{

}

 

실행부 예제

class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog();
        Cat cat = new Cat();

        dog.Eat();  // Animal is eating.
        cat.Eat();  // Animal is eating.
    }

}

 

상속의 종류

상속의 종류는 다음과 같다.

  • 단일 상속: 하나의 자식 클래스가 하나의 부모 클래스만 상속.
  • 다중 상속: 하나의 자식 클래스가 여러 개의 부모 클래스를 동시에 상속. C#은 다중 상속을 지원하지 않는다.
  • 인터페이스 상속: 클래스가 인터페이스를 상속. C#은 인터페이스는 다중 상속이 가능하다.

한 마디로, C#은 하나의 부모 클래스와 여러 개의 인터페이스를 상속받을 수 있다.

 

C#에서 다중 상속을 지원하지 않는 이유

 

1. 다이아몬드 문제(Diamond Problem)

하나의 클래스에서 다중 상속을 할 경우, 부모 클래스가 같은 클래스로부터 상속받는 경우가 있을 수 있다. 위 사진을 예로 들자면, FatherA 클래스와 FatherB 클래스가 같은 GrandFather클래스로부터 상속받는 상황에서 Son클래스는 FatherA, FatherB로부터 상속 받으려한다. 이 때, Son클래스는 FatherA, FatherB 클래스로부터 동일한 이름의 멤버와 메소드들을 상속받게 되는데( GrandFather 클래스의 멤버), 상속받는 입장인 Son클래스는 둘 중 어느 멤버를 사용해야하는 지 모호해지는 문제가 발생한다.(추가로, FatherA, FatherB 클래스에서 오버라이딩으로 재정의한 메서드라도 존재한다면, 더욱 문제가 커진다.)

 

이렇듯 같은 클래스를 상속받은 서로 다른 부모 클래스들로부터 상속 받았을 때 동일한 이름의 멤버의 상속으로 인한 모호성이 발생하는 문제를 다이아몬드 문제라고 한다. (추가로, 다이아몬드 형태가 아니더라도 동일한 이름의 멤버의 상속으로 인한 모호성은 충분히 발생할 수 있다.)

 

이러한 모호성을 해결하기 위한 규칙이 필요하게 되는데, 이로 인해 코드가 복잡해지고 가독성이 저하될 수 있다.

 

2. 설계의 복잡성 증가

다중 상속을 허용하면 클래스 간의 관계가 복잡해지게 된다. 클래스가 다중 상속을 받을 경우, 어떤 클래스로부터 어떤 멤버를 상속받을 지 결정하는 것이 더욱 어려워진다. 이렇듯 다중 상속을 하게되면 상속 관계를 파악하기 어려워지고, 코드의 유지 보수를 더욱 힘들게 하는 요인이 될 수 있다.

 

결론

C#은 다중 설계를 했을 때 클래스 간 관계의 복잡성 증가, 멤버 이름 충돌 문제 해결의 어려움을 이유로 설계의 일관성과 단순성을 유지하기 위해 다중 상속을 지원하지 않는다.


다형성(Polymorphism)

다형성은 같은 타입, 같은 이름의 메서드가 다양한 동작들을 수행할 수 있는 특성을 말한다. 다형성은 메서드 오버로딩(Overloading)과 오버라이딩(Overriding)으로 구현되며, 오버라이딩은 가상 메서드(Virtual Method)와 추상 클래스(Abstract Class)를 통해 구현된다. 주로 같은 부모 타입으로 묶어서 메서드를 실행해도 각각의 자식 메서드가 호출되도록 하고 싶을 때 사용한다.

 

오버라이딩(Overriding)

오버라이딩은 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 재정의하는 것을 의미한다. 부모-자식 관계에 있는 클래스 간에 발생하며, 오버라이딩할 메서드는 내부 로직을 제외한 나머지 부분(메서드의 이름, 매개변수 및 반환 타입)이 동일해야한다.

 

가상 메서드(Virtual Method)

가상 메서드는 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 재정의 할 수 있도록 허용한 메서드이다. 가상 메서드는 virtual 키워드로 선언하며, 가상 메서드로 선언된 메서드는 자식 클래스에서 override 키워드를 통해 오버라이딩 하거나, 오버라이딩 하지않고 그대로 사용할 수도 있다.

 

+) 왜 가상(virtual)이라고 이름이 붙었는지 궁금해서 좀 찾아봤다. C#이나 C++에서 가상(virtual)이라는 개념은 "가상 세계에서는 상황에 따라 다른 모습을 가질 수 있다"는 개념을 가져와 컴파일 시점에서 호출한 메서드가 고정되지 않고 런타임에서 동적으로 바인딩되는(객체 타입에 따라 호출하는 메서드가 달라지는) 것을 가상이라고 하는 것 같더라...

 

선언부 예제

public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing a shape.");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}

public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a rectangle.");
    }
}

 

실행부 예제

class Program
{
    static void Main(string[] args)
    {
        Shape shape1 = new Circle();
        Shape shape2 = new Rectangle();

        List<Shape> shapes = new List<Shape>() {shape1, shape2};
.
        
        foreach (Shape shape in shapes)
        {
            shape.Draw();
        }
        
        // 가상 메서드를 사용해서 가능한 결과(동적 바인딩이 되었기 때문!)
        // [출력 결과]
        // Drawing a circle.
        // Drawing a rectangle.

    }

}

 

추상 클래스(Abstract Class)

추상 클래스는 직접적으로 인스턴스를 생성할 수 없는 클래스이다. 주로 상속을 위한 베이스 클래스로 사용되며, 메서드 구현에 강제성을 부여하는 것이 목적이다. 추상 클래스는 abstract 키워드를 사용하여 선언되며, 추상 메서드를 포함할 수 있게된다. 추상 메서드 선언 시, 상속받은 클래스는 해당 클래스를 override를 통해 반드시 구현해야한다.

 

선언부 예제

abstract class Shape
{
    public abstract void Draw();
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

 

실행부 예제

List<Shape> shapes = new List<Shape>();
shapes.Add(new Circle());
shapes.Add(new Square());

foreach (Shape shape in shapes)
{
    shape.Draw();
}

// [출력 결과]
// Drawing a circle.
// Drawing a square.