언어/C#

C# 제너릭 - 1

tsyang 2021. 1. 9. 14:38

일반


제너릭 타입이나 메서드를 정의할 때 ,<T>를 붙여 데이터 타입을 지정하지 않고도 동작할 수 있음을 나타내는데 이 때 T와 같이 데이터 타입으로 지정하는 변수를 타입 매개변수라고 한다. 또한 제네릭 타입이나 메서드를 사용할 때, 지정하는 데이터 타입을 타입 인자라고 한다.

 

마이크로소프트의 디자인 가이드라인에 따르면 제네릭 매개변수는 첫 글자를 대문자로 시작하고 접두사로 T를 붙인다. (TValue, TKey...)

 

제네릭이 개발자들에게 제공하는 이점은 다음과 같다.

 

  • 소스 코드 보호 : 제네릭 알고리즘을 사용하기 위해 알고리즘을 구현하는 소스 코드가 반드시 필요하지는 않다. (먼소리지)
  • 타입 안정성 : 지정된 타입과 호환하는 타입에 대해서만 사용할 수 있도록 한다. 그렇지 않으면 컴파일 에러가 발생한다.
  • 간결한 코드 : 간결함
  • 더 나은 성능 : 제네릭 이전에는 모든 타입을 Object 타입으로 다뤄야했다. 따라서 값 타입을 사용한 경우 박싱과 언박싱이 일어나는데 제네릭은 값 타입의 인스턴스를 값 자체로 전달할 수 있으므로 박싱이 수행되지 않는다. 따라서 성능이 더 좋다.

 

 

제네릭 하부 구조


열린 타입과 닫힌 타입

 

CLR은 응용프로그램에서 사용하는 모든 타입에 대해서 개별적으로 내부적인 자료 구조를 생성한다. 이것을 타입 객체라고 부른다. 

 

//부분적으로만 타입 인자가 지정된 열린 타입
class DictionaryStringKey<TValue>
    : Dictionary<string, TValue> { } 


public static class Program
{
    public static void Main()
    {
        Object o = null;

        //두 개의 매개변수를 지정하지 않은 열린 타입
        Type t1 = typeof(Dictionary<,>);

        //한 개의 매개변수를 지정하지 않은 열린 타입
        Type t2 = typeof(DictionaryStringKey<>);

        // 닫힌 타입
        Type t3 = typeof(DictionaryStringKey<int>);

        o = Activator.CreateInstance(t1); // 실패 : ArgumentException 발생
        o = Activator.CreateInstance(t2); // 실패 : ArgumentException 발생
        o = Activator.CreateInstance(t3); // 성공
    }
}

 

CLR은 제네릭 타입 매개변수를 가지는 타입에 대해서도 역시 각각의 타입 객체를 만든다. 제네릭 타입 매개변수를 가질 수 있는 참조, 값, 인터페이스, 델리게이트 타입이 그 대상이다. 각각의 타입에 대해서 제네릭 타입 매개변수가 등장하는 타입을 열린 타입이라고 한다. CLR은 열린 타입에 대해서는 인스턴스 생성을 허용하지 않는다. 

 

그 반대의 경우는 닫힌 타입이라고 하며 닫힌 타입은 인스턴스를 생성할 수 있다. 

 

 

제네릭 타입과 상속

제네릭 타입도 타입이므로, 다른 타입을 상속받아 정의할 수 있다. 

 

class Node<T>
{
    public T m_data;
    public Node<T> m_next;

    //생성자..
    public Node(T data, Node<T> next = null)
    {
        m_data = data; m_next = next;
    }
}

public static class Program
{
    public static void Main()
    {
        Node<char> head = new Node<char>('C');
        head = new Node<char>('B', head);
        head = new Node<char>('A', head);
    }
}

위와 같은 연결 리스트의 노드 클래스가 있다. 이 Node 클래스를 보면 m_next 필드는 반드시 동일한 데이터 타입을 지정한 다른 Node 인스턴스를 참조해야 한다. 따라서 char 노드, string 노드, int 노드를 하나의 연결리스트에 연결하려 한다면 불가능하다. 만약 Node<Object>로 만든다면 여러 타입을 연결하는 노드를 만들 수 있지만, 이 경우 컴파일 시점의 타입 안정성을 잃어버림은 물론 박싱이 일어나 성능이 감소할 수 있다.

 

class Node
{
    protected Node m_next;
    public Node(Node next) { m_next = next; }
}

class TypedNode<T> : Node
{
    public T m_data;
    public TypedNode(T data, Node next = null) : base(next)
    { m_data = data; }
}

public static class Program
{
    public static void Main()
    {
        Node head = new TypedNode<char>('.');
        head = new TypedNode<DateTime>(DateTime.Now, head);
        head = new TypedNode<string>("12345");
    }
}

그러나 위처럼 상속을 이용하면 서로 다른 데이터 타입들로 이뤄진 연결 리스트를 작성할 수 있다.

 

제너릭 타입의 독자성

 

아마 일부 개발자들은 <> 기호가 흩어져 나타나는 것을 읽기 힘들어 할 수 있다. 그래서 가끔 다음과 같은 클래스를 선언하여 사용하곤 한다.

 

class DateTimeList : List<DateTime> { }

위와 같은 코드를 사용하면

 

List<DateTime> dt1 = new List<DateTime>();
DateTimeList dt2 = new DateTimeList();

위 코드의 아래 문장처럼 간단하게 쓸 수 있는데, 이런 코드는 작성하지 말아야 한다. 이 경우 타입에 대한 독자성을 잃어버리고 아래와 같이 동일성 비교도 할 수 없다.

 

bool isSameType = (typeof(List<DateTime>) == typeof(DateTimeList); // false

 

위처럼 동일성 비교에 영향을 주지 않고 using 키워드를 이용하여 간단하게 제너릭을 쓸 수 있다.

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

bool isSameType = (typeof(List<DateTime>) == typeof(DateTimeList); // true

 

 

 

코드 폭증


 

제네릭 타입 매개변수를 사용하는 메서드를 JIT 컴파일 하면, CLR은 메서드의 IL코드를 가져와 지정된 타입 인자로 대체한 후, 해당 타입을 사용하는 네이티브 코드를 생성한다. 즉, 사용하는 타입 인자마다 네이티브 코드를 따로 생성한다는 말이다. 이런 식으로 CLR이 모든 메서드/타입의 조합별로 네이티브 코드를 생성한다면 엄청나게 코드가 많이 생기게 된다. 이걸 코드 폭증(code explosion)이라고 한다.

 

이런 현상을 그대로 둘 경우 응용프로그램의 작업 집합의 크기가 점점 늘어나게 되어 성능에 부정적 영향을 끼치게 된다.

 

다행히도 CLR은 코드 폭증을 예방하기 위한 몇 가지 최적화 기술이 포함되어 있다. 

 

우선 제네릭 메서드가 특정 타입 인자를 이용하여 호출한 적이 있고, 이후에 다시 호출한다면 CLR은 이러한 유형의 메서드/타입 조합을 한 번만 컴파일 한다. 이러면 서로 다른 어셈블리에서 제네릭 타입을 써도 한 번만 컴파일한다.

 

이 외에도 CLR은 타입 인자로 지정되는 타입이 참조 타입이라면 모두 동등한 타입으로 분류하여 코드를 공유한다. 그러나 값 타입은 그렇지 않다.

 

'언어 > C#' 카테고리의 다른 글

C# 제너릭 - 3  (0) 2021.01.20
C# - 제네릭 2  (0) 2021.01.18
C# 이벤트  (1) 2021.01.02
C# 속성 (Property) - 2  (0) 2020.12.27
C# 속성 (Property) - 1. 속성, 매개변수 없는 속성  (0) 2020.12.20