17. 제네릭 가변성(공변 out / 반공변 in)과 배열 공변성의 차이
난이도 중내 리뷰 · C#
해설 · C#
해설 — 제네릭 가변성(공변 out / 반공변 in)과 배열 공변성의 차이
난이도: 중상
요약
IEnumerable<out T> 는 공변(T 를 출력으로만 써서 안전), Action<in T> 는 반공변(T 를 입력으로만 써서 안전)이다. List<T> 는 T 를 입력·출력 양쪽에 써서 불변(invariant). 배열은 역사적 이유로 공변이지만 그 공변은 타입 안전하지 않아 잘못된 저장 시 런타임에 ArrayTypeMismatchException 을 던진다.
예측 출력
Cat Cat
animal Cat
ArrayTypeMismatchException
done
핵심 개념 · 단계별
- (1) 공변
IEnumerable<out T>:out T는 T 가 반환(출력) 위치에만 나타남을 보증한다.IEnumerable<Cat>에서 꺼내는 건 전부 Cat→Animal 로 안전하게 업캐스트되므로IEnumerable<Animal> = List<Cat>대입이 허용된다.PrintAll이 두 Cat 의 이름을 출력 →Cat Cat. - (2) 반공변
Action<in T>:in T는 T 가 인자(입력) 위치에만 나타남을 보증한다.Action<Animal>(어떤 Animal 이든 처리)은Action<Cat>(Cat 만 처리하면 됨)이 요구하는 계약을 만족하므로Action<Cat> = Action<Animal>이 허용된다.assigned(new Cat())→printAnimal(cat)→animal Cat. - (3) 배열 공변성:
Animal[] arr = new Cat[2];는 컴파일된다(배열은 공변). 그러나 실제 객체는Cat[]이라arr[0] = new Animal()은 요소 타입 검사에서 실패 → 런타임에ArrayTypeMismatchException. CLR 이 배열 저장마다 요소 타입을 검사하기 때문. - (4)
List<T>불변:List<Cat>을List<Animal>로 대입하면, 이후Add(new Dog())같은 입력 위치를 통해 Cat 리스트에 Dog 를 넣을 수 있어 타입 안전이 깨진다. 그래서 C# 은List<T>를 불변으로 두고 대입을 컴파일 단계에서 거부(CS0266).
흔한 오해·함정
- "배열 공변이 되니 제네릭도 그럴 것": 아니다. 배열 공변은 제네릭이 없던 시절의 유산이고 런타임 비용(저장 시 타입 체크) + 불안전(예외) 을 동반한다. 제네릭 인터페이스 변성은 컴파일 타임에 안전성을 강제한다.
- 변성은 인터페이스·델리게이트에만: 클래스(
List<T>)에는out/in을 못 붙인다. 변성이 필요하면IEnumerable<T>/IReadOnlyList<T>(공변) 같은 인터페이스로 노출. - 값 형식은 변성 제외:
IEnumerable<int>를IEnumerable<object>로 못 받는다(공변은 참조 변환에만 적용, 박싱은 참조 변환 아님).
면접 포인트
out(출력 전용 → 공변)/in(입력 전용 → 반공변)의 직관과, 컴파일러가 그 제약을 어떻게 강제하는지.- 배열 공변성이 컴파일은 되지만 런타임 예외를 내는 이유, 그리고 그 때문에 치르는 저장 시 타입 체크 비용.
List<T>가 불변인 이유(입력+출력 동시 사용)와, 변성이 필요할 때 읽기 전용 인터페이스로 노출하는 패턴.