본문 바로가기
책/Effective C#

Effective C# - Item 22 공변성과 반공변성을 지원하라

by 코딩하는 돼징 2023. 12. 14.
반응형

타입의 가변성(Variance)은 특정 타입의 객체를 다른 타입의 객체로 변환할 수 있는 성격을 나타낸다. 이 가변성은 크게 공변성(Convariance)과 반공변성(Contravariance)으로 나눌 수 있다.

 

공변성 (Convariance)
X를 Y로 바꾸어 사용할 수 있는 경우 C<X>를 C<Y>로도 바꾸어 사용할 수 있다면 C<T>는 공변이다.

string[] str = new string[10];
object[] obj = str;

공변성은 배열과 같은 특정 컬렉션 형식에서 파생 타입을 기본 타입으로 사용하는 것을 허용한다 . 위의 예제처럼 string[]은 object[]의 파생타입이므로 string[]을 object[]로 할당할 수 있다.


반공변성(Contravariance)

Y를 X로 바꾸어 사용할 수 있는 경우 C<X>를 C<Y>로도 바꿔 사용할 수 있다면 C<T>는 반공변이다.

원래 지정된 것보다 더 제네릭한(덜 파생적인)형식을 사용할 수 있다.


불가변성(Invariance)

원래 명시된 형식만 사용할 수 있음을 의미한다. 고정 제네릭 형식 매개변수는 공변 및 반공변이 아니다.

string[] str = new string[10];
object[] obj = new object[str.Length];

C# 4.0이전에 배열이 예외적으로 공변적으로 다루어졌다. 이는 제네릭이 아닌 배열에서 요소의 형식을 다룰 때 자동으로 타입 변환이 이루어지는 것이었다. 하지만 이는 안전한 방식이 아니었다.

string[] str = new string[10];
object[] obj = str;

obj[0] = 1;

코드에서 string[]배열을 object[]로 할당하는 것이 가능하다. 하지만 obj[0]=1과 같이 string객체가 아닌 다른 형식의 객체가 포함되는 경우 예외가 발생할 수 있다. 


C# 4.0제네릭 인터페이스와 델리게이트에서 공변성과 반공변성을 지원하기 위해 in과 out키워드가 추가되었다 .이 키워드를 사용하면 유연하게 타입 변환을 할 수 있다.

class Animal
{
    public void Eat() { }
}

class Pig : Animal
{
    public void PigEat() { }
}

class Code_Piggy : Pig
{
    public void Code_PiggyEat() { }
}

 


제네릭 타입에 대한 공변성

out키워드를 사용하여 선언된 제네릭 인터페이스는 공변성을 지원한다. 이는 위의 그림처럼 파생타입('Pig')을 기본 타입('Animal')로 변환할 수 있다.

public interface IEnumerable<out T> : IEnumerable
{
     IEnumerator<T> GetEnumerator();
}

코드 예시

interface ICreate<out T> // out 키워드를 사용하여 공변성 선언
{
    T Create();
}

class PigCreator : ICreate<Pig>
{
    public Pig Create()
    {
        return new Pig();
    }
}

 

ICreate<Pig>는 ICreate<Animal>의 서브타입이다. 따라서 ICreate<Pig>를 ICreate<Animal>로 변환할 수 있다.

ICreate<Pig> pigCreator = new PigCreator();
ICreate<Animal> creator = pigCreator;
Animal ani = creator.Create();
ani.Eat();

제네릭 타입에 대한 반공변성

in키워드를 사용하여 선언된 제네릭 인터페이스는 반공변성을 지원한다.

interface IExample<in T>
{
    void Run(T t);
}

class AnimalTest : IExample<Animal>
{
    public void Run(Animal T)
    {
        T.Eat();
    }
}

IExample<Pig>를 AnimalTest 클래스의 인스턴스에 할당할 수 있는 이유는 반공변성 때문이다. 

 

 

IExample<Pig> pig = new AnimalTest();

따라서 아래와 같이 Pig객체와 Code_Piggy객체가 전달이 가능하다.

pig.Run(new Pig());
pig.Run(new Code_Piggy());

결론

가능하다면 제네릭 인터페이스와 제네릭 델리게이트를 정의할 때는 in이나 out 데코레이터를 반드시 사용하는 것이 좋다. 이렇게 하면 가변성과 관련한 오류를 컴파일러가 사전에 확인할 수 있다. 컴파일러는 인터페이스와 델리게이트를 정의할 때 실수한 부분도 확인하지만 실제로 이를 사용하는 과정에서 저지른 실수도 확인한다.

 

 

 

 

 

본 게시글은 Effective C#을 읽고 정리하였습니다.

 

 

 

반응형

댓글