ITEM 18 :: EFFECTIVE C#
안녕하세요, 18번째 시간입니다. 예전부터 조금 궁금하던 개념이 드디어 나왔군요!
이번 챕터는 제네릭과 템플릿에 관한 내용입니다. 정확한 챕터이름은 3. 제네릭 활용입니다만, 제네릭이라는 것 자체가 C++에서 템플릿만을 써오던 저에게는 생소한 개념이었기 때문에 조금 관심이 있었습니다.(자바에서 제네릭이라는 개념이 자주 사용되더라구요) 각설하고, 이번 내용은 짧지만 중요한, 제네릭과 템플릿의 간단한 차이점까지 해서 설명을 해볼까 합니다. 자, 들어가시죠!
먼저, 제네릭과 템플릿이 무엇인지?
사실, 단어는 다르지만, 둘은 굉장히 비슷한 개념이다. 형식 매개 변수라고 부르기도 하는 이 개념들은, 함수나 인터페이스에서도 사용이 가능하며, 사용자가 전달하는 ‘형식’을 매개 변수로 사용하여 형식 내부에서 형식 매개 변수로 정의된(주로 단일 형식 매개 변수에서는 T를 사용한다)형식을 교체하는 것이다. 쉽게 말하자면 찰흙의 틀이랑 비슷하다고 볼 수 있다. 별 모양 틀에 찰흙을 찍으면 별 모양이되고, 사각형 모양 틀에 찍으면 사각형이 되는 것과 아주 동일한 원리이다!
다음으로, 이 둘의 차이는 MSDN에서 이렇게 말하고 있다.
-
C# 제네릭은 C++ 템플릿과 동일한 수준의 유연성을 제공하지 않습니다. 예를 들어 C# 제네릭 클래스에서 산술 연산자는 호출할 수 없지만 사용자 정의 연산자는 호출할 수 있습니다.
-
C#에서는 temmplate c
{} 같은 비형식 템플릿 매개 변수를 허용하지 않습니다. -
C#은 명시적 특수화 즉, 특정 형식에 대한 템플릿의 사용자 지정 구현을 지원하지 않습니다.
-
C#은 부분 특수화 즉, 형식 인수의 하위 집합에 대한 사용자 지정 구현을 지원하지 않습니다.
-
C#에서는 형식 매개 변수를 제네릭 형식에 대한 기본 클래스로 사용할 수 없습니다.
-
C#에서는 형식 매개 변수가 기본 형식을 사용할 수 없습니다.
-
C#에서 제네릭 형식 매개 변수 자체는 제네릭이 될 수 없지만, 생성된 형식은 제네릭으로 사용할 수 있습니다. C++에서는 템플릿 매개 변수를 허용합니다.
-
C++에서는 템플릿의 일부 형식 매개 변수에 적합하지 않아 형식 매개 변수로 사용되는 특정 형식을 확인하는 코드를 허용합니다. C#에서는 제약 조건을 충족하는 모든 형식에서 작동하는 방식으로 작성할 코드가 클래스에 필요합니다. 예를 들어 C++에서는 산술 연산자 + 및 -를 사용하는 함수를 형식 매개 변수의 개체에서 작성하여 이러한 연산자를 지원하지 않는 형식으로 템플릿을 인스턴스화할 때 오류를 생성할 수 있습니다. C#에서는 이를 허용하지 않습니다. 허용되는 유일한 언어 구문은 제약 조건에서 추론할 수 있는 구문입니다.
라고 되어있다.
간단하게 정리해보자면,
-
제네릭은 템플릿에 비해 유연성이 낮다, 가 주된 내용이다.
-
그렇게 된 이유는 아마도 C#의, 내부적으로 작동하는 무언가에 대해 맞추기 위해서일 것이리라 생각한다. C++에 비해 C#은 내부적으로, 또, 자동적으로 이루어지는 것들이 많기 때문이다.
그렇다. 이 정도가 템플릿과 제네릭의 차이이며, 사실, 이거만 보고 템플릿이 좋다고 하긴 힘든 것이, 제네릭은 C#에서 사용하기 좀 더 쉽고 효율적일 수 있게 설계된 기능이므로, C++의 템플릿과 뭐가 좋고 안좋다 비교하는 것은 좋지 않다. 그냥 이 정도의 차이점이 있다는 것만 확실하게 알고 있으면 좋을 것 같다.
그래서, 다음이야기는
이제야 Effective C#으로 돌아갈 수 있게 되었다.
이번 챕터는 ‘반드시 필요한 제약조건만 설정하라’ 이다.
그 내용에 대해 간단히 살펴보자
제약조건?
먼저 제약조건에는 where 절이라고 하는, SQL문에서 많이봤던 예약어가 등장한다. 이 where 절은 제네릭 형식, 메서드, 대리자(델리게이트) 또는 로컬 함수의 형식 매개 변수(제네릭)에 대한 인수로 사용되는 형식에 대한 제약 조건을 지정할 수 있다.
이에 대한 예는 아래와 같다.
public class UsingEnum<T> where T : System.Enum { } // Enum을 사용하여 구현하도록 where 절로 제약조건을 설정한 경우
public class UsingDelegate<T> where T : System.Delegate { } // Delegate를 사용하여 구현하도록 where 절로 제약조건을 설정한 경우
public class Multicaster<T> where T : System.MulticastDelegate { } // Multicaster를 사용하여 구현하도록 where 절로 제약조건을 설정한 경우
//여기서 볼 점은 where 절은 형식이 class 혹은 struct임을 알 수 있다.
//다음으로 class 및 struct의 제약조건을 주는 경우의 예는 아래와 같다.
class MyClass<T, U>
where T : class // T에는 클래스 형식만
where U : struct // U에는 구조체 형식만
{ }
//또한 Unmanaged 제약 조건을 추가로 사용하게 되면 C#에서 하위 수준의 interop 코드를 더 쉽게 작성할 수 있다.
class UnManagedWrapper<T>
where T : unmanaged
{ }
//그리고 where 절에 new() 생성자 제약 조건이 포함되는 경우도 있는데, 이 조건을 사용하면 new 연산자를 사용하여 형식 매개 변수의 인스턴스를 만들 수 있게 되며,
//new() 제약 조건을 사용하게 되면 컴파일러는 제공된 인수에 엑세스 가능하고 매개 변수가 없는 생성자가 있어야 한다는 것을 알게 된다.
public class MyGenericClass<T> where T : IComparable<T>, new() //new() 제약 조건은 where절 맨 끝에 나타냄!
{
// The following line is not possible without new() constraint:
T item = new T();
}
//여기서 형식 매개 변수가 여러 개일 경우, 각 형식 매개 변수 하나 당, 하나의 where 절을 사용한다.
public interface IMyInterface { }
namespace CodeExample
{
class Dictionary<TKey, TVal>
where TKey : IComparable<TKey>
where TVal : IMyInterface
{
public void Add(TKey key, TVal val) { }
}
}
//마지막으로 제네릭 메서드의 형식 매개 변수에 연결할 수도 있고, 대리자에 대해서도 연결할 수 있다.
public void MyMethod<T>(T t) where T : IMyInterface { }
delegate T MyDelegate<T>() where T : new();
이 정도가 제약조건에 대한 MSDN의 설명이다. 쉽고 간단하게 나와있지만, 어떤 경우에 사용해야할 지는 아직 감이 안온다. 이 쯤에서 Effective C#으로 돌아가보자.
반드시 필요한 제약조건만 설정하라
결국, 제약조건은 제네릭을 활용함에 있어서 좀 더 효율좋게 운용하는 방법이나, 과도한 제약조건이나, 혹은 사용 빈도가 떨어지는 제약조건은 지양하는 편이 좋다. 가독성을 떨어뜨릴 뿐만 아니라, 기능의 안전성도 보장하지 않기 때문이다.
결론은 제너릭 타입을 활용할때 타입 조건을 넣고싶다면 제약조건 예약어 where를 사용하는 것이 좋다.
그러나 기본값을 할당하는데 있어서 default()와 new()는 분명한 차이가 있으므로, 제약조건에 new()를 넣을땐 이용자가 구현에 어려움을 겪을수도 있다는 것을 생각해야한다.