다형성(C# 프로그래밍 가이드)
다형성은 캡슐화, 상속의 뒤를 이어 개체 지향 프로그래밍의 세 번째 기둥으로도 불립니다.다형성은 "여러 모양"을 의미하는 그리스 단어로 두 가지 다른 측면을 가지고 있습니다.
런타임에 파생 클래스의 개체는 메서드 매개 변수 및 컬렉션 또는 배열에서 기본 클래스의 개체로 간주될 수 있습니다.이런 경우 개체의 선언된 형식은 더 이상 개체의 런타임 형식과 일치하지 않습니다.
기본 클래스는 virtual메서드를 정의 및 구현할 수 있으며 파생 클래스는 이를 재정의하여 고유의 정의와 구현을 제공할 수 있습니다.런타임에 클라이언트 코드에서 메서드를 호출하면 CLR은 개체의 런타임 형식을 찾아 가상 메서드의 해당 재정의를 호출합니다.따라서 소스 코드에서 기본 클래스의 메서드를 호출하여 해당 메서드의 파생 클래스 버전이 실행되도록 할 수 있습니다.
가상 메서드를 사용하면 일관된 방식으로 관련 개체 그룹을 사용할 수 있습니다.예를 들어 사용자가 그리기 화면에 다양한 종류의 도형을 만들 수 있는 그리기 응용 프로그램이 있다고 가정합니다.컴파일 타임에는 사용자가 어떤 종류의 도형을 만들지 알 수 없습니다.하지만 응용 프로그램은 생성되는 모든 종류의 도형을 추적하고 사용자의 마우스 동작에 응답하여 이를 업데이트해야 합니다.다형성을 사용하여 이 문제를 두 가지 기본 단계로 해결할 수 있습니다.
각 특정 도형 클래스가 공통 기본 클래스에서 파생되는 클래스 계층 구조를 만듭니다.
기본 클래스 메서드에 대한 단일 호출을 통해 파생 클래스의 적절한 메서드를 호출할 수 있도록 가상 메서드를 사용합니다.
먼저 Shape라는 기본 클래스와 Rectangle, Circle 및 Triangle이라는 파생 클래스를 만듭니다.Shape 클래스에 Draw라는 가상 메서드를 만든 다음 각 파생 클래스에서 각자의 클래스에 해당하는 특정 도형을 그리도록 이 메서드를 재정의합니다.List<Shape> 개체를 만들고 Circle, Triangle 및 Rectangle을 추가합니다.그리기 화면을 업데이트하기 위해 foreach 루프를 사용하여 목록을 반복하면서 목록의 각 Shape에 대해 Draw 메서드를 호출합니다.목록에 있는 각 개체의 선언된 형식은 Shape이지만 호출되는 것은 런타임 형식(각 파생 클래스에 재정의된 메서드 버전)입니다.
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");
}
}
class Circle : Shape
{
public override void Draw()
{
// Code to draw a circle...
Console.WriteLine("Drawing a circle");
base.Draw();
}
}
class Rectangle : Shape
{
public override void Draw()
{
// Code to draw a rectangle...
Console.WriteLine("Drawing a rectangle");
base.Draw();
}
}
class Triangle : Shape
{
public override void Draw()
{
// Code to draw a triangle...
Console.WriteLine("Drawing a triangle");
base.Draw();
}
}
class Program
{
static void Main(string[] args)
{
// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used whereever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
System.Collections.Generic.List<Shape> shapes = new System.Collections.Generic.List<Shape>();
shapes.Add(new Rectangle());
shapes.Add(new Triangle());
shapes.Add(new Circle());
// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (Shape s in shapes)
{
s.Draw();
}
// Keep the console open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* 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를 상속하므로 다형성을 가집니다.
다형성 개요
가상 멤버
파생 클래스가 기본 클래스에서 상속되면 기본 클래스의 메서드, 필드, 속성 및 이벤트를 모두 상속받습니다.파생 클래스 설계자는 다음과 같은 선택을 할 수 있습니다.
기본 클래스의 가상 클래스 재정의
가장 가까운 기본 클래스 메서드를 재정의하지 않고 상속
기본 클래스 구현을 숨기는 해당 멤버의 새로운 비가상 구현 정의
파생 클래스는 기본 클래스 멤버가 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 = (BaseClass)B;
A.DoWork(); // Also calls the new method.
가상 메서드 및 속성을 사용하면 파생 클래스에서 메서드의 기본 클래스 구현을 사용하지 않고 기본 클래스를 확장할 수 있습니다.자세한 내용은 Override 및 New 키워드를 사용하여 버전 관리(C# 프로그래밍 가이드)를 참조하십시오.인터페이스는 파생 클래스에서 구현할 수 있는 메서드 또는 메서드 집합을 정의하는 또 다른 방법을 제공합니다.자세한 내용은 인터페이스(C# 프로그래밍 가이드)를 참조하십시오.
새 멤버로 기본 클래스 멤버 숨기기
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에서 가상 멤버를 선언하고 클래스 B를 A에서 파생한 다음 클래스 C를 B에서 파생하면 클래스 C에서는 가상 멤버를 상속하며, 클래스 B에서 해당 멤버에 대한 재정의를 선언했는지 여부와 상관없이 클래스 C에서 이 가상 멤버를 재정의할 수 있습니다.코드 예제는 다음과 같습니다.
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에서 파생된 클래스에 더 이상 가상이 아닙니다.형식 B 또는 형식 A로 캐스팅하는 경우에도 C의 인스턴스에 대해서는 여전히 가상입니다.봉인된 메서드는 다음 예제와 같이 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 키워드를 사용하여 이 멤버의 기본 클래스 구현을 호출하는 것이 좋습니다.파생 클래스에서 기본 클래스의 동작을 활용하면 파생 클래스 고유의 동작을 구현하는 데만 집중할 수 있습니다.기본 클래스 구현을 호출하지 않으면 파생 클래스에서 기본 클래스의 동작을 대체할 수 있는 자체 동작을 함께 구현해야 합니다.
단원 내용
참고 항목
참조
추상 및 봉인 클래스와 클래스 멤버(C# 프로그래밍 가이드)