C#에는 C++의 template와 같이 자료형을 실제 사용할 때 지정할 수 있는 기능이 있는데 Generic(제네릭)이라고 한다. Generic은 특정 클래스를 정의할 때 클래스 이름 옆에 <T>를 붙여 '해당 클래스에서 T 자료형은 실제 사용할 때 지정함'을 나타낸다.
아래 코드는 Generic을 사용해 Point 클래스를 정의하는 예제 코드이다.
class Point<T>
{
private T x;
private T y;
public Point(T xPos = default(T), T yPos = default(T))
{
x = xPos;
y = yPos;
}
}
class Program
{
public static void Main(string[] args)
{
Point<int> pt = new Point<int>(1, 1);
Point<double> pt2 = new Point<double>(1.1, 1.1);
}
}
생성자나 함수의 매개변수에 사용되는 default parameter에 Generic을 사용할 때, 정의할 때는 자료형을 알 수 없기 때문에 default 키워드를 사용한다. default를 사용하면 T의 기본값을 default parameter로 사용하게 된다.
클래스의 정의 말고도 함수의 반환형식에도 Generic을 사용할 수 있다. 클래스에서 사용하는 방법과 비슷하게 함수를 정의할 때 함수 이름 옆에 <T>를 붙여 'Generic Method'를 사용할 수 있다.
함수를 사용할 때 사용할 자료형을 지정하는 방식으로 사용할 수 있는데, 컴파일러가 T를 유추할 수 있는 경우 생략을 할 수 있는 경우도 있다.
class Program
{
public static T Foo1<T>(T a)
{
return a;
}
public static void Main(string[] args)
{
Foo1<int>(3); // 호출할 때 자료형을 지정
Foo1(3); // 이것도 가능
}
}
T의 타입을 찍어보면 어떻게 나올까? 매개변수로 전달받은 변수의 GetType을 호출하면 실제 매개변수가 가리키는 객체 타입이 반환되고, typeof(T)를 하면 T 자체의 타입이 반환된다.
class Program
{
public static T Foo2<T>(T a, T b)
{
// a가 가리키는 실제 객체 타입
Type type1 = a.GetType();
// T 자체의 타입
Type type2 = typeof(T);
return a;
}
public static void Main(string[] args)
{
Foo2<object>(1, "AAA");
}
}
모든 타입에 대한 Max라는 함수를 구현한다고 할 때 가장 먼저 생각나는 방법은 자료형을 object로 하는 것이다.
public static object Max(object a, object b)
{
IComparable? c1 = a as IComparable;
IComparable? c2 = b as IComparable;
return c1.CompareTo(c2) < 0 ? b : a;
}
하지만 위와 같이 구현한다 할때, 다음과 같은 문제점이 존재한다.
두번째로 캐스팅 과정을 줄이기 위해 매개변수의 자료형을 IComparable로 설정하는 방법이 존재한다.
public static object Max(IComparable a, IComparable b)
{
return a.CompareTo(b) < 0 ? b : a;
}
C#에서 모든 인터페이스를 참조 타입이기 때문에 두번째 방법 역시 Boxing/Unboxing이 발생하게 된다. 그리고 반환값이 아직 object이기 때문에 사용후 캐스팅이 필요한 상황이다.
세번째로 Generic을 사용하는 방법이 있다.
public static T Max<T>(T a, T b)
{
// T가 CompareTo를 가지고 있다고 확신 불가
return a.CompareTo(b) < 0 ? b : a; // Error!
}
Generic을 사용하면 Boxing/Unboxing이 발생하지 않겠지만 위의 코드에서는 에러가 발생하게 된다. T가 CompareTo 함수를 정의하고 있는지 어떻게 확신할 수 없기 때문에 컴파일 에러가 발생하게 된다.
모든 타입은 object를 상속받기 때문에 Equals와 같은 함수는 사용이 가능하다.
결국 문제는 T가 IComparable을 구현하고 있는지 모르기 때문에 발생하고 있다. 이럴때 사용하는 것이 Generic Constraint로 특정 조건을 만족하는 T에 대해서만 해당 함수를 호출할 수 있도록 한다.
public static T Max<T>(T a, T b) where T : IComparable<T>
{
// T가 CompareTo를 가지고 있다고 확신 가능
return a.CompareTo(b) < 0 ? b : a; // Ok!
}
주로 where에 사용하는 Constraint에는 아래와 같다.