다음을 통해 공유


다형성

다형성은 흔히 캡슐화와 상속의 뒤를 이어 개체 지향 프로그래밍의 세 번째 특징으로 일컬어집니다. 다형성은 "여러 형태"를 의미하는 그리스어 단어이며 다음과 같은 두 가지 고유한 측면을 가집니다.

  • 런타임에 파생 클래스의 개체가 메서드 매개 변수 및 컬렉션 또는 배열과 같은 위치에서 기본 클래스의 개체로 처리될 수 있습니다. 이러한 다형성이 발생하면 개체의 선언된 형식이 더 이상 해당 런타임 형식과 같지 않습니다.
  • 기본 클래스는 가상 메서드를 정의 및 구현할 수 있으며, 파생 클래스는 이러한 가상 메서드를 재정의할 수 있습니다. 즉, 파생 클래스는 고유한 정의 및 구현을 제공합니다. 런타임에 클라이언트 코드에서 메서드를 호출하면 CLR은 개체의 런타임 형식을 조회하고 가상 메서드의 해당 재정의를 호출합니다. 소스 코드에서 기본 클래스에 대해 메서드를 호출하여 메서드의 파생 클래스 버전이 실행되도록 할 수 있습니다.

가상 메서드를 사용하면 동일한 방식으로 관련 개체 그룹에 대한 작업을 수행할 수 있습니다. 예를 들어, 사용자가 그리기 화면에서 다양한 종류의 도형을 만들 수 있는 그리기 애플리케이션이 있다고 가정합니다. 컴파일 시간에는 사용자가 어떤 특정한 유형의 도형을 만들 것인지 알 수 없습니다. 그러나 애플리케이션은 만들어지는 다양한 모든 형식의 도형을 추적해야 하며, 사용자의 마우스 작업에 따라 이러한 도형을 업데이트해야 합니다. 다음과 같은 기본 두 단계로 다형성을 사용하여 이 문제를 해결할 수 있습니다.

  1. 각 특정 도형 클래스가 공통 기본 클래스에서 파생되는 클래스 계층 구조를 만듭니다.
  2. 가상 메서드를 사용하여 기본 클래스 메서드에 대한 단일 호출을 통해 모든 파생 클래스에 대해 적절한 메서드를 호출합니다.

먼저, Shape라는 기본 클래스를 만들고 Rectangle, CircleTriangle과 같은 파생 클래스를 만듭니다. Shape 클래스에 Draw라는 가상 메서드를 제공하고, 각 파생 클래스에서 이를 재정의하여 클래스가 나타내는 특정 도형을 그립니다. List<Shape> 개체를 만들고 이 개체에 Circle, TriangleRectangle을 추가합니다.

public class Shape
{
    // A few example members
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }

    // Virtual method
    public virtual void Draw()
    {
        Console.WriteLine("Performing base class drawing tasks");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        // Code to draw a circle...
        Console.WriteLine("Drawing a circle");
        base.Draw();
    }
}
public class Rectangle : Shape
{
    public override void Draw()
    {
        // Code to draw a rectangle...
        Console.WriteLine("Drawing a rectangle");
        base.Draw();
    }
}
public class Triangle : Shape
{
    public override void Draw()
    {
        // Code to draw a triangle...
        Console.WriteLine("Drawing a triangle");
        base.Draw();
    }
}

그리기 화면을 업데이트하려면 foreach 루프를 사용하여 목록을 반복하고 목록의 각 Shape 개체에 대해 Draw 메서드를 호출합니다. 목록의 각 개체에 선언된 형식 Shape가 있더라도 이는 호출될 런타임 형식(각 파생 클래스에 있는 메서드의 재정의된 버전)입니다.

// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used wherever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
var shapes = new List<Shape>
{
    new Rectangle(),
    new Triangle(),
    new Circle()
};

// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (var shape in shapes)
{
    shape.Draw();
}
/* Output:
    Drawing a rectangle
    Performing base class drawing tasks
    Drawing a triangle
    Performing base class drawing tasks
    Drawing a circle
    Performing base class drawing tasks
*/

C#에서 모든 형식은 사용자 정의 형식을 포함한 모든 형식이 Object에서 파생되므로 다형 형식입니다.

다형성 개요

가상 멤버

파생 클래스가 기본 클래스에서 상속되는 경우 기본 클래스의 모든 멤버가 포함됩니다. 기본 클래스에 선언된 모든 동작은 파생 클래스의 일부입니다. 이로써 파생 클래스의 개체를 기본 클래스의 개체로 처리할 수 있습니다. 액세스 한정자(public, protected, private 등)는 파생 클래스 구현에서 해당 멤버에 액세스할 수 있는지 여부를 결정합니다. 가상 메서드는 디자이너에게 파생 클래스의 동작에 대한 다양한 선택권을 제공합니다.

  • 파생 클래스가 기본 클래스의 가상 멤버를 재정의하여 새로운 동작을 정의할 수 있습니다.
  • 파생 클래스는 재정의하지 않고 가장 가까운 기본 클래스 메서드를 상속할 수 있어 기존 동작을 유지하지만 추가 파생 클래스가 메서드를 재정의할 수 있습니다.
  • 파생 클래스가 기본 클래스 구현을 숨기는 멤버의 새로운 비가상 구현을 정의할 수 있습니다.

파생 클래스는 기본 클래스 멤버가 virtual 또는 abstract로 선언된 경우에만 기본 클래스 멤버를 재정의할 수 있습니다. 파생 멤버는 override 키워드를 사용하여 메서드가 가상 호출에 참여하도록 되어 있음을 명시적으로 나타내야 합니다. 다음 코드는 예제를 제공합니다.

public class BaseClass
{
    public virtual void DoWork() { }
    public virtual int WorkProperty
    {
        get { return 0; }
    }
}
public class DerivedClass : BaseClass
{
    public override void DoWork() { }
    public override int WorkProperty
    {
        get { return 0; }
    }
}

필드는 가상일 수 없습니다. 메서드, 속성, 이벤트, 인덱서만 가상일 수 있습니다. 파생 클래스가 가상 멤버를 재정의하면 해당 멤버는 해당 클래스의 인스턴스가 기본 클래스의 인스턴스로 액세스되는 경우에도 호출됩니다. 다음 코드는 예제를 제공합니다.

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = B;
A.DoWork();  // Also calls the new method.

가상 메서드 및 속성을 통해 파생 클래스는 메서드의 기본 클래스 구현을 사용할 필요 없이 기본 클래스를 확장할 수 있습니다. 자세한 내용은 Override 및 New 키워드를 사용하여 버전 관리를 참조하세요. 인터페이스는 구현이 파생 클래스에 남겨진 메서드 또는 메서드 집합을 정의하는 또 다른 방법을 제공합니다.

새 멤버로 기본 클래스 멤버 숨기기

파생 클래스가 기본 클래스의 멤버와 동일한 이름을 갖는 멤버를 갖도록 하려면 new 키워드를 사용하여 기본 클래스 멤버를 숨길 수 있습니다. new 키워드는 바꿀 클래스 멤버의 반환 형식 앞에 배치됩니다. 다음 코드는 예제를 제공합니다.

public class BaseClass
{
    public void DoWork() { WorkField++; }
    public int WorkField;
    public int WorkProperty
    {
        get { return 0; }
    }
}

public class DerivedClass : BaseClass
{
    public new void DoWork() { WorkField++; }
    public new int WorkField;
    public new int WorkProperty
    {
        get { return 0; }
    }
}

파생 클래스의 인스턴스를 기본 클래스의 인스턴스로 캐스팅하여 숨겨진 기본 클래스 멤버를 클라이언트 코드에서 액세스할 수 있습니다. 예시:

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.

파생 클래스가 가상 멤버를 재정의하지 못하도록 설정

가상 멤버는 가상 멤버와 원래 가상 멤버를 선언한 클래스에서 선언된 클래스의 개수와 관계없이 가상으로 유지됩니다. 클래스 A가 가상 멤버를 선언하고, 클래스 BA에서 파생되며, 클래스 CB에서 파생되면 클래스 C는 클래스 B가 해당 멤버에 대한 재정의를 선언했는지 여부와 관계없이 가상 멤버를 상속하며, 해당 가상 멤버를 재정의할 수 있습니다. 다음 코드는 예제를 제공합니다.

public class A
{
    public virtual void DoWork() { }
}
public class B : A
{
    public override void DoWork() { }
}

파생 클래스는 재정의를 sealed로 선언하여 가상 상속을 중지할 수 있습니다. 가상 상속을 중지하려면 클래스 멤버 선언에서 override 키워드 앞에 sealed 키워드를 배치해야 합니다. 다음 코드는 예제를 제공합니다.

public class C : B
{
    public sealed override void DoWork() { }
}

앞의 예제에서 DoWork 메서드는 C에서 파생된 모든 클래스에 대해 더 이상 가상이 아닙니다. 그러나 이 메서드는 C의 인스턴스가 형식 B 또는 형식 A로 캐스팅되더라도 여전히 C의 인스턴스에 대해서는 가상입니다. sealed 메서드는 다음 예제에서처럼 new 키워드를 사용하여 파생 클래스로 바꿀 수 있습니다.

public class D : C
{
    public new void DoWork() { }
}

이 경우 형식 D 변수를 사용하여 D에 대해 DoWork을 호출하면 새 DoWork가 호출됩니다. 형식 C, B 또는 A 변수를 사용하여 D의 인스턴스에 액세스하면 DoWork에 대한 호출은 가상 상속의 규칙을 따라 해당 호출을 클래스 C에 대한 DoWork의 구현으로 라우팅합니다.

파생 클래스에서 기본 클래스 가상 멤버에 액세스

메서드 또는 속성을 바꾸었거나 재정의한 파생 클래스는 계속해서 base 키워드를 사용하여 기본 클래스에 대한 메서드 또는 속성에 액세스할 수 있습니다. 다음 코드는 예제를 제공합니다.

public class Base
{
    public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
    public override void DoWork()
    {
        //Perform Derived's work here
        //...
        // Call DoWork on base class
        base.DoWork();
    }
}

자세한 내용은 base를 참조하세요.

참고 항목

가상 멤버는 base를 사용하여 자체 구현에서 해당 멤버의 기본 클래스 구현을 호출하는 것이 좋습니다. 기본 클래스 동작이 발생하도록 하면 파생 클래스가 파생 클래스에 대한 동작 구현에 집중할 수 있습니다. 기본 클래스 구현이 호출되지 않는 경우 해당 동작이 기본 클래스의 동작과 호환되도록 하는 것은 파생 클래스의 책임입니다.