C#으로 이해하는 자료구조 전자책
C# 제네릭에서의 불변성과 가변성

[제목] C# 제네릭에서의 불변성과 가변성

C# 제네릭에서의 불변성과 가변성

C#에서 가변성은 배열, 델리게이트, 제네릭에 적용될 수 있는데, 여기서는 제네릭에 포커스하여 가변성을 살펴본다.

C# 제네릭(generics)은 불변성(invariance)와 가변성(variance)를 지원한다. C# 3.0 / .NET 3.5 까지는 제네릭은 불변성만을 지원하였으나, C# 4.0 / .NET 4.0 부터는 제네릭에서 불변성과 가변성을 모두 지원하였다.

가변성(variance)이란 암묵적인 레퍼런스 변환(implict reference conversion)을 가능하게 하는 기능으로서, 제네릭 레퍼런스를 하위클래스(서브클래스)에서 상위클래스의 레퍼런스로 변환하거나 그 반대로 상위에서 하위 타입의 레퍼런스로 변환할 수 있는 것을 말한다.

가변성은 크게 공변성(covariance)과 반공변성(contravariance)로 나뉘는데, Covariance은 하위 타입(more derived type)에서 상위 타입(less derived type)으로 레퍼런스를 변환할 수 있는 것을 말하며, Contravariance는 Covariance과 반대로 상위 타입(less derived type)에서 하위 타입(more derived type)으로 레퍼런스를 변환할 수 있는 것을 말한다. 그리고, 불변성이란 이러한 가변성을 사용할 수 없다는 것을 의미한다.

C# 제네릭에서의 가변성은 제네릭 인터페이스와 제네릭 델리게이트에서 사용할 수 있으며, 가변성은 암묵적으로 레퍼런스를 변환할 수 있는 능력이므로, Value Type에는 사용할 수 없고, Reference Type에만 적용된다. (주: Non-generic "델리게이트"에도 가변성 지원됨)

C# 제네릭에서 불변성/가변성은 타입 T 파라미터를 어떻게 사용하는가에 따라 결정된다. 즉, 타입 T 파라미터를 <out T>로 사용하면 공변성(covariance)이 되고, <in T>로 사용하면 반공변성(contravariance)이 되며, in이나 out 없이 <T>로 사용하면 불변성(invariance)이 된다.

공변성(Covariance) 제네릭 인터페이스에서 공변성을 사용하기 위해서는, 타입 T를 out T 로 지정하고 해당 타입 T 파라미터를 메서드의 리턴 타입으로 사용한다. 아래 예제는 제네릭 인터페이스에서의 공변성을 예시한 것으로서, ICreate 인터페이스는 <out T> 를 가지며, T를 Create() 메서드의 리턴 타입으로 지정하고 있다.

class Animal
{
    public int Age { get; set; }
    public void Move() { }
}
class Dog : Animal
{
    public void Bark() { }
}
class K9 : Dog
{
    public void FindDrug() { }
}

// 제네릭 인터페이스: Covariance 
interface ICreate<out T>
{
    T Create();
}

class DogFactory : ICreate<Dog>
{
    public Dog Create()
    {
        return new Dog();
    }
}

class TestClass
{
    public void Test()
    {
        ICreate<Dog> dogCreator = new DogFactory();
        ICreate<Animal> creator = (ICreate<Dog> )dogCreator;
        Animal ani = creator.Create();
        ani.Move();
    }
}

위 예제에서 ICreate<Animal> creator = (ICreate<Dog>)dogCreator 와 같은 레퍼런스 변환을 수행하였고, creator.Create() 메서드는 Animal 객체를 리턴하고 있다. 즉, 하위 클래스보다 상위에 있는 클래스의 레퍼런스를 리턴받아서 상위 클래스의 멤버(Move 메서드)를 사용함으로서 안전하게 (상위 클래스의) 멤버들을 사용할 수 있게 된 것이다. 만약 ICreate<K9> creator = (ICreate<Dog>)dogCreator 와 같이 Animal 대신 K9 클래스를 사용한다면, 리턴된 객체가 K9 타입이 되고, 따라서 Dog 클래스가 갖지 않은 K9 멤버를 사용할 수 있게 되어 타입 변환 에러가 발생하게 된다. 이러한 에러는 런타임 뿐만 아니라 컴파일타임에서 발생하여 Visual Studio에서 에러를 즉시 표시하게 된다.

반공변성(Contravariance) 제네릭 인터페이스에서 반공변성을 사용하기 위해서는, 타입 T를 in T 로 지정하고 해당 타입 T 파라미터를 메서드의 입력 파라미터나 속성의 set에서 사용한다. 아래 예제는 제네릭 인터페이스에서의 반공변성을 예시한 것으로서, ITest 인터페이스는 <in T>를 가지며, T를 Run() 메서드의 입력 파라미터 타입으로 사용하고 있다.

// 제네릭 인터페이스: Contravariance 
interface ITest<in T>
{
    void Run(T t);
}

class AnimalTest : ITest<Animal>
{
    public void Run(Animal t)
    {
        t.Move();
    }
}

class TestClass
{
    public void Test()
    {
        // contravariance 
        ITest<Dog> d = (ITest<Animal>) new AnimalTest();
        d.Run(new Dog());
        d.Run(new K9());
    }
}

위 예제에서 ITest<Dog> d = (ITest<Animal>) new AnimalTest(); 와 같은 레퍼런스 변환을 수행하였고, d.Run(new Dog()) 문장에서 Animal 타입보다 하위클래스인 객체를 받아들였다. 이때 Dog 객체는 AnimalTest.Run() 메서드에서 입력파라미터 타입인 Animal 타입으로 캐스팅되고, Run() 메서드 안에서는 Animal 클래스의 멤버만을 사용하므로, 타입 변환에 따른 오류없이 안전하게 사용할 수 있게 된다.

.NET 제네릭 타입들

.NET 4.0부터 제네릭에서의 가변성을 지원하였는데, 위와 같이 개발자가 가변성 사용할 수 있게 되었다는 것 이상으로 .NET Framework의 여러 타입들이 가변성을 지원하게 되었다는데에 큰 의미가 있다.

공변성(Covariance)을 지원하는 대표적인 .NET 타입으로 IEnumerable<out T>, IEnumerator<out T>, IQueryable<out T>, Func<out T> 등이 있으며, 반공변성(Contravariance)을 지원하는 대표적인 .NET 타입으로 IComparer<in T>, IComparable<in T>, IEqualityComparer<in T>, Action<in T> 등이 있다.

아래 예제는 .NET 타입인 IEnumerable<out T> 과 IComparer<in T> 를 사용하여 공변성과 반공변성을 사용하는 코드를 예시한 것이다.

class TestClass
{
    public void Test()
    {
        // covariance 
        IEnumerable<Animal> animals = new List<Dog>() { new Dog() };
        animals.GetEnumerator().Current.Move();

        // contravariance 
        IComparer<Dog> dogComparer = new AnimalComparer();
        Dog dogA = new Dog { Age = 10 };
        Dog dogB = new Dog { Age = 10 };
        int result = dogComparer.Compare(dogA, dogB);
    }
}

class AnimalComparer : IComparer<Animal>
{
    int IComparer<Animal>.Compare(Animal a, Animal b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.Age.CompareTo(b.Age);
    }
}


본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.