| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- ui
- 최적화
- 직장인공부
- RSA
- unity
- 게임개발
- Job 시스템
- 프레임워크
- Framework
- 패스트캠퍼스후기
- Custom Package
- 직장인자기계발
- C#
- base64
- 환급챌린지
- sha
- TextMeshPro
- AES
- 2D Camera
- job
- 샘플
- DotsTween
- Unity Editor
- Tween
- adfit
- 패스트캠퍼스
- 암호화
- 오공완
- Dots
- 가이드
- Today
- Total
EveryDay.DevUp
[PART7.제네릭(1/3)] 제네릭 — <T>가 해결하는 문제 본문
제네릭 — <T>가 해결하는 문제
한 줄의 <int>가 boxing 3번, 캐스팅 3번, 런타임 예외 1번을 동시에 없앤다. 제네릭이 뭔지 모르면, List<T>를 왜 쓰는지도 모르는 것이다.
목차
문제 제기 — ArrayList가 남긴 세 가지 부채
Unity 프로젝트에서 적(enemy) 체력을 관리하는 코드를 상상해 보자.
using System.Collections;
ArrayList hpList = new ArrayList();
hpList.Add(100);
hpList.Add(85);
hpList.Add("풀피"); // 실수로 문자열을 넣었다 — 컴파일 에러 없음
int total = 0;
for (int i = 0; i < hpList.Count; i++)
{
total += (int)hpList[i]; // "풀피"에서 InvalidCastException 💥
}
이 코드에는 세 가지 문제가 숨어 있다.
- 타입 안전성 부재 —
string이 들어가도 컴파일러가 잡아주지 않는다. 런타임에 터져야 비로소 알게 된다. - 박싱/언박싱 —
int(값 타입)를object(참조 타입)로 변환하는 과정에서 매번 힙 메모리를 할당한다. Unity의 GC(Garbage Collector, 메모리를 자동으로 회수하는 런타임 구성요소)가 이 쓰레기를 수거할 때 GC 스파이크(갑작스러운 프레임 드랍)가 발생한다. - 코드 중복 — 타입 안전성을 원하면
IntList,StringList,PlayerList를 각각 만들어야 한다. 로직은 같은데 타입만 다른 클래스가 늘어난다.
C# 2.0(2005)에서 등장한 제네릭(Generics)은 이 세 가지 문제를 한 번에 해결한다. 이 글에서는 제네릭이 정확히 무엇이고, 내부에서 어떻게 동작하며, Unity에서 어떻게 활용하는지를 IL(Intermediate Language, C# 코드가 컴���일되는 중간 언어) 수준에서 증명한다.
개념 정의 — 타입을 나중에 결정하는 틀
뽑기 기계 비유
제네릭을 이해하는 가장 쉬운 방법은 뽑기 기계를 떠올리는 것이다.
기계 자체(코드 로직)는 하나다. 하지만 어떤 캡슐(타입)을 넣느냐에 따라 나오는 결과물이 달라진다. List<int>는 "정수 전용 뽑기 기계"이고, List<string>은 "문자열 전용 뽑기 기계"다. 기계의 구조는 같지만, 한번 타입을 지정하면 다른 타입의 캡슐은 물리적으로 들어가지 않는다 — 이것이 컴파일 타임 타입 검사다.

Before/After — ArrayList vs List<int>
Before — ArrayList (제네릭 이전)
ArrayList는 모든 요소를 object로 저장한다. 값 타입을 넣으면 박싱이 발생하고, 꺼낼 때는 명시적 캐스팅(언박싱)이 필요하다.
using System.Collections;
public class BeforeGeneric
{
public static int SumArrayList()
{
ArrayList list = new ArrayList();
list.Add(10);
list.Add(20);
list.Add(30);
int sum = 0;
for (int i = 0; i < list.Count; i++)
{
sum += (int)list[i];
}
return sum;
}
}
.method public hidebysig static
int32 SumArrayList () cil managed
{
.locals init (
[0] class [System.Runtime]System.Collections.ArrayList,
[1] int32, // sum
[2] int32, // i (루프 카운터)
[3] bool,
[4] int32 // 반환값
)
IL_0001: newobj instance void [System.Runtime]System.Collections.ArrayList::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.s 10
IL_000a: box [System.Runtime]System.Int32 // 💥 boxing — 10을 힙에 할당
IL_000f: callvirt instance int32 ArrayList::Add(object)
IL_0014: pop
IL_0015: ldloc.0
IL_0016: ldc.i4.s 20
IL_0018: box [System.Runtime]System.Int32 // 💥 boxing — 20을 힙에 할당
IL_001d: callvirt instance int32 ArrayList::Add(object)
IL_0022: pop
IL_0023: ldloc.0
IL_0024: ldc.i4.s 30
IL_0026: box [System.Runtime]System.Int32 // 💥 boxing — 30을 힙에 할당
IL_002b: callvirt instance int32 ArrayList::Add(object)
// ... 루프 시작 ...
IL_003b: callvirt instance object ArrayList::get_Item(int32)
IL_0040: unbox.any [System.Runtime]System.Int32 // 💥 unboxing — 타입 확인 후 값 복사
IL_0045: add
// ... 루프 끝 ...
}
Add 호출마다 box 명령어가 3번 등장한다. 값 하나를 넣을 때마다 힙에 약 24바이트(Object Header 8 + Method Table Pointer 8 + 값 4 + 패딩 4)가 할당된다. 루프에서는 unbox.any로 매번 타입 확인 + 값 복사가 일어난다.
After — List<int> (제네릭)
using System.Collections.Generic;
public class AfterGeneric
{
public static int SumList()
{
List<int> list = new List<int>();
list.Add(10);
list.Add(20);
list.Add(30);
int sum = 0;
for (int i = 0; i < list.Count; i++)
{
sum += list[i];
}
return sum;
}
}
.method public hidebysig static
int32 SumList () cil managed
{
.locals init (
[0] class [System.Collections]System.Collections.Generic.List`1<int32>,
[1] int32, // sum
[2] int32, // i (루프 카운터)
[3] bool,
[4] int32 // 반환값
)
IL_0001: newobj instance void class List`1<int32>::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.s 10
IL_000a: callvirt instance void class List`1<int32>::Add(!0) // box 없음!
IL_0010: ldloc.0
IL_0011: ldc.i4.s 20
IL_0013: callvirt instance void class List`1<int32>::Add(!0) // box 없음!
IL_0019: ldloc.0
IL_001a: ldc.i4.s 30
IL_001c: callvirt instance void class List`1<int32>::Add(!0) // box 없음!
// ... 루프 시작 ...
IL_002c: callvirt instance !0 class List`1<int32>::get_Item(int32) // unbox 없음!
IL_0031: add
// ... 루프 끝 ...
}
box와 unbox.any가 완전히 사라졌다. List1<int32>는 내부적으로 int[] 배열을 사용하므로, 값 타입이 힙에 별도 객체로 박싱되지 않고 배열에 직접 저장된다. Add(!0)에서 !0은 "첫 번째 타입 매개변수(여기서는 int32`)"를 의미한다.
쉽게 말하면, 제네릭은 "이 리스트에는 정수만 들어간다"고 컴파일러에게 약속하는 것이다. 약속 덕분에 타입 검사도 불필요하고, 상자 포장(boxing)도 불필요하다.
정확히 표현하면, 제네릭은 타입 매개변수(Type Parameter) T를 사용하여 클래스·메서드·인터페이스를 정의하고, 사용 시점에 구체적인 타입 인수(Type Argument)를 지정하여 타입 안전성과 성능을 동시에 확보하는 C#의 매개변수화된 다형성(Parametric Polymorphism) 메커니즘이다.
내부 동작 — JIT 컴파일러의 두 가지 전략
택배 분류 센터 비유
제네릭 코드가 실행될 때 JIT(Just-In-Time, 실행 시점에 IL을 기계어로 변환하는 컴파일러)는 택배 분류 센터처럼 동작한다.
- 값 타입 택배(int, float, struct): 크기와 모양이 제각각이라 전용 분류 라인을 타입마다 하나씩 만든다 → 특수화(Specialization)
- 참조 타입 택배(string, class): 크기가 모두 같다(포인터 8바이트). 하나의 라인으로 모두 처리한다 → 코드 공유(Code Sharing)

왜 이렇게 나누는가
값 타입은 int(4바이트), float(4바이트), Vector3(12바이트) 등 크기가 제각각이다. 메모리 레이아웃이 다르므로 배열 인덱싱, 메서드 호출 시 생성되는 기계어가 달라야 한다. JIT가 각 값 타입마다 별도의 네이티브 코드를 생성(특수화)하기 때문에, 제네릭 컬렉션은 네이티브 배열(int[])과 동일한 성능을 낸다.
참조 타입은 실제 객체가 아닌 참조(포인터)를 저장한다. 64비트 환경에서 모든 참조는 8바이트로 동일하다. List<string>이든 List<Player>든 내부 배열의 원소 크기가 같으므로, JIT는 하나의 네이티브 코드를 공유한다. 이 덕분에 참조 타입 제네릭은 코드 팽창(Code Bloat)을 ��지한다.
제네릭 메서드의 IL 구조
제네릭 메서드가 IL에서 어떻게 표현되는지 확인해 보자.
public class GenericMethod
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
.method public hidebysig static
void Swap<T> (
!!T& a, // !!T = 메서드의 타입 매개변수 T에 대한 참조
!!T& b
) cil managed
{
.locals init (
[0] !!T // temp — 타입이 T로 미확정
)
IL_0001: ldarg.0
IL_0002: ldobj !!T // a의 값을 T 타입으로 로드
IL_0007: stloc.0 // temp에 저장
IL_0008: ldarg.0
IL_0009: ldarg.1
IL_000a: ldobj !!T // b의 값을 T 타입으로 로드
IL_000f: stobj !!T // a에 저장
IL_0014: ldarg.1
IL_0015: ldloc.0
IL_0016: stobj !!T // b에 temp 저장
}
IL에서 !!T는 "메서드 수준의 타입 매개변수"를 의미한다. ldobj !!T와 stobj !!T는 T의 실제 크기에 맞춰 값을 로드하고 저장한다. JIT가 Swap<int>를 처리할 때는 4바이트씩, Swap<Vector3>를 처리할 때는 12바이트씩 복사하는 별도 코드를 생성한다.
box나 unbox 명���어가 전혀 없다. 제네릭 덕분에 값 타입을 다룰 때도 힙 할당 없이 스택에서 직접 교환이 이루어진다.
실전 적용 — 제네릭을 언제, 어떻게 쓰는가
제약 조건으로 T의 능력을 보장한다
where T : ...— 제네릭 제약 조건 (Generic Constraint) 타입 매개변수T가 만족해야 할 조건을 컴파일러에게 알려준다. 제약이 없으면T는object와 동일한 최소 기능만 사용할 수 있다.
예시:where T : struct→ T는 값 타입이어야 한다
제약 조건이 없으면 T는 어떤 타입이든 될 수 있으므로, 컴파일러는 T에 대해 object가 제공하는 ToString(), Equals(), GetHashCode() 정도만 사용할 수 있다고 가정한다. T에 특정 메서드나 속성이 있음을 보장하려면 제약 조건이 필요하다.
| 제약 | 의미 | 사용 예 |
|---|---|---|
where T : struct |
값 타입만 | 스택 할당 보장, Nullable 래핑 |
where T : class |
참조 타입만 | null 비교 허용 |
where T : new() |
매개변수 없는 생성자 필수 | 팩토리 패턴 (항상 맨 마지막에) |
where T : BaseClass |
특정 클래스 상속 | 다형성 활용 |
where T : IInterface |
인터페이스 구현 | 기능 보장 |
where T : notnull |
null 불가 (C# 8.0+) | null-free API |
where T : unmanaged |
비관리 타입 (C# 7.3+) | 포인터 연산, NativeArray |
IL에서 constrained. 접두사의 역할
제약 조건이 있든 없든, 제네릭 코드에서 T의 메서드를 호출하면 IL에 constrained. 접두사가 붙는다.
public class WithConstraint
{
public static string GetString<T>(T value) where T : struct
{
return value.ToString();
}
}
.method public hidebysig static
string GetString<valuetype .ctor ([System.Runtime]System.ValueType) T> (
!!T 'value'
) cil managed
{
IL_0001: ldarga.s 'value' // value의 주소를 로드
IL_0003: constrained. !!T // T가 값 타입이면 boxing 없이 직접 호출
IL_0009: callvirt instance string [System.Runtime]System.Object::ToString()
}
constrained. !!T + callvirt의 조합이 핵심이다. JIT가 이 조합을 만나면:
- T가 값 타입이고 해당 메서드를 직접 구현했으면 → boxing 없이 직접 호출 (
call) - T가 값 타입이지만 메서드를 오버라이드하지 않았으면 → boxing 후
callvirt - T가 참조 타입이면 → 그냥
callvirt
int는 ToString()을 직접 구현하고 있으므로, constrained. 덕분에 boxing이 발생하지 않는다.
Before/After — object 기반 Swap vs 제네릭 Swap
Before — object 기반 (boxing 발생)
public class NonGenericSwap
{
public static void Swap(ref object a, ref object b)
{
object temp = a;
a = b;
b = temp;
}
public static void Demo()
{
object x = 10, y = 20; // boxing!
Swap(ref x, ref y);
}
}
.method public hidebysig static
void Demo () cil managed
{
.locals init (
[0] object,
[1] object
)
IL_0001: ldc.i4.s 10
IL_0003: box [System.Runtime]System.Int32 // 💥 boxing — 힙 할당
IL_0008: stloc.0
IL_0009: ldc.i4.s 20
IL_000b: box [System.Runtime]System.Int32 // 💥 boxing — 힙 할당
IL_0010: stloc.1
IL_0011: ldloca.s 0
IL_0013: ldloca.s 1
IL_0015: call void NonGenericSwap::Swap(object&, object&)
}
정수 두 개를 교환하는 데 box 명령어가 2번 발생한다. Unity Update() 루프에서 매 프레임 호출하면 초당 120번의 불필요한 힙 할당이 생긴다.
After — 제네릭 Swap (boxing 없음)
public class GenericMethod
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static void Demo()
{
int x = 10, y = 20;
Swap(ref x, ref y); // T = int, boxing 없음
string s1 = "A", s2 = "B";
Swap(ref s1, ref s2); // T = string, 참조만 교환
}
}
.method public hidebysig static
void Demo () cil managed
{
.locals init (
[0] int32, // x — 스택에 직접 저장
[1] int32, // y — 스택에 직접 저장
[2] string,
[3] string
)
IL_0001: ldc.i4.s 10
IL_0003: stloc.0 // box 없이 바로 저장
IL_0004: ldc.i4.s 20
IL_0006: stloc.1
IL_0007: ldloca.s 0
IL_0009: ldloca.s 1
IL_000b: call void GenericMethod::Swap<int32>(!!0&, !!0&) // 전용 코드
IL_0011: ldstr "A"
IL_0016: stloc.2
IL_0017: ldstr "B"
IL_001c: stloc.3
IL_001d: ldloca.s 2
IL_001f: ldloca.s 3
IL_0021: call void GenericMethod::Swap<string>(!!0&, !!0&) // 공유 코드
}
box 명령어가 완전히 사라졌다. Swap<int32>는 값 타입 특수화 코드를, Swap<string>은 참조 타입 공�� 코드를 각각 사용한다. 하나의 Swap<T> 메서드로 모든 타입을 처리하면서도, 각 타입에 최적화된 경로를 탄다.
Unity 실전 — 제네릭 오브젝트 풀
Unity에서 가장 실용적인 제네릭 활용 중 하나는 오브젝트 풀링(Object Pooling)이다. 총알, 이펙트, 적 등 반복 생성되는 오브젝트를 미리 만들어두고 재활용하여 Instantiate/Destroy의 GC 비용을 줄인다.
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool<T> where T : Component
{
private readonly Queue<T> _pool = new Queue<T>();
private readonly T _prefab;
public ObjectPool(T prefab, int initialSize)
{
_prefab = prefab;
for (int i = 0; i < initialSize; i++)
{
T obj = Object.Instantiate(_prefab);
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
}
public T Get()
{
T obj = _pool.Count > 0
? _pool.Dequeue()
: Object.Instantiate(_prefab);
obj.gameObject.SetActive(true);
return obj; // 반환 타입이 T — 캐스팅 불필요
}
public void Release(T obj)
{
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
}
// 사용 예
// ObjectPool<Bullet> bulletPool = new ObjectPool<Bullet>(bulletPrefab, 20);
// Bullet b = bulletPool.Get(); // 타입 안전, 캐스팅 없음
where T : Component 제�� 조건 덕분에 T에서 gameObject, SetActive 등 Unity API를 직접 호출할 수 있다. 반환 타입도 T이므로 호출하는 쪽에서 캐스팅이 필요 없다.
함정과 주의사항 — 제네릭이 만능은 아니다
함정 1: 값 타입의 인터페이스 호출과 boxing
제네릭 없이 인터페이스를 통해 값 타입의 메서드를 호출하면 boxing이 발생한다.
// ❌ 인터페이스 변수에 값 타입 대입 — boxing 발생
IComparable comp = 42; // boxing!
int result = comp.CompareTo(10); // 이미 boxing된 상태에서 호출
// ✅ 제네릭 제약으로 boxing 방지
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
int bigger = Max(10, 20); // boxing 없음 — constrained. 접두사
// ✅ 제네릭 버전의 IL
.method public hidebysig static
!!T Max<(class [System.Runtime]System.IComparable`1<!!T>) T> (
!!T a,
!!T b
) cil managed
{
IL_0001: ldarga.s a
IL_0003: ldarg.1
IL_0004: constrained. !!T // boxing 없이 IComparable<T>.CompareTo 직접 호출
IL_000a: callvirt instance int32 class IComparable`1<!!T>::CompareTo(!0)
IL_000f: ldc.i4.0
IL_0010: bge.s IL_0015 // >= 0이면 a 반환
IL_0012: ldarg.1 // b 반환
IL_0013: br.s IL_0016
IL_0015: ldarg.0 // a 반환
}
constrained. !!T가 IComparable<T>.CompareTo를 값 타입의 직접 구현으로 디스패치하므로, boxing 없이 비교가 수행된다.
함정 2: IL2CPP와 AOT 컴파일의 제네릭 제한
Unity의 IL2CPP(IL을 C++로 변환하는 빌드 파이프라인)는 AOT(Ahead-Of-Time, 사전 컴파일) 방식이다. JIT처럼 실행 시점에 코드를 생성할 수 없으므로, 빌드 시점에 사용된 제네릭 조합만 코드로 변환된다.
// ❌ 리플렉션으로 런타임에 제네릭 타입을 동적 생성
Type listType = typeof(List<>).MakeGenericType(someRuntimeType);
object instance = Activator.CreateInstance(listType);
// IL2CPP 빌드에서 ExecutionEngineException 발생 가능!
// ✅ 빌드 타임에 사용할 제네릭 조합을 명시적으로 참조
void EnsureAOT()
{
// 코드에서 직접 사용하지 않더라도, 이 줄이 있으면 IL2CPP가 코드를 생성한다
new List<MyCustomStruct>();
new Dictionary<int, MyCustomStruct>();
}
함정 3: List<T>는 공변이 아니다
// ❌ 컴파일 에러 — List<T>는 공변이 아니다
List<string> strings = new List<string>();
// List<object> objects = strings; // CS0029 에러!
List<T>에는 Add(T item) ���서드가 있어서 T가 입력 위치에도 사용된다. 만약 공변이 허용되면 List<object>로 취급된 리스트에 int를 넣을 수 있게 되어 타입 안전성이 무너진다.
// ✅ IEnumerable<out T>는 공변이다 — 읽기 전용이므로 안전
IEnumerable<object> objects = strings; // OK
C# 버전별 변화 — 제네릭의 진화
C# 2.0 (2005) — 제네릭의 탄생
List<T>, Dictionary<TKey, TValue>, 제네릭 메서드, 제네릭 인터페이스, 제약 조건이 모두 이 버전에서 등장했다. System.Collections.Generic 네임스페이스가 추가되면서 ArrayList, Hashtable은 레거시가 되었다.
C# 4.0 (2010) — 공변성/반공변성
out— 공변성 (Covariance) 타입 매개변수를 출력(반환값) 위치에만 사용하겠다는 선언. 파생 타입 → 기반 타입 방향의 할당을 허용한다.
예시:IEnumerable<out T>→IEnumerable<string>을IEnumerable<object>에 할당 가능
in— 반공변성 (Contravariance) 타입 매개변수를 입력(매개변수) 위치에만 사용하겠다는 선언. 기반 타입 → 파생 타입 방향의 할당을 허용한다.
예시:Action<in T>→Action<object>를Action<string>에 할당 가능
Before C# 4.0 — 변성 지원 없음
// C# 2.0~3.5에서는 이것이 불가능했다
IEnumerable<string> strings = new List<string> { "A", "B" };
// IEnumerable<object> objects = strings; // 컴파일 에러!
After C# 4.0 — out/in 키워드 도입
// C# 4.0부터 IEnumerable<out T> 덕분에 가능
IEnumerable<string> strings = new List<string> { "A", "B" };
IEnumerable<object> objects = strings; // ✅ 공변성
Action<object> printObj = o => Console.WriteLine(o);
Action<string> printStr = printObj; // ✅ 반공변성
printStr("Hello");
// CovarianceDemo.Demo()
.locals init (
[0] class List`1<string>,
[1] class IEnumerable`1<object> // string → object 할당
)
IL_001f: ldloc.0 // List<string> 로드
IL_0020: stloc.1 // IEnumerable<object>에 저장 — 캐스팅 명령어 없음!
IL_0021: ldloc.0
IL_0022: call void CovarianceDemo::PrintAll(class IEnumerable`1<object>)
IL에 캐스팅 명령어(castclass, isinst)가 전혀 없다. 런타임이 IEnumerable<out T>의 out 선언을 인식하여 List<string>을 IEnumerable<object>로 안전하게 취급하는 것이다.
C# 7.3 (2018) — 추가 제약 조건
where T : unmanaged, where T : Enum, where T : Delegate 제약이 추가되었다.
// Before C# 7.3 — Enum 제약 불가
// public static string GetName<T>(T value) where T : Enum { } // 컴파일 에러!
// After C# 7.3 — Enum 제약 가능
public static string GetName<T>(T value) where T : Enum
{
return value.ToString();
}
unmanaged 제약은 Unity DOTS의 NativeArray<T>에서 핵심적이다. NativeArray<T> where T : struct보다 더 엄격하게, 참조 타입 필드가 없는 순수 값 타입만 허용하여 비관리 메모리에 안전하게 배치할 수 있다.
C# 11 (2022) — 정적 추상 멤버로 제네릭 수학
인터페이스에 정적 추상 멤버(Static Abstract Members)가 도입되면서, ��네릭 코드에서 연산자(+, -, * 등)를 직접 사용할 수 있게 되었다.
// Before C# 11 — 제네릭으로 덧셈 불가
// public static T Add<T>(T a, T b) { return a + b; } // 컴파일 에러!
// After C# 11 — INumber<T> 인터페이스로 제네릭 수학 가능
using System.Numerics;
public static T Add<T>(T a, T b) where T : INumber<T>
{
return a + b; // ✅ 연산자 사용 가능
}
// int, float, double 등 모든 숫자 타입에서 동작
int sum = Add(10, 20);
float fSum = Add(1.5f, 2.5f);
정리 — 핵심 체크리스트
- [ ] 제네릭 = 타입의 매개변수화 — 코드 로직은 하나, 타입은 사��� 시점에 결정
- [ ]
ArrayList→List<T>— boxing/unboxing 제거, 컴파일 타임 타입 검사, 코드 재사용 - [ ] JIT 전략 — 값 타입은 타입별 특수화(전용 코드), 참조 타입은 코드 공유(하나의 코드)
- [ ]
constrained.접두사 — 값 타입의 메서드 호출 시 boxing을 방지하는 IL 최적화 - [ ] 제약 조건 —
where T : struct/class/new()/BaseClass/IInterface로 T의 능력을 보장 - [ ] 공변성/반공변성 —
out T(출력만),in T(입력만)으로 제네릭 인터페이스의 할당 유연성 확보 - [ ] IL2CPP 주의 — AOT 빌드에서는 사용할 제네릭 조합을 코드에 명시적으로 참조해야 한다
- [ ]
List<T>는 무공변 — 공변이 필요하면IEnumerable<out T>등 읽기 전용 인터페이스를 사용
'C# 심화' 카테고리의 다른 글
| [PART7.제네릭(3/3)] 공변성과 반공변성 — in/out이 필요한 이유 (0) | 2026.04.05 |
|---|---|
| [PART7.제네릭(2/3)] 제네릭 제약 조건 — where T : 의 의미 (0) | 2026.04.05 |
| [PART6.문자열(3/3)] 문자열 포맷팅 — String.Format·보간·Span 기반 포맷 (0) | 2026.04.05 |
| [PART6.문자열(2/3)] string vs StringBuilder vs Span<char> — 문자열 처리 도구 선택 (0) | 2026.04.05 |
| [PART6.문자열(1/3)] string은 왜 불변인가 (0) | 2026.04.05 |
