제네릭 메서드를 포함한 여러 개의 오버로드된 메서드가 있는 경우, 컴파일러는 제네릭 메서드의 타입 매개변수가 다른 타입으로 다양하게 변경될 수 있음을 고려하여 오버로드된 메서드 중 하나를 선택한다. 그런데 자칫 이러한 동작 방식을 제대로 인지하지 못 할 경우, 응용프로그램이 이상하게 동작할 수도 있다.
하지만, 개발자는 컴파일러가 오버로드된 메서드 중 어떤 메서드르 선태갛는지 정확히 알고 있어야한다.
우선, 코드를 살펴보자.
public class MyBase { }
public interface IMessageWriter
{
void WriteMessage();
}
public class MyDerived : MyBase, IMessageWriter
{
void IMessageWriter.WriteMessage() => WriteLine("Inside MyDerived.WriteMessage");
}
public class AnotherType : IMessageWriter
{
public void WriteMessage() => WriteLine("Inside AnotherType.WriteMessage");
}
class Program
{
static void WriteMessage(MyBase b)
{
WriteLine("Inside WriteMessage(MyBase)");
}
static void WriteMessage<T>(T obj)
{
Write("Inside WriteMessage<T>(T): ");
WriteLine(obj.ToString());
}
static void WriteMessage(IMessageWriter obj)
{
Write("Inside WriteMessage(IMessageWriter): ");
obj.WriteMessage();
}
static void Main(string[] args)
{
MyDerived d = new MyDerived();
WriteLine("Calling Program.WriteMessage");
WriteMessage(d);
WriteLine();
WriteLine("Calling through IMessageWriter interface");
WriteMessage((IMessageWriter)d);
WriteLine();
WriteLine("Cast to base object");
WriteMessage((MyBase)d);
WriteLine();
WriteLine("Another Type test:");
AnotherType anObject = new AnotherType();
WriteMessage(anObject);
WriteLine();
WriteLine("Cast to IMessageWriter:");
WriteMessage((IMessageWriter)anObject);
}
}
이번 파트르 좀 더 이해하고싶다면 위 코드만 보고 Program 클래스의 Main 메서드의 출력값이 어떻게 될지 추측해보자. 제네릭 메서드가 정의되어 있는 경우 필요한 메서드의 원형에 정확하게 부합하도록 닫힌 메서드가 생성된다는 것을 참고하자.
결과
Calling Program.WriteMessage Inside WriteMessage<T>(T): Item14.MyDerived
Calling through IMessageWriter interface Inside WriteMessage(IMessageWriter): Inside MyDerived.WriteMessage
Cast to base object Inside WriteMessage(MyBase)
Another Type test: Inside WriteMessage<T>(T): Item14.AnotherType
Cast to IMessageWriter: Inside WriteMessage(IMessageWriter): Inside AnotherType.WriteMessage
첫 번재 테스트의 결과는 메서드 확인 규칙에서 가장 중요한 개념 중 하나를 보여준다.
MyBase를 상속한 MyDerived 클래스는 WriteMessage(MyBase b) 보다 WriteMessage<T>(T obj)가 더 정확히 일치한다. 왜냐하면 제네릭 메서드의타입 매개변수인 T를 MyDerived로대체하면 컴파일러 입장에서는 요청한 메서드와 정확히 일치하는 메서드를 찾을 수 있기 때문이다. 반면 WriteMessage(MyBasee b)는 암시적 형 변환이 필요하다.
두번째, 세 번째 테스트는 MyBase나 IMessageWriter로의 명시적 형변환이 메서드 확인 규칙에 어떻게 영향을 미치는지를 보여준다.
예상한대로 제네릭 메서드가 아닌 두 메서드가 호출되었다.
네 번재와 마지막 테스트는 상속 관계는 없지만 특정 인터페이스르 구현하고있는 타입(ImessageWriter)을 사용할 경우 어떤 메서드가선택되는지를 보여준다.
네 번재 경우엔 해당 타입의 매개변수를 필요로 하는 메서드가 없어 제네릭 메서드를 이요하고 마지막은 IMessageWriter 인터페이스로 명시적 형 변환을해주었기 때문에 IMessageWriter 인터페이스의 매개변수를 가지는 메서드가 호출되었다.
결론
베이스 클래스와 이로부터 파생된 클래스에 대해서 모두 수행 가능하도록 하기 위해서 베으스 클래스를 이용하여 제네릭을 특화(specialization)하려는 시도는 바람직하지 않다. 특히, 인터페이스에 대해 제네릭을 특화하게 되면 오류가 발생할 가능성이 너무 높다.
아니면 앞서 아이템18(반드시 필요한 제약 조건만 설정하라)처럼 타입 매개변수로 지정할 수 있는 타입별로 각기 특화된 코드를 작성하는 편이 나을 수 있다. 런타임에 타입을 확인하도록 코드를 추가하는 것보단 차라리 컴파일러의 타입 확인기능을확용하는것이 낫다.
그렇지만 런타임에 확인할 조건이 몇 없다면 런타임 중에 타입 확인하는 것도 나쁘지 않다.
참조 - Effective C# <강력한 C# 코드를 구현하는 50가지 전략과 기법, 이펙티브>, 빌 와그너, 김명신, 한빛미디어
'C# > Effective C#' 카테고리의 다른 글
[Effective C#] Item26 제네릭 인터페이스와 논 제네릭 인터페이스를 함께 구현하라 (0) | 2022.10.26 |
---|---|
[Effective C#] Item 25 타입 매개변수로 인스턴스 필드를 만들 필요가 없다면 제네릭 메서드를 정의하라 (0) | 2022.10.25 |
[Effective C#] Item 23 타입 매개변수에 대해 메서드 제약 조건을 설정하려면 델리게이트를 활용하라1 (0) | 2022.10.11 |
[Effective C#] Item 22 공변성과 반공변성을 지원하라 (1) | 2022.10.07 |
[Effective C#] Item 21 타입 매개변수가 IDisposable을 구현한 경우를 대비하여 제네릭 클래스를 작성하라 (1) | 2022.10.05 |