← 문제로

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> 가 불변인 이유(입력+출력 동시 사용)와, 변성이 필요할 때 읽기 전용 인터페이스로 노출하는 패턴.