숏컷 코드
// 제네릭 클래스
public class Box<T>
{
public T Value { get; set; }
public Box(T value) => Value = value;
}
Box<int> numBox = new(42);
Box<string> strBox = new("hello");
// 제네릭 메서드
T Identity<T>(T value) => value;
int n = Identity(10); // T = int 추론
string s = Identity("hi"); // T = string 추론문법
어떤 제네릭 형태가 먼저 나오나
| 형태 | 먼저 떠올릴 상황 |
|---|---|
List<T> 같은 제네릭 클래스 | 타입별 컨테이너를 재사용할 때 |
T Method<T>(...) 같은 제네릭 메서드 | 알고리즘 하나를 여러 타입에 적용할 때 |
where T : ... | 타입에 필요한 기능을 계약으로 걸 때 |
out T / in T | 인터페이스 공변/반공변을 설명할 때 |
제네릭 vs object — boxing 비용
제네릭이 없던 시절에는 object 타입을 사용해 범용 컨테이너를 만들었습니다. 이 방식은 값 타입을 object에 담을 때 boxing(힙 할당)이 발생하고, 꺼낼 때 캐스팅이 필요합니다.
// ❌ object 기반 — boxing/unboxing 발생
ArrayList list = new ArrayList();
list.Add(42); // int → object boxing (힙 할당)
int n = (int)list[0]; // object → int unboxing (캐스팅)
// ✅ 제네릭 — boxing 없음, 타입 안전
List<int> generic = new List<int>();
generic.Add(42); // boxing 없음
int m = generic[0]; // 캐스팅 없음제네릭은 컴파일 타임에 타입을 고정합니다. JIT가 각 타입별로 특화된 코드를 생성하므로 값 타입에서는 boxing이 없고 성능이 향상됩니다.
where 제약 — 타입에 필요한 조건 명시
T가 아무 타입이나 될 수 있으면 T의 멤버를 쓸 수 없습니다. where 제약으로 타입에 필요한 조건을 명시하면 해당 기능을 사용할 수 있습니다.
// 비교 가능한 타입만 허용
T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
Max(3, 5); // ✅ int는 IComparable<int> 구현
Max("a", "b"); // ✅ string도 구현
// 자주 쓰는 제약 종류
where T : class // 참조 타입만
where T : struct // 값 타입만
where T : new() // 매개변수 없는 생성자 보유
where T : IDisposable // 특정 인터페이스 구현
where T : BaseClass // 특정 클래스 또는 파생 클래스
where T : notnull // null 불허 타입
// 복수 제약 조합
void Process<T>(T item) where T : class, IDisposable, new() { }// ❌ 제약이 없으면 T의 멤버를 직접 쓸 수 없음
T Max<T>(T a, T b)
{
// return a.CompareTo(b) >= 0 ? a : b; // 컴파일 오류
return a;
}
// ✅ 필요한 능력을 where로 명시
T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;제네릭 클래스 vs 제네릭 메서드
제네릭 클래스는 클래스 전체가 같은 타입 매개변수를 공유합니다. 제네릭 메서드는 메서드 하나만 일반화합니다.
// 제네릭 클래스 — 인스턴스 생성 시 타입 결정
public class Repository<T>
{
private List<T> _items = new();
public void Add(T item) => _items.Add(item);
public T? FindById(Func<T, bool> predicate) => _items.FirstOrDefault(predicate);
}
var repo = new Repository<User>();
repo.Add(new User("Mina"));
// 제네릭 메서드 — 호출 시 타입 결정 (또는 추론)
public static void Swap<T>(ref T a, ref T b) => (a, b) = (b, a);
int x = 1, y = 2;
Swap(ref x, ref y); // T = int 추론공변성과 반공변성 (간략)
인터페이스에서 out T(공변)는 T를 반환만 하고, in T(반공변)는 T를 입력만 받습니다.
// IEnumerable<out T> — 공변: IEnumerable<Dog>를 IEnumerable<Animal>로 사용 가능
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // ✅ string은 object
// IComparer<in T> — 반공변: IComparer<Animal>을 IComparer<Dog>로 사용 가능선택 기준
| 상황 | 더 적합한 선택 |
|---|---|
| 같은 알고리즘을 여러 타입에 적용 | 제네릭 |
| 특정 타입에만 쓰는 일회성 로직 | 일반 구체 타입 |
| 타입의 특정 기능이 필요함 | where 제약 있는 제네릭 |
object로 값 타입을 다룸 | 제네릭으로 boxing 제거 |
체크포인트
| 문법 | 설명 |
|---|---|
class Box<T> | 제네릭 클래스 |
T Method<T>(T x) | 제네릭 메서드 |
where T : IFoo | 타입 제약 |
where T : new() | 기본 생성자 요구 |
out T / in T | 인터페이스 공변/반공변 |
주의할 점
제네릭이 너무 깊어지면 (Repository<T, TKey, TSpec>) 코드가 오히려 읽기 어려워집니다. "단순한 중복 제거인지, 진짜 타입 추상화가 필요한지" 를 먼저 판단하는 편이 좋습니다.
제네릭 타입 매개변수 이름은 관례상 단일 대문자 T, TKey, TValue처럼 씁니다. 여러 개가 있을 때는 역할이 드러나는 이름(TKey, TValue)이 T1, T2보다 읽기 좋습니다.
참고 링크
2 sources