Quick Reference
제네릭은 알고리즘·컨테이너의 타입을 type parameter로 열어 두되, 호출 시 구체 타입을 정하는 문법입니다. where는 단순 필터가 아니라 generic body에서 사용할 수 있는 기능을 compiler에 약속하는 계약입니다.
public sealed class Box<T>(T value)
{
public T Value { get; } = value;
}
static T Max<T>(T left, T right) where T : IComparable<T>
=> left.CompareTo(right) >= 0 ? left : right;
int largest = Max(3, 5); // T는 호출 인자에서 추론- 컨테이너 또는 instance가 한 타입을 계속 보관하면 generic class, 한 알고리즘만 일반화하면 generic method를 씁니다.
List<int>처럼 concreteT를 보관하면 object cast와 일반적인 collection boxing을 피할 수 있습니다.T에 멤버·생성·형식 제약이 필요하면 정확한where를 선언합니다.out/invariance는 interface와 delegate의 참조 타입 변환에만 적용됩니다.
제약 문법
| 제약 | T에 보장하는 것 | 위치·주의 |
|---|---|---|
class / class? | non-null / nullable reference type | nullable context에서 의미가 갈림 |
notnull | non-null reference 또는 non-null value type | 위반은 일반적으로 compiler warning |
struct | non-nullable value type | new()을 이미 포함하므로 함께 못 씀 |
unmanaged | 참조를 포함하지 않는 non-nullable value type | struct를 포함하며 memory-oriented API용 |
| base class / interface | 해당 type의 member·계약 | Base?, IFoo? nullable form도 가능 |
new() | public parameterless constructor | 마지막 일반 제약이어야 함 |
default | unconstrained override/explicit implementation 표시 | 해당 두 경우에만 사용 |
allows ref struct | ref struct도 type argument로 허용 | C# 13+, 모든 제약 뒤 마지막 |
첫 다섯 primary constraint(class, class?, notnull, struct, unmanaged) 중 하나만 쓸 수 있고 먼저 와야 합니다. base class, interface, new(), allows ref struct 순서를 지킵니다.
static void CreateAndDispose<T>()
where T : class, IDisposable, new()
{
using T value = new();
}
static T First<T>(ReadOnlySpan<T> values) where T : unmanaged
=> values.IsEmpty ? throw new ArgumentException("At least one value is required.") : values[0];notnull은 null 가능 type argument를 막는 강한 runtime 검사나 예외가 아닙니다. nullable context에서 compiler 경고를 만드는 annotation입니다. T?의 정확한 해석과 default 제약은 nullable·override 같은 고급 경우에만 필요하므로, 일반 API에는 더 좁고 읽기 쉬운 제약을 우선합니다.
sizeof(T)처럼 generic type의 raw size를 다루는 코드는 unmanaged 외에도 unsafe context가 필요할 수 있습니다. memory layout이 아닌 값·계약만 다루는 API에 unmanaged를 억지로 붙이지 않습니다.
복사·boxing·variance
제네릭은 compile-time type safety를 제공하며, T가 value type일 때도 object container처럼 무조건 boxing하지 않습니다. 그러나 generic body에서 T를 object나 interface로 변환하거나 해당 overload를 호출하면 boxing conversion은 여전히 생길 수 있습니다. runtime의 code sharing, JIT specialization, AOT 결과는 구현과 target에 따라 달라지는 최적화 문제이므로 API 계약으로 설명하지 않습니다.
var scores = new List<int> { 90, 100 }; // int를 object로 보관하지 않음
object boxed = scores[0]; // 여기서 int boxing
int restored = (int)boxed;variance는 생산자·소비자 역할을 타입 변환에 반영합니다. IEnumerable<out T>는 IEnumerable<string>을 IEnumerable<object>로 사용할 수 있는 공변이고, IComparer<in T>는 더 넓은 입력 타입의 comparer를 좁은 타입에 쓸 수 있는 반공변입니다. value type에는 이 reference conversion이 적용되지 않습니다.
IEnumerable<string> names = ["Mina"];
IEnumerable<object> objects = names;선택과 실패 경계
generic parameter가 무엇을 할 수 있는지 이름과 constraint로 드러나야 합니다. 단지 중복을 줄이려고 Repository<T, TKey, TSpec>처럼 parameter를 늘리면 호출부와 오류 메시지가 더 어려워질 수 있습니다.
where T : class 안의 ==는 concrete type의 overloaded equality를 보장하지 않습니다. 값 equality가 필요하면 IEquatable<T> 같은 계약을 요구하거나 EqualityComparer<T>.Default의 정책을 명시합니다. unconstrained T에는 임의의 operator나 member가 있다고 가정할 수 없습니다.
자주 틀리는 부분
new() 제약은 public parameterless constructor만 보장합니다. 생성 직후 유효한 도메인 상태, 파일·네트워크 연결, dependency 주입까지 보장하지 않으므로 그런 객체에는 factory나 delegate 입력을 사용하세요.
allows ref struct는 C# 13 compiler가 필요한 고급 anti-constraint입니다. target compiler가 낮거나 generic body가 ref safety 규칙을 지킬 수 없다면 사용할 수 없습니다.
참고 링크
2 sources