
일반적인 클래스는 정의할 때 타입이 고정된다. 예를 들어, 클래스 MyClass를 만들면 int MyClass, string MyClass 같은 변형을 만들 수 없다. 하지만 제네릭을 사용하면 "또 다른 클래스를 만들 필요 없이" 다양한 타입을 지원할 수 있다.
다음 컬렉션 예제를 보자.
List<int> intList = new List<int>();
List<string> strList = new List<string>();
intList.Add(10);
strList.Add("Hello");
제네릭 클래스를 사용하면, 저장되는 데이터의 타입에 관계없이 동일한 방식으로 추가/삭제 등의 작업을 수행할 수 있다.
ArrayList list = new ArrayList();
list.Add(10); // int -> object (박싱)
list.Add("Hello"); // string -> object (박싱)
비제네릭 클래스의 경우 박싱/언박싱이 발생할 수 있어 성능에 영향을 줄 수 있다.
이처럼 제네릭은 특정 데이터 형식에 의존하지 않는 기능을 캡슐화하여, 코드의 재사용성과 확장성을 높이는 역할을 한다. 또한 컴파일 시점에 타입이 결정되므로 타입 안정성을 유지하면서도 유연한 설계를 할 수 있다.
System.Collections.Generic 네임스페이스에 있는 모든 자료구조 관련 클래스들은 제네릭 타입이다.
ex) List, Dictionary, LinkedList, Stack, Queue 등
public class MyClass<T>
{
public T x;
public T method(T p) { ... }
}
제네릭 클래스를 선언하는 방법은 클래스 이름 뒤에 <T>를 붙이면 된다. 이렇게 하면 클래스 내에서 사용되는 모든 데이터 타입을 T 하나로 처리할 수 있다. 클래스 이외에도 인터페이스나 메서드에도 적용될 수 있다.
C# 제네릭과 내부구조는 다르지만 비슷한 개념으로 C++의 템플릿이 있다.
제네릭 타입을 선언할 때, 타입 파라미터가 Value Type인지 Reference Type인지, 또는 어떤 특정 Base 클래스로부터 파생된 타입인지 제약을 걸 수 있다. 이는 where T : 제약조건 이런 식으로 where 뒤에 제약 조건을 붙이면 된다.
// Value 타입
class MyClass<T> where T : struct
// Reference 타입
class MyClass<T> where T : class
// 디폴트 생성자 필요
class MyClass<T> where T : new()
// MyBase의 파생클래스
class MyClass<T> where T : MyBase
// IComparable 인터페이스 필요
class MyClass<T> where T : IComparable
// 다중 제약
class EmployeeList<T> where T : Employee,
IEmployee, IComparable<T>, new()
// 복수 타입 파라미터 제약
class MyClass<T, U>
where T : class
where U : struct
제네릭 클래스가 다른 클래스를 상속 받을 때, 상속받는 클래스의 타입 매개변수가 확정되었는지 여부에 따라 개방형 또는 폐쇄형 제네릭 타입이라고 부른다.
개방형 제네릭 타입 (Open Constructed Type)
class NodeOpen<T> : BaseNodeGeneric<T> { } // BaseNodeGeneric<T>는 개방형
폐쇄형 제네릭 타입 (Closed Constructed Type)
class BaseNodeGeneric<T> { }
class NodeClosed<T> : BaseNodeGeneric<int> { }
// BaseNodeGeneric<int>는 폐쇄형.
// NodeClosed도 생성 과정에서 특정 타입으로 치환될 예정이기 때문에 완전 폐쇄형.
제네릭 클래스(C# 프로그래밍 가이드)
C# 제네릭(C# Generics)
C# 실력다지기(2. 제네릭 클래스 이해하기)