| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- C#
- 오공완
- job
- AES
- 직장인공부
- base64
- Job 시스템
- Unity Editor
- 게임개발
- adfit
- 환급챌린지
- 프레임워크
- 암호화
- 최적화
- RSA
- sha
- TextMeshPro
- 직장인자기계발
- unity
- 패스트캠퍼스
- Tween
- ui
- 2D Camera
- 가이드
- 샘플
- Custom Package
- Framework
- Dots
- DotsTween
- 패스트캠퍼스후기
- Today
- Total
EveryDay.DevUp
[PART7.제네릭(2/3)] 제네릭 제약 조건 — where T : 의 의미 본문
제네릭 제약 조건 — where T : 의 의미
제네릭 <T>는 강력하지만, 아무 타입이나 받으면 T에 대해 아는 것이 없다. where T :는 컴파일러에게 "이 T는 최소한 이런 능력을 가진 타입이다"라고 증명서를 보여주는 것이다. 이 증명 덕분에 boxing 없이 인터페이스 메서드를 호출하고, 컴파일 타임에 실수를 잡고, JIT(Just-In-Time, 실행 시점 컴파일) 컴파일러가 최적화된 코드를 생성할 수 있다.
문제 제기
Unity로 모바일 게임을 만들고 있다. 무기마다 데미지 계산 로직이 다르고, 이를 하나의 시스템으로 처리하고 싶다.
// 인터페이스로 추상화했지만...
public interface IAttackable
{
int GetDamage();
}
public struct Sword : IAttackable
{
public int Power;
public int GetDamage() => Power;
}
public class BattleSystem
{
// 인터페이스 파라미터로 받는 방식
public static int CalculateDamage(IAttackable weapon)
{
return weapon.GetDamage();
}
}
겉보기에는 깔끔하다. 하지만 Sword는 구조체(struct)다. IAttackable 파라미터로 넘기는 순간 boxing이 발생한다. Sword의 4바이트 데이터가 힙(Heap)에 복사되고, GC(Garbage Collector, 메모리를 자동으로 회수하는 런타임 구성요소)가 추적해야 할 객체가 하나 늘어난다.
Update 루프에서 매 프레임 호출된다면? 초당 수십 개의 임시 객체가 힙에 쌓인다.
"인터페이스로 추상화하면서도 boxing을 피할 방법은 없을까?" — where T : 제네릭 제약 조건이 바로 그 답이다.
개념 정의
제약 조건이란 — 타입에 붙이는 자격 요건
일상에서 비유하면, 제네릭 <T>는 "아무나 들어올 수 있는 열린 문"이다. 제약 조건 where T :는 그 문 앞에 세운 경비원이다. "이 자격증을 가진 사람만 입장 가능합니다"라고 확인하는 것이다.
컴파일러는 경비원이 확인한 자격증을 기반으로, 그 타입이 어떤 메서드와 속성을 가지고 있는지 미리 알 수 있다. 그래서 T에 대해 안전하게 멤버를 호출할 수 있다.

제약 조건이 없으면 T는 System.Object 수준이라 ToString(), Equals() 같은 기본 메서드만 호출할 수 있다. where T : IAttackable을 붙이면 컴파일러가 "이 T는 GetDamage()를 반드시 가지고 있다"는 것을 알게 되고, 안전하게 호출을 허용한다.
where— 제네릭 제약 조건 키워드 제네릭 타입 매개변수T가 만족해야 할 조건을 선언한다. 메서드나 클래스 선언부 뒤에 위치한다.
예시:public void Attack<T>(T weapon) where T : IAttackableT는 반드시 IAttackable 인터페이스를 구현한 타입이어야 한다
기본적인 코드 형태를 보자.
// 제약 조건 없음 — T에 대해 아는 것이 없다
public static void PrintInfo<T>(T item)
{
// item.GetDamage(); → 컴파일 에러! T가 GetDamage()를 가졌는지 알 수 없음
Console.WriteLine(item.ToString()); // Object의 메서드만 가능
}
// 인터페이스 제약 — T가 IAttackable을 구현함을 보장
public static int GetDamage<T>(T weapon) where T : IAttackable
{
return weapon.GetDamage(); // 컴파일 OK — T는 GetDamage()를 반드시 가짐
}
제약 조건은 타입의 "자격 요건"이다. 컴파일러는 이 요건을 충족하지 않는 타입이 전달되면 즉시 컴파일 에러를 발생시킨다 — 런타임이 아니라 코드 작성 시점에 실수를 잡아준다.
내부 동작
IL에서 보는 boxing 유무 — constrained. 접두어의 비밀
제약 조건의 진짜 가치는 IL(Intermediate Language, C# 코드가 컴파일되어 생성되는 중간 코드) 수준에서 드러난다. 같은 인터페이스 메서드를 호출하더라도, 제약 조건 유무에 따라 IL 코드가 완전히 달라진다.

Before — 인터페이스 파라미터 (boxing 발생)
public interface IAttackable
{
int GetDamage();
}
public struct Sword : IAttackable
{
public int Power;
public int GetDamage() => Power;
}
public class BattleSystem
{
// 인터페이스 파라미터로 직접 받음
public static int CalculateDamage(IAttackable weapon)
{
return weapon.GetDamage();
}
}
// 호출부
var sword = new Sword { Power = 50 };
int damage = BattleSystem.CalculateDamage(sword); // Sword → IAttackable 변환
// === CalculateDamage(IAttackable) — 인터페이스 파라미터 버전 ===
.method public hidebysig static
int32 CalculateDamage (
class IAttackable weapon
) cil managed
{
IL_0001: ldarg.0
IL_0002: callvirt instance int32 IAttackable::GetDamage() // 가상 호출 — 이미 boxing된 상태
IL_000b: ret
}
// === Main — 호출부 ===
IL_0014: ldloc.0 // 스택에 있는 Sword 로드
IL_0015: box Sword // ★ boxing — Sword를 힙에 복사
IL_001a: call int32 BattleSystem::CalculateDamage(class IAttackable)
box Sword 명령어가 핵심이다. 구조체 Sword를 인터페이스 IAttackable로 넘기기 위해 힙에 복사본을 만든다. 이 복사본은 GC가 나중에 회수해야 할 쓰레기가 된다.
After — 제네릭 제약 조건 (boxing 제거)
public class BattleSystem
{
// 제네릭 제약 조건으로 받음
public static int CalculateDamageGeneric<T>(T weapon) where T : IAttackable
{
return weapon.GetDamage();
}
}
// 호출부
var sword = new Sword { Power = 50 };
int damage = BattleSystem.CalculateDamageGeneric(sword); // boxing 없음
// === CalculateDamageGeneric<T> — 제네릭 제약 조건 버전 ===
.method public hidebysig static
int32 CalculateDamageGeneric<(IAttackable) T> (
!!T weapon
) cil managed
{
IL_0001: ldarga.s weapon // T의 주소를 로드 (복사 아님)
IL_0003: constrained. !!T // ★ constrained 접두어 — boxing 회피
IL_0009: callvirt instance int32 IAttackable::GetDamage()
IL_0012: ret
}
// === Main — 호출부 ===
IL_0020: ldloc.0 // 스택에 있는 Sword 로드
IL_0021: call int32 BattleSystem::CalculateDamageGeneric<valuetype Sword>(!!0) // boxing 없이 직접 호출
constrained. !!T 접두어가 핵심이다. 이 접두어는 JIT 컴파일러에게 "T가 값 타입이면 boxing 없이 직접 메서드를 호출하고, 참조 타입이면 일반 가상 호출을 수행하라"고 지시한다.
호출부에서도 box 명령어가 완전히 사라졌다. Sword가 스택에서 바로 전달되고, 힙 할당은 제로다.
컴파일러가 이 최적화를 수행할 수 있는 이유: where T : IAttackable 제약 덕분에 컴파일러는 T가 반드시 GetDamage()를 구현한다는 것을 알고 있다. 이 보장이 있으니 constrained. 접두어를 안전하게 생성할 수 있고, JIT는 값 타입에 대해 boxing 없는 직접 호출로 최적화한다.
JIT 컴파일러의 코드 생성 전략
제약 조건이 적용된 제네릭 코드는 런타임에 JIT 컴파일러가 실제 타입을 보고 네이티브 코드를 생성한다.
- 참조 타입:
CalculateDamageGeneric<Enemy>(),CalculateDamageGeneric<Boss>()등 모든 참조 타입은 네이티브 코드를 단 한 번만 생성하고 공유한다. 모든 참조는 동일한 크기(포인터)이기 때문이다. - 값 타입:
CalculateDamageGeneric<Sword>(),CalculateDamageGeneric<Shield>()등 각 값 타입마다 별도의 네이티브 코드를 생성한다. 타입마다 크기와 필드 구조가 다르기 때문이다. 이 과정에서 인라이닝(함수 본문을 호출 지점에 직접 삽입)까지 적용되어 가상 호출 비용도 사라질 수 있다.
실전 적용
제약 조건의 종류와 Unity 활용
C#이 제공하는 주요 제약 조건과, 각각이 Unity에서 어떤 상황에 쓰이는지 정리한다.
struct 제약 — GC 압력 차단
where T : struct는 T를 값 타입으로 한정한다. 참조 타입이 들어오는 것을 막아 힙 할당을 원천 차단한다.
// 이벤트 데이터를 값 타입으로 강제 — GC 압력 제거
public struct DamageEvent
{
public int TargetId;
public float Amount;
}
public class EventBus
{
public static void Fire<T>(T eventData) where T : struct
{
// T는 반드시 값 타입 — 힙 할당 없이 스택에서 처리
ProcessEvent(eventData);
}
private static void ProcessEvent<T>(T data) where T : struct
{
Console.WriteLine($"Event fired: {typeof(T).Name}");
}
}
// 사용
EventBus.Fire(new DamageEvent { TargetId = 1, Amount = 25.5f });
// EventBus.Fire("string"); → 컴파일 에러! string은 참조 타입
Unity의 Update 루프에서 매 프레임 이벤트를 발생시키는 경우, struct 제약이 없으면 실수로 참조 타입 이벤트를 넘겨 GC 스파이크를 유발할 수 있다.
class 제약 — null 비교 허용
where T : class는 T를 참조 타입으로 한정한다. null 비교와 as 연산자 사용이 가능해진다.
public class NullChecker
{
public static bool IsNull<T>(T value) where T : class
{
return value == null; // class 제약 덕분에 null 비교 가능
}
}
// === IsNull<T> — class 제약 ===
.method public hidebysig static
bool IsNull<class T> (
!!T 'value'
) cil managed
{
IL_0001: ldarg.0
IL_0002: box !!T // 참조 타입이므로 실제로는 no-op (이미 참조)
IL_0007: ldnull
IL_0008: ceq // null과 비교
IL_000e: ret
}
IL에서 box !!T가 보이지만, T가 참조 타입으로 제약되어 있으므로 JIT 컴파일러는 이 box 명령어를 아무 동작도 하지 않는 no-op으로 최적화한다. 실제 힙 할당은 발생하지 않는다.
new() 제약 — 팩토리 패턴
where T : new()는 T가 매개변수 없는 public 생성자를 가진다는 것을 보장한다. new T() 표현식으로 인스턴스를 생성할 수 있다.
public class Factory
{
public static T CreateDefault<T>() where T : new()
{
return new T(); // new() 제약 덕분에 인스턴스 생성 가능
}
}
var sword = Factory.CreateDefault<Sword>(); // Sword의 기본 생성자 호출
// === CreateDefault<T> — new() 제약 ===
.method public hidebysig static
!!T CreateDefault<.ctor T> () cil managed // .ctor 제약이 메타데이터에 기록됨
{
IL_0001: call !!0 [System.Runtime]System.Activator::CreateInstance<!!T>() // ★ Activator.CreateInstance 호출
IL_000a: ret
}
new T()가 IL에서는 Activator.CreateInstance<T>()로 변환된다. 이 메서드는 내부적으로 리플렉션을 사용한다. 다만 JIT 컴파일러는 값 타입에 대해 initobj 명령어(스택에서 0으로 초기화)로 최적화한다.
주의: Unity의 IL2CPP(Intermediate Language To C++, Unity가 IL을 C++ 코드로 변환하는 AOT 컴파일러) 환경에서는 Activator.CreateInstance가 AOT(Ahead-Of-Time) 제약으로 인해 문제를 일으킬 수 있다. 값 타입에는 안전하지만, 참조 타입에 대해서는 빌드 시 확인이 필요하다.
struct + default — 안전한 기본값 생성
where T : struct와 default(T)를 조합하면 boxing 없이 안전하게 기본값을 생성한다.
public class Factory
{
public static T CreateStruct<T>() where T : struct
{
return default(T); // 모든 필드가 0으로 초기화된 T 반환
}
}
// === CreateStruct<T> — struct 제약 ===
.method public hidebysig static
!!T CreateStruct<valuetype .ctor ([System.Runtime]System.ValueType) T> () cil managed
{
IL_0001: ldloca.s 0
IL_0003: initobj !!T // ★ 스택에서 0으로 초기화 — 힙 할당 없음
IL_0009: ldloc.0
IL_000e: ret
}
initobj !!T는 스택 위의 메모리를 0으로 채운다. Activator.CreateInstance와 달리 리플렉션이 전혀 개입하지 않으며, 힙 할당도 없다. Unity 핫패스에서 안전하게 사용할 수 있다.
복수 제약 조건 — Unity 오브젝트 풀
하나의 타입 매개변수에 여러 제약을 동시에 걸 수 있다. 순서 규칙이 있다.
where T : class, IPoolable, new()
↑ ↑ ↑
구조 제약 인터페이스 생성자 (항상 마지막)
public interface IPoolable
{
void OnSpawn();
void OnDespawn();
}
// T는 참조 타입 + IPoolable 구현 + 기본 생성자 보유
public class ObjectPool<T> where T : class, IPoolable, new()
{
private Queue<T> pool = new Queue<T>();
public T Spawn()
{
T obj = pool.Count > 0 ? pool.Dequeue() : new T();
obj.OnSpawn();
return obj;
}
public void Despawn(T obj)
{
obj.OnDespawn();
pool.Enqueue(obj);
}
}
// === ObjectPool<T>.Spawn — 복수 제약 조건 ===
.method public hidebysig
instance !T Spawn () cil managed
{
IL_0001: call !!0 [System.Runtime]System.Activator::CreateInstance<!T>() // new T()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: box !T // IPoolable 인터페이스 호출을 위한 box
IL_000d: callvirt instance void IPoolable::OnSpawn() // 인터페이스 메서드 호출
IL_0018: ret
}
IL에서 box !T가 보인다. 이 경우 T가 class 제약으로 참조 타입임이 보장되므로, JIT는 이 box를 no-op으로 최적화한다. 만약 class 제약 없이 값 타입이 들어올 수 있었다면 실제 boxing이 발생했을 것이다.
복수 제약 조건 순서 규칙:
| 위치 | 허용되는 제약 | 예시 |
|---|---|---|
| 첫 번째 | class / struct / unmanaged / notnull 중 하나 |
where T : struct |
| 중간 | 기본 클래스 (최대 1개) + 인터페이스 (여러 개) | where T : MonoBehaviour, IPoolable |
| 마지막 | new() (항상 가장 마지막) |
where T : class, IPoolable, new() |
struct와 new()는 동시에 쓸 수 없다 — 구조체는 항상 매개변수 없는 생성자를 가지므로 암시적으로 new() 조건을 만족한다.
Unity 싱글톤 — 기본 클래스 제약
Unity에서 가장 자주 만나는 패턴이다. 여러 매니저 클래스(GameManager, SoundManager)에 대해 싱글톤 보일러플레이트를 반복 작성하는 대신, 제네릭 제약 조건으로 하나의 베이스 클래스에 담는다.
// T는 반드시 MonoBehaviour를 상속한 타입이어야 한다
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<T>(); // where T : MonoBehaviour 덕분에 가능
}
return instance;
}
}
}
// 사용 — 한 줄로 싱글톤 완성
public class GameManager : Singleton<GameManager>
{
public int Score { get; set; }
}
// 다른 곳에서
GameManager.Instance.Score += 100;
where T : MonoBehaviour 제약이 없으면 FindObjectOfType<T>()를 호출할 수 없다 — 이 Unity API 자체가 Component를 상속하는 타입만 받기 때문이다. 제약 조건이 Unity API와 제네릭을 안전하게 연결해주는 다리 역할을 한다.
함정과 주의사항
함정 1: new() 제약과 IL2CPP AOT 문제
// ❌ 위험 — IL2CPP에서 런타임 에러 가능
public class Factory
{
public static T Create<T>() where T : new()
{
return new T(); // Activator.CreateInstance 사용 — 리플렉션 기반
}
}
new T()는 IL에서 Activator.CreateInstance<T>()로 변환된다. 이 메서드는 리플렉션을 사용하는데, Unity의 IL2CPP AOT 환경에서는 리플렉션으로 생성할 타입의 코드가 빌드 시점에 포함되지 않으면 ExecutionEngineException이 발생할 수 있다.
// ✅ 안전 — 팩토리 델리게이트 사용
public class SafeFactory<T>
{
private readonly Func<T> creator;
public SafeFactory(Func<T> creator)
{
this.creator = creator;
}
public T Create() => creator();
}
// 사용
var factory = new SafeFactory<Enemy>(() => new Enemy());
var enemy = factory.Create(); // Activator 없이 직접 생성
팩토리 델리게이트를 주입하면 Activator.CreateInstance를 우회할 수 있다. IL2CPP 환경에서도 안전하다.
함정 2: struct 제약에서 null 비교 시도
// ❌ 컴파일 에러 — struct는 null이 될 수 없다
public static bool IsEmpty<T>(T value) where T : struct
{
return value == null; // CS0019: 'T'와 '<null>'에 '==' 연산자를 적용할 수 없습니다
}
where T : struct로 제약된 T는 값 타입이므로 null이 될 수 없다. null 비교 자체가 컴파일 에러다.
// ✅ 올바른 방식 — default와 비교
public static bool IsDefault<T>(T value) where T : struct
{
return EqualityComparer<T>.Default.Equals(value, default(T));
}
값 타입의 "비어있음"을 확인하려면 default(T)와 비교한다. EqualityComparer<T>.Default는 boxing 없이 비교를 수행한다.
함정 3: 제약 조건은 상속되지 않는다
public class Base
{
public virtual void Process<T>(T item) where T : IComparable<T>
{
// ...
}
}
public class Derived : Base
{
// ❌ 컴파일 에러 — override 메서드에는 제약 조건을 다시 명시할 수 없다
// public override void Process<T>(T item) where T : IComparable<T> { }
// ✅ 제약 조건은 자동으로 상속됨 — 다시 쓰지 않는다
public override void Process<T>(T item)
{
// T는 여전히 IComparable<T>를 구현한다 — 상위 클래스의 제약이 그대로 적용
int result = item.CompareTo(default(T));
}
}
override할 때 where 절을 다시 쓰면 컴파일 에러가 발생한다. 제약 조건은 베이스 클래스의 선언에서 자동으로 상속된다.
C# 버전별 변화
제네릭 제약 조건은 C# 2.0에서 처음 도입된 이후, 여러 버전에 걸쳐 새로운 제약이 추가되었다.
C# 2.0 — 기본 제약 조건 도입 (2005)
struct, class, new(), 기본 클래스, 인터페이스, U(naked type) 제약이 모두 이 버전에서 도입되었다. 이전에는 제네릭 자체가 없었으므로 ArrayList와 object 캐스팅에 의존해야 했다.
// Before (C# 1.x) — 제네릭 없음, object 캐스팅 필수
ArrayList weapons = new ArrayList();
weapons.Add(new Sword { Power = 50 }); // boxing 발생
int damage = ((IAttackable)weapons[0]).GetDamage(); // unboxing + 캐스팅
// After (C# 2.0) — 제네릭 제약 조건으로 타입 안전 + boxing 제거
public static int GetDamage<T>(T weapon) where T : IAttackable
{
return weapon.GetDamage(); // boxing 없음, 타입 안전
}
C# 7.3 — unmanaged, Enum, Delegate 제약 (2018)
포인터 연산이 필요한 고성능 코드와 열거형/델리게이트를 타입 안전하게 다룰 수 있게 되었다.
// Before (C# 7.2 이하) — Enum을 제약할 방법이 없음
public static string GetName<T>(T value) // T가 Enum인지 런타임에야 알 수 있음
{
if (typeof(T).IsEnum)
return Enum.GetName(typeof(T), value);
throw new ArgumentException("Enum only");
}
// After (C# 7.3) — 컴파일 타임에 Enum만 허용
public static string GetName<T>(T value) where T : struct, Enum
{
return Enum.GetName(typeof(T), value); // T가 Enum임이 보장됨
}
where T : unmanaged는 포인터를 포함하지 않는 순수 값 타입(int, float, 참조 필드가 없는 struct)만 허용한다. Unity의 ECS(Entity Component System) / DOTS(Data-Oriented Technology Stack)에서 NativeArray<T>에 저장할 수 있는 타입을 컴파일 타임에 보장할 때 사용된다.
// C# 7.3 — unmanaged 제약
public struct EnemyData // 참조 필드 없는 순수 값 타입
{
public int Id;
public float Hp;
}
public static void ProcessBuffer<T>(T[] data) where T : unmanaged
{
// sizeof(T) 사용 가능, Span<T>과 조합하여 zero-allocation 처리 가능
int size = System.Runtime.InteropServices.Marshal.SizeOf<T>();
}
C# 8.0 — notnull 제약 (2019)
NRT(Nullable Reference Types, null 허용 참조 타입)와 함께 도입되었다. null이 될 수 없는 타입만 허용한다.
// C# 8.0 — null을 허용하지 않는 타입만 받음
public class Registry<TKey, TValue>
where TKey : notnull
{
private Dictionary<TKey, TValue> map = new();
public void Register(TKey key, TValue value)
{
map[key] = value; // TKey가 null이 될 수 없으므로 Dictionary 키로 안전
}
}
C# 11 — 정적 가상 인터페이스 멤버 (2022)
인터페이스에 정적 연산자를 정의하고, 이를 제약 조건으로 사용할 수 있게 되었다. 이전에는 T + T 같은 수학 연산을 제네릭으로 작성하는 것이 불가능했다.
// Before (C# 10 이하) — 제네릭 덧셈 불가능
// public static T Add<T>(T a, T b) => a + b; // 컴파일 에러
// After (C# 11) — IAdditionOperators 제약으로 가능
using System.Numerics;
public static T Add<T>(T a, T b) where T : IAdditionOperators<T, T, T>
{
return a + b; // T가 + 연산자를 지원함이 보장됨
}
int sum = Add(5, 10); // 15
float fSum = Add(3.14f, 2.0f); // 5.14
이 기능은 Unity에서 아직 .NET 버전 제약으로 바로 사용하기 어렵지만, Unity 6 이후 .NET 8+ 지원이 확대되면 수학 유틸리티 작성에 큰 변화가 예상된다.
정리
| 항목 | 핵심 내용 |
|---|---|
| 제약 조건이란 | 타입 매개변수 T에 자격 요건을 부여하여, 컴파일 타임에 타입 안전성을 보장하는 메커니즘 |
| boxing 제거 | where T : IInterface로 인터페이스 제약을 걸면 constrained. IL 접두어 덕분에 값 타입도 boxing 없이 인터페이스 메서드 호출 가능 |
| struct 제약 | where T : struct는 값 타입만 허용 — GC 압력 차단, Unity 핫패스에서 필수 |
| class 제약 | where T : class는 참조 타입만 허용 — null 비교, as 연산자 사용 가능 |
| new() 제약 | new T() 가능하지만 IL에서 Activator.CreateInstance 사용 — IL2CPP AOT 주의 |
| 복수 제약 | 구조 제약 → 기본 클래스 → 인터페이스 → new() 순서, struct와 new() 동시 불가 |
| Unity 패턴 | 싱글톤(where T : MonoBehaviour), 오브젝트 풀(where T : class, IPoolable, new()), 이벤트 버스(where T : struct) |
| IL2CPP 함정 | new T()의 Activator.CreateInstance가 AOT에서 문제 가능 — 팩토리 델리게이트로 우회 |
| 제약 상속 | override 메서드에서 where 절 재선언 불가 — 자동 상속됨 |
'C# 심화' 카테고리의 다른 글
| [PART8.컬렉션과 LINQ(1/6)] List<T> vs Array — 언제 무엇을 선택하는가 (1) | 2026.04.05 |
|---|---|
| [PART7.제네릭(3/3)] 공변성과 반공변성 — in/out이 필요한 이유 (0) | 2026.04.05 |
| [PART7.제네릭(1/3)] 제네릭 — <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 |
