Genius DM

Generics > Covariance and Contravariance 본문

.NET

Generics > Covariance and Contravariance

Damon Jung 2018. 7. 14. 07:40

제너릭의 공변성과 반공변성


제너릭에서 공변성과 반공변성 하나의 용어이자, 최초에 정의된 타입보다 더 정확하거나, 덜 정확한 파생 타입을 사용할 수 있는지에 대한 여부를 결정하는 특성이다. 무슨 말인지 이해가 안 될지도 모른다, 하지만 코드를 보면 이해가 빠를 것이다.


샘플 모델



- Cup > Base 클래스.

- PlasticCup > Cup 을 상속하는 파생 클래스.

- PaperCup > Cup 을 상속하는 파생 클래스.



이렇게 베이스 클래스와 파생 클래스는 다형성으로 인해 아래와 같은 할당이 가능해진다.


IEnumerable<PlasticCup> plasticCups = new List<PlasticCup>();


IEnumerable<Cup> cups = plasticCups;


단순히 베이스 클래스와 파생 클래스 관계 뿐만 아니라, List<T> 클래스는 IEnumerable<T> 인터페이스를 상속하기에 저런 값 할당이 가능한 것이다. 방금 설명한 것이 바로 공변성이다. 반대로 반공변성은 어떨까? "반" 이라고 했으니, 공변성과 반대되는 개념일 것만 같다. 베이스 클래스도, 파생 클래스에 값을 할당할 수 있어야 할 것만 같다. 파생 클래스를 베이스 클래스에 할당하는 것이 가능하니, 베이스 클래스 또한 파생 클래스로 할당할 수 있지 않을까? 또, 파생 클래스를 베이스 클래스에 할당할 수 있으니, (List<Cup> cups) 로 메서드 파라메터를 지정하면, 파생 클래스들을 넘길 수 있지 않을까?


// Case 1 void ContravarianceTest(List<Cub> cubs) { ... }; // Call ContravarianceTest {     // Case 1-A     ContravarianceTest(new List<PlasticCub>());     // Case 1-B     ContravarianceTest(new List<PaperCub>());     // Case 1-C     ContravarianceTest(new List<Cub>()); }


// Case 2

IEnumerable<PlasticCup> plasticCups = new List<Cup>();


어떤 코드만이 컴파일 타임 에러에서 살아남을 수 있을까? 결론부터 얘기하면 Case 1-C 를 제외한 나머지 코드는 컴파일 타임에서 에러가 발생할 것이다. 그렇다면 왜 안되는가? 답은 심플하다. IEnumerable<T> 는 반공변성을 지원하지 않는다. 이것은 선험적으로 훈련이 되어있는 상태라면, 굳이 반공변성이라는 개념 자체를 몰라도 아래와 같이 설명할 수 있다.


IList<Cup> plasticCups = new List<PlasticCup>();


plasticCups.Add(new PaperCup()); // Oh no... 이래서 안 되는 거에요;


IEnumerable<T> 가 반공변성을 지원했다면, 위와 같은 코드가 허용 되었을 것이다. 보기만 해도 PlasticCup 에 대한 타입을 확신할 수 없어 보인다. 그렇다면 모든 제너릭에 반공변성이 없을까? 그렇지 않다. 아래 예제를 보자


Action<Cub> cubAction = (cub) => { Console.WriteLine(cub.GetType().Name); };


Action<PlasticCub> plasticAction = cubAction;


plasticAction(new PlasticCub());


IEnumerable<T> 와는 다르게, 파생 클래스 타입에 베이스 클래스 타입으로 선언한 Action<T> 를 할당할 수 있다. 100% 안전한 타입이고, 컴파일 타임에서도 전혀 문제 없고, 당연히 런타임에서도 문제가 되지 않는다. Action<T> 는 왜 IEnumerable<T> 와 다르게 파생 클래스에 베이스 클래스를 거꾸로 대입시키는 것이 가능할까? 이것 역시 단순하게 Action<T> 가 반공변성을 지원하기 때문이다.


이제 궁금해지는 것이 무엇이 공변성이 가능하고, 무엇이 반공변성이 가능한가? 이다. 아래 표를 보자.


타입

공변 타입 파라메터반공변 타입 파라메터
Action<T>Yes
Comparison<T>Yes
Converter<TInput,TOutput>YesYes
Func<TResult>Yes
Func<T,TResult>YesYes
IComparable<T>Yes
Predicate<T>Yes
IComparer<T>Yes
IEnumerable<T>Yes
IEnumerator<T>Yes
IEqualityComparer<T>Yes
IGrouping<TKey,TElement>Yes
IOrderedEnumerable<TElement>Yes
IOrderedQueryable<T>Yes
IQueryable<T>Yes


아마 공변성과 반공변성은 C# 에서 가장 잘 알려지지 않은 특성 중에 하나가 아닐까 생각한다.















Generics. Convariance and Contravariance


Convariance and contravariance are terms and also a kind of property to be able to use a specifically derived type or a less derived type than originally specified. This may not come straight to your head, let's see some codes.


Sample Model



- Cup > A base class.

- PlasticCup > A derived class implementing Cup.

- PaperCup > A dervied class implementing Cup.


Thanks for polymorphism, you can assign the value like this.


IEnumerable<PlasticCup> plasticCups = new List<PlasticCup>();

IEnumerable<Cup> cups = plasticCups;


This is possible not only for the polymorphism in OOP but also for List<T> to implement IEnumerable<T> interface and this is called convariance. What about the opposite of the convariance? It's called contravariance and the contra comes before the word, which means "Not". Something is NOT convariance... um, it seems like something is supposed to work backwards. It's safe to say that, with contravariance, we can assign the base class to the derived class, it looks like and if we have a method that takes (List<Cup> cups) as an argument, I think it's going to be okay to pass in the derived types.


// Case 1 void ContravarianceTest(List<Cub> cubs) { ... }; // Call ContravarianceTest { // Case 1-A ContravarianceTest(new List<PlasticCub>()); // Case 1-B ContravarianceTest(new List<PaperCub>()); // Case 1-C ContravarianceTest(new List<Cub>()); } // Case 2 IEnumerable<PlasticCup> plasticCups = new List<Cup>();


Which cases are you 100% sure will survive the compile time type check ups? Well directly to the point, None of them will pass the screening process except Case1-C, which makes me wonder why this cannot be done? The answer will be pretty simple. IEnumerable<T> doesn't support contravariance. You've learned this lesson in a real programming life and you already know why this behavior cannot be done. Probably you can explain to other colleagues that "You know, accepting contravariance in IEnumerable<T> will result in something like this...."


IList<Cup> plasticCups = new List<PlasticCup>(); plasticCups.Add(new PaperCup()); // See the point? you just can't.... and shouldn't even try this !


If IEnumerable<T> supported contravariance, something above could have happened and this does look like a serious problem. How can you be sure that the list contains genuine PlasticCup classes only? However, there're some types that support contravariance in generics.


Action<Cub> cubAction = (cub) => { Console.WriteLine(cub.GetType().Name); };


Action<PlasticCub> plasticAction = cubAction;


plasticAction(new PlasticCub());


Unlike IEnumerable<T>, Action<T> allows a base type to be assigned to its derived type. It's a 100% type-safe code and it's going to certainly survive from the compile time check-ups and of course, no problem at the runtime. How can this be done? Well simply enough again, Action<T> just supports contravariance.


Now I'm seriously wondering what kind of types support convariance and contravariance and what aren't. See this table below.


Type

Convariant type parameter

Contravariant type parameter

Action<T>Yes
Comparison<T>Yes
Converter<TInput,TOutput>YesYes
Func<TResult>Yes
Func<T,TResult>YesYes
IComparable<T>Yes
Predicate<T>Yes
IComparer<T>Yes
IEnumerable<T>Yes
IEnumerator<T>Yes
IEqualityComparer<T>Yes
IGrouping<TKey,TElement>Yes
IOrderedEnumerable<TElement>Yes
IOrderedQueryable<T>Yes
IQueryable<T>Yes

Well, convariance and contravariance is probably one of the least known stuff in C#.

























'.NET' 카테고리의 다른 글

C# > Span<T>  (0) 2018.07.31
C# > Tuples  (0) 2018.07.31
IdentityServer > Storing an access token in IdentityServer3  (0) 2018.07.08
DataAnnotation > Recursive validation for collection items  (0) 2017.12.29
DataAnnotation > Make a custom attribute  (0) 2017.12.22
Comments