-
12장. 제네릭(Generics)CLR Via C# 2022. 2. 12. 01:35
제네릭 클래스나 메서드에서는 를 붙여 정의한다. 앞의 T와 같이 데이터 타입으로 지정하는 변수를 타입 매개변수(Type Parameter)라고 한다. 여기서 T는 변수의 타입이 된다. 또한 제네릭 타입을 사용할 때 지정하는 데이터 타입을 타입 인자(Type Argument)라고 부른다.
제네릭의 사용 이점은, 지정된 타입과 호환되지 않는 경우 컴파일 오류가 발생해줘서 타입 안정성을 보장해주고, 캐스팅이 필요없어 간결하고 직관적이며, 박싱이 일어나지 않아 더 나은 성능을 보인다.
Framework Class Library(FCL)에서의 제네릭
대부분의 제네릭 컬렉션 클래스들은
System.Collections.Generic네임스페이스와System.Collections.ObjectModel네임스페이스 내에 정의되어 있으며, 스레드 안정성을 고려하여 작성된 제네릭 컬렉션 클래스들은System.Collections.Concurrent네임스페이스 내에 정의되어 있다.열린 타입과 닫힌 타입
CLR은 응용프로그램에서 사용하는 모든 타입에 대해서 개별적인 자료구조 타입 객체를 만든다. 제네릭 타입 매개변수를 가지는 타입도 당연히 타입 객체를 만든다. 각각의 타입에 대해서 제네릭 타입 매개변수가 등장하는 타입을 열린 타입(Open Type)이라고 한다. CLR은 열린 타입에 대해서는 인스턴스 생성을 허용하지 않는다. (즉 최소 하나 이상 매개변수의 타입이 확정되지 않은 타입)
제네릭 타입을 참조할 때 제네릭 타입 인자들을 지정할 수 있는데, 이 과정에서 모든 타입 인자들을 실제 데이터 타입으로 지정하게 되면, 이를 닫힌 타입(Closed Type)이라고 한다. CLR은 닫힌 타입에 대해서는 인스턴스 생성을 허용한다. (즉 모든매개변수의 타입이 확정된 타입)
각각의 닫힌 타입은 각자의 정적 필드를 가지면, 각각의 닫힌 타입별로 한 번씩 타입 생성자가 실행된다.
제너릭 타입과 상속
제너릭 타입도 하나의 타입이므로, 다른 타입을 상속받아 정의할 수 있다. 다음은 비제너릭 타입 상속을 활용한 노드의 예시이다.
internal class Node { protected Node m_next; public Node(Node next){ m_next = next; } } internal sealed class TypeNode<T> : Node { public T m_data; public TypeNode(T data) : this(data,null){ } public TypeNode(T data, Node next) : base(next){ m_data = data; } }이런 식으로 비제너릭 Node클래스를 정의한 후, 이 클래스를 상속하여 TypeNode 클래스를 작성하면 다양한 타입의 노드를 저장할 수 있다. 만약 Node
공변성과 반공변성 타입 매개변수를 사용하는 제네릭 델리게이트와 제네릭 인터페이스
제네릭 인터페이스, 제네릭 델리게이트도 있는데 이를 통해 값 타입을 박싱 없이 전달할 수 있다.
제네릭 인터페이스와 델리게이트 매개변수들에 공변성과 반공변성의 성절을 나타낼 수 있다. 이러한 기능을 이용하면 제네릭의 타입 인자가 달라도, 특정 제네릭 델리게이트 타입의 변수를 이와 호환되는 다른 델리게이트 타입으로 캐스팅할 수 있다.
- 고정(Invariant) : 제네릭 타입 매개변수는 변경될 수 없음을 의미한다.
- 반공변성(Contra-Variant) : 제네릭 타입 매개변수로 지정한 타입을 해당 타입을 상속한 타입으로(업캐스팅) 변경할 수 있음을 의미한다. in 키워드를 사용하여 적용. 메서드의 매개변수와 같이 입력 용도로 사용되는 인스턴스에 대해서만 적용할 수 있다.
- 공변성(Covariant) : 제네릭 타입 매개변수로 지정한 타입을 해당 타입이 상속한 타입으로(다운캐스팅) 변경할 수 있음을 의미한다. out 키워드를 사용하여 적용. 메서드의 반환 타입과 같이 반환 용도로 사용되는 경우에 대해서만 적용할 수 있다.
public delegate TResult Func<in T, out TResult>(T arg);제네릭 메서드와 타입 유추
제네릭 클래스나 제네릭 구조체 혹은 제네릭 인터페이스 내부에 메서드를 정의할 때, 이 메서드들이 타입 매개변수를 취하도록 정의할 수 있을 것이다. 또한 C#에서는 타입 유추(Type Inference)라는 기능을 제네릭 메서드에 대해 제공한다. 이는 <타입>을 명시적으로 안써도 컴파일러가 메서드를 호출하는 시점에서 자동으로 적절한 타입을 찾아내거나 유추하는 기능이다.
private static void Swap<T>(ref T o1, ref T o2){ ... } ... Int32 n1 = 1, n2 = 2; Swap(ref n1, ref n2); // Swap<Int32> 메서드를 호출한다!검증 가능성과 제약조건
제네릭을 사용하는 타입 내부에서, 제네릭으로 할 수 있는 일은 기본적으로 Object 타입에서 정의하고 있는 메서드를 호출하는 정도이다. 하지만 더욱 다양한 일을 할 수 있게 하는 것이 제약조건이다. 아래 코드의 where T : IComparable부분이 제약조건이다. 이는 T가 IComparable 인터페이스를 구현하는 경우만 작동하고, 구현하지 않았으면 오류가 발생한다.
public static T Minc<T>(T o1, T o2) where T :IComparable<T> { if (o1.CompareTo(o2) < 0 ) return o1; return o2; }제약조건으로 구분되는 오버로딩은 불가능하다. 가상 제네릭 메서드를 재정의할 때 제약조건을 추가하거나 변경할 수 없고, 상위 클래스의 제약조건을 따라간다.
기본 제약조건
2개 이상은 지정할 수 없다. 기본 제약조건으로 지정한 클래스는 타입인자(T)와 같거나 제약조건 클래스를 상속한 클래스여야 한다. System.Object 타입을 제약조건으로 지정할 수 없다.(이는 default값같은 느낌으로 직접 지정은 불가능하다.)
또한 기본 제약조건은 특별하게 class와 struct라는 것을 사용할 수 있다. class *제약조건은 타입인자로 반드시 *참조 타입만을 지정할 수 있다. struct는 타입 인자로 반드시 값 타입만 지정할 수 있다.
확장 제약조건
인터페이스 타입으로 제약사항을 지정하기 위해서 사용된다. 또한 타입 매개변수 제약조건이라는 확장 제약조건도 있다. 이는 매개변수 끼리의 호환성을 검사하기 위해서 사용된다.
private static List<Base> ConvertIList<T, TBase> (IList<T> list) where T : TBase { ... }생성자 제약조건
2개 이상은 지정할 수 없다. 생성자 제약조건을 지정하면, 컴파일러는 지정된 타입 인자가 추상 타입이 아니면서 동시에 매개변수가 없는 기본 public 생성자를 정의하고 있는지를 검사하게 된다. 주의해야 할 것은 이 제약조건을 struct 제약조건과 같이 사용하면 오류가 발생한다. 구조체의 경우 기본 생성자를 이미 가지고 있기 때문이다.
internal sealed class ConstructorConstraint<T> where T : new() { public static T Factory(){ return new T(); } }기타 검증 가능성 관련 문제
- 제약조건의 호환성에 따라서만 캐스팅이 가능하다.
- 제네릭 타입 변수에 기본값을 설정하는 방법은 default(T)를 사용하는 것이다.
- 제네릭 타입 변수와 null은 비교가 가능하다. struct 제약조건이 설정되어 있으면 오류를 의도했다고 판단해 컴파일 오류가 발생한다.
- 두 개의 제네릭 타입 변수 간 비교는 class 제약조건에선 가능하다. 연산자 오버로드 메서드가 정의되어 있으면 그것을 호출한다.
- 제네릭 타입 변수를 피연산자로 사용은 불가능하다. (+,-,*,/ 와 같은 기본적인 연산)
'CLR Via C#' 카테고리의 다른 글
14장. 문자, 문자열, 텍스트 사용하기 (0) 2022.02.21 13장. 인터페이스 (0) 2022.02.16 11장. 이벤트 (0) 2022.02.07 10장. 속성 (0) 2022.02.02 9장. 매개변수 (0) 2022.01.30