개발공부/C#

[Effective C#] 아이템 24 : 베이스 클래스나 인터페이스에 대해서 제네릭을 특화하지 말라

개발자 찐빵이 2021. 11. 30. 00:10
728x90

컴파일러는 제네릭 메서드의 타입 매개변수가 다른 타입으로 다양하게 변경될 수 있음을 고려하여 오버로드된 메서드 중 하나를 선택한다.

그런데 이런 동작 방식을 제대로 인지하지 못하면, 프로그램이 이상하게 동작할 수 있다.

namespace _210210_Item24Ex
{
    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("Callling 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);
        }
    }
}
결과
Callling Program.WriteMessage // 1
Inside WriteMessage<T>(T):  _210210_Item24Ex.MyDerived

Calling through IMessageWriter interface // 2
Inside WriteMessage(IMessageWriter):  Inside MyDerived.WriteMessage

Cast to base object // 3
Inside WriteMessage(MyBase)

Another Type test: // 4
Inside WriteMessage<T>(T):  _210210_Item24Ex.AnotherType

Cast to IMessageWriter: // 5
Inside WriteMessage(IMessageWriter):  Inside AnotherType.WriteMessage

설명

첫 번째

MyBase를 상속한 MyDerived 클래스는 WriteMessage(MyBase b)보다 WriteMessage<T> (T obj)에 더 정확히 일치한다.
제네릭 메서드 타입 매개변수인 T를 MyDerived로 대체하면 컴파일러 입장에서는 요청한 메서드와 정확히 일치하는 메서드를 찾을 수 있기 때문이다.
반면 WriteMessage(MyBase b)는 암시적 형변환이 필요하다.

두 번째, 세 번째

MyBase나 IMessageWriter로의 명시적 형변환이 메서드 확인 규칙에 어떻게 영향을 주는지 보여준다.

네 번째, 다섯 번째

상속 관계는 없지만 특정 인터페이스를 구현하고 있는 타입을 사용할 경우 어떤 메서드가 선택되는지 보여준다.

결론

베이스 클래스와 파생된 클래스에 대해 모두 수행 가능하도록 하기 위해 인터페이스에 대해 제네릭을 특화하면 오류가 발생할 가능성이 높다.

런타임에 타입을 확인하도록 코드를 추가하는 것 보단 차라리 컴파일러의 타입 확인 기능을 활용하는 것이 낫다.

반응형