타입의 가변성(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#을 읽고 정리하였습니다.
'책 > Effective C#' 카테고리의 다른 글
Effective C# - Item 24 베이스 클래스나 인터페이스에 대해서 제네릭을 특화하지말라 (1) | 2023.12.18 |
---|---|
Effective C# - Item23 타입 매개변수에 대해 메서드 제약 조건을 설정하려면 델리게이트를 활용하라 (0) | 2023.12.15 |
Effective C# - Item 21 타입 매개 변수가 IDisposable을 구현한 경우를 대비하여 제네릭 클래스를 작성하라 (0) | 2023.12.10 |
Effective C# - Item20 IComparable<T>와 IComparer<T>를 이용하여 객체의 선후 관계를 정의하라 (0) | 2023.12.10 |
Effective C# - Item19 런타임에 타입을 확인하여 최적의 알고리즘을 사용하라 (0) | 2023.12.10 |
댓글