[PART1.C# 런타임과 .NET 플랫폼 기초(5/11)] using 별칭으로 튜플·제네릭에 이름 붙이기 (C# 12)
긴 타입 이름을 한 줄로 줄이는 컴파일 타임 장치 / 튜플·배열·포인터에도 이름을 붙이는 C# 12의 확장 / IL에는 흔적이 남지 않는 순수 편의 기능
목차
1. 문제 제기 — 타입 이름이 코드보다 길어질 때
Unity 모바일 클라이언트를 만들다 보면 "격자 좌표"를 표현해야 할 때가 있습니다. 타일맵, 퍼즐 보드, 인벤토리 슬롯 전부 (행, 열) 쌍으로 다룹니다. 여기에 UnityEngine.Vector2Int를 쓰면 편하지만, 순수 C# 로직 계층(예: 길찾기 알고리즘)에서는 UnityEngine 의존성을 빼고 싶을 때가 많습니다. 그러면 선택지는 둘입니다.
- 선택지 A: 매번
(int row, int column)튜플을 타이핑 - 선택지 B:
readonly record struct GridCell(int Row, int Column)를 선언
A는 타이핑이 매번 반복되고, 함수 시그니처가 지저분해집니다. B는 제대로 된 해결책이지만 "그냥 두 정수 묶음"에 파일 하나를 소비하는 게 과합니다. 특히 메서드가 필요 없고 도메인 이름만 붙이고 싶을 때 배보다 배꼽이 큽니다.
// 함수 시그니처가 타입 이름에 잡아먹힌다
public static List<(int row, int column)> FindPath(
(int row, int column) start,
(int row, int column) goal,
Dictionary<(int row, int column), float> costMap)
{
// ...
}
C# 1.0부터 있던 using 별칭 지시문은 이런 상황을 위해 존재했지만, 오래된 약점이 있었습니다. 튜플·배열·포인터 같은 "이름 없는 타입"에는 쓸 수 없었다는 것입니다. C# 12는 이 한 가지 제약을 풀어주는 것이 핵심입니다. 이제 using GridCell = (int Row, int Column); 한 줄로 파일 상단에서 도메인 이름을 부여하고, 본문은 짧게 쓸 수 있습니다.
이 글은 이 한 줄의 문법이 컴파일러에 어떻게 처리되는지, IL에 흔적이 남는지, 언제 별칭으로 끝내고 언제 record struct로 승격해야 하는지 까지 한 번에 정리합니다.
2. 개념 정의 — "이름표" 한 장을 파일에 붙인다
비유: 우편 수취함의 이름표
아파트 우편함에는 정해진 호수(101, 102, ...)가 있습니다. 여기에 "홍길동", "관리실" 같은 이름표를 붙이면 편하지만, 이름표가 사라진다고 호수가 바뀌지는 않습니다. 택배기사는 실제 호수로 물건을 배달합니다. 이름표는 사람이 빨리 알아보라고 붙여놓은 것일 뿐입니다.
using 별칭이 정확히 이 역할입니다. 컴파일러는 소스에 적힌 이름표(GridCell)를 컴파일 시점에 원래 타입(System.ValueTuple<int, int>)으로 바꿔서 IL에 기록합니다. 런타임(CLR, Common Language Runtime — .NET 프로그램을 실행하는 가상 머신)은 GridCell이라는 이름을 아예 모릅니다.
개념 다이어그램

기본 코드
파일 최상단(네임스페이스 선언 위 또는 파일 범위 네임스페이스 아래)에 선언합니다.
// 파일 상단 — 네임스페이스 위
using GridCell = (int Row, int Column);
using CostMap = System.Collections.Generic.Dictionary<(int, int), float>;
namespace MyGame.Logic;
public static class Pathfinding
{
public static GridCell Step(GridCell from, int dr, int dc)
{
// 별칭은 "진짜 타입"처럼 쓸 수 있다 — 필드 이름 Row/Column도 그대로 접근
return (from.Row + dr, from.Column + dc);
}
}
using X = Type;— using 별칭 지시문 (using alias directive) 파일 범위에서 긴 타입 이름이나 네임스페이스에 짧은 이름을 부여하는 지시문이다. C# 12부터 튜플·배열·포인터·nullable 같은 이름 없는 타입에도 적용할 수 있다.
예시:using GridCell = (int Row, int Column);이후 이 파일 안에서GridCell은(int Row, int Column)튜플을 가리킨다.
핵심은 using GridCell = ... 한 줄이 선언된 소스 파일 안에서만 유효하다는 점입니다(파일 스코프). 다른 .cs 파일에서 GridCell을 쓰려면 각자 다시 선언하거나, C# 10에서 도입된 global using 과 결합해야 합니다.
기본 동작의 IL 증거
다음 두 코드는 완전히 같은 IL을 생성합니다.
// Before — 별칭 없음
public class Before
{
public static (int X, int Y) GetPoint() => (3, 5);
}
// After — using 별칭
using Point = (int X, int Y);
public class After
{
public static Point GetPoint() => (3, 5);
}
두 파일을 같은 프로젝트에서 컴파일한 뒤 ilspycmd로 디컴파일한 실제 IL은 아래와 같습니다.
// After::GetPoint — 별칭 사용
.method public hidebysig static
valuetype [System.Runtime]System.ValueTuple`2<int32, int32> GetPoint() cil managed
{
.param [0]
.custom instance void TupleElementNamesAttribute::.ctor(string[])
= ( 01 00 02 00 00 00 01 58 01 59 00 00 ) // 요소 이름 "X", "Y"
IL_0000: ldc.i4.3 // 상수 3 로드
IL_0001: ldc.i4.5 // 상수 5 로드
IL_0002: newobj instance void
valuetype ValueTuple`2<int32, int32>::.ctor(!0, !1) // 튜플 생성
IL_0007: ret
}
// Before::GetPoint — 별칭 없음
.method public hidebysig static
valuetype [System.Runtime]System.ValueTuple`2<int32, int32> GetPoint() cil managed
{
// ... 위와 완전히 동일한 본문 ...
}
반환 타입(ValueTuple\2<int32, int32>), TupleElementNamesAttribute 의 바이트 배열(01 58 01 59 = 'X', 'Y'), 본문 IL 까지 **한 바이트도 다르지 않습니다**. 컴파일러가 Point` 라는 이름을 치환한 뒤 더 이상 어디에도 기록하지 않는다는 뜻입니다.
3. 내부 동작 — C# 컴파일러는 무엇을 치환하는가
치환 순서 다이어그램

튜플 요소 이름은 어디에 저장되는가
using GridCell = (int Row, int Column); 에서 Row, Column 이라는 이름은 컴파일러가 따로 보관합니다. 이 이름들은 타입 시스템의 일부가 아니라 메타데이터로 붙습니다.
using GridCell = (int Row, int Column);
public class Demo
{
public static GridCell Make(int r, int c) => (r, c);
}
.method public hidebysig static
valuetype [System.Runtime]System.ValueTuple`2<int32, int32> Make(int32 r, int32 c) cil managed
{
.param [0]
// 반환 값에 "Row", "Column" 이름을 붙이는 메타데이터
.custom instance void [System.Runtime]System.Runtime.CompilerServices.TupleElementNamesAttribute::.ctor(string[])
= ( 01 00 02 00 00 00 03 52 6F 77 06 43 6F 6C 75 6D 6E 00 00 )
// ^^^^^^^^^^^^^^^ "Row" (03 = 길이 3, "R" "o" "w")
// ^^^^^^^^^^^^^^^^^^^^^^ "Column" (06 = 길이 6)
IL_0000: ldarg.0 // r 로드
IL_0001: ldarg.1 // c 로드
IL_0002: newobj instance void
valuetype ValueTuple`2<int32, int32>::.ctor(!0, !1)
IL_0007: ret
}
TupleElementNamesAttribute 에는 "Row", "Column" 이라는 문자열 이름이 바이트 배열로 인코딩돼 들어갑니다. .Row 같은 필드 접근은 이 메타데이터를 참고해 Item1 에 대한 접근으로 컴파일됩니다. 즉, 소스에서 p.Row 라고 써도 IL 수준에서는 p.Item1 이 된다는 뜻입니다.
이것이 중요한 이유는 리플렉션으로 튜플 필드 이름에 의존하면 안 된다는 결론이 여기서 나오기 때문입니다. 런타임에 GetFields() 로 얻는 이름은 Item1, Item2 이지 Row, Column 이 아닙니다.
별칭 자체는 타입이 아니다
using GridCell = (int Row, int Column); 을 해도 GridCell 이라는 새 타입이 생성되지는 않습니다. 아래 사실이 이를 증명합니다.
using Point = (int X, int Y);
public class Demo
{
public static void Run()
{
Point p = (3, 5);
(int X, int Y) q = (3, 5);
// typeof 로 확인하면 둘은 같은 타입
System.Console.WriteLine(typeof(Point) == typeof((int X, int Y))); // True
System.Console.WriteLine(p.GetType().Name); // ValueTuple`2
// 두 변수는 대입도 자유롭다
p = q; // 타입 검사 통과
}
}
// typeof(Point) == typeof((int X, int Y)) 부분
IL_000d: ldtoken valuetype [System.Runtime]System.ValueTuple`2<int32, int32>
IL_0012: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype System.RuntimeTypeHandle)
IL_0017: ldtoken valuetype [System.Runtime]System.ValueTuple`2<int32, int32> // ← 완전히 같은 토큰
IL_001c: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype System.RuntimeTypeHandle)
IL_0021: call bool [System.Runtime]System.Type::op_Equality(...)
typeof(Point) 와 typeof((int X, int Y)) 가 내놓는 ldtoken 이 완전히 동일합니다. Point 는 별명일 뿐, 타입 시스템에는 존재하지 않는다는 IL 레벨 증거입니다.
4. 실전 적용 — Unity 로직 계층에서 이름값을 챙기는 법
언제 쓸 것인가
| 상황 | 별칭이 맞는가 | 대안 |
|---|---|---|
순수 로직에서 (int, int) 두 개 묶어 전달 |
✅ | - |
| 한 파일 안에서 자주 쓰는 제네릭 타입 축약 | ✅ | - |
| 팀 전체가 공유하는 도메인 개념 | ⚠️ (global using 필요) |
readonly record struct |
| 연산(거리, 회전 등) 을 붙여야 함 | ❌ | readonly record struct |
| public API 로 노출되는 타입 | ❌ | readonly record struct |
Before — 별칭 없이 타이핑 지옥
Unity 로직 계층에서 A* 길찾기 함수를 작성한다고 가정하겠습니다. UnityEngine.Vector2Int 의존을 피하고 순수 C# 으로 두고 싶습니다.
// Before — 시그니처에 튜플이 반복 등장
using System.Collections.Generic;
namespace MyGame.Logic;
public static class PathfindingBefore
{
public static List<(int row, int column)> FindPath(
(int row, int column) start,
(int row, int column) goal,
Dictionary<(int row, int column), float> costMap)
{
var open = new PriorityQueue<(int row, int column), float>();
var cameFrom = new Dictionary<(int row, int column), (int row, int column)>();
// ... 중략 ...
return new List<(int row, int column)>();
}
}
(int row, int column) 이 한 함수에 5번 등장합니다. 가독성이 떨어지고, 나중에 3차원으로 확장한다면 5곳을 모두 수정해야 합니다.
After — 별칭으로 한 줄 요약
// After — 별칭으로 도메인 의미 부여
using System.Collections.Generic;
using GridCell = (int Row, int Column);
using OpenQueue = System.Collections.Generic.PriorityQueue<(int Row, int Column), float>;
using CameFromMap = System.Collections.Generic.Dictionary<(int Row, int Column), (int Row, int Column)>;
namespace MyGame.Logic;
public static class PathfindingAfter
{
public static List<GridCell> FindPath(
GridCell start,
GridCell goal,
Dictionary<GridCell, float> costMap)
{
var open = new OpenQueue();
var cameFrom = new CameFromMap();
// ... 중략 ...
return new List<GridCell>();
}
}
시그니처가 한눈에 들어오고, 격자 차원을 바꿀 때는 using GridCell = ...; 한 줄만 수정하면 됩니다. 성능은 Before와 동일 — IL 레벨에서 두 코드는 완전히 같은 명령어 시퀀스를 생성합니다.
이벤트 페이로드에 도메인 이름 붙이기
Unity 에서 이벤트를 쏠 때 가벼운 데이터 묶음을 전달할 일이 많습니다. 매번 struct 를 만들면 파일이 범람하므로, 별칭이 중간 해답으로 유용합니다.
// 파일 상단에서 이벤트 페이로드를 별칭으로 정의
using DamagePayload = (int AttackerId, float Amount, DamageType Type);
namespace MyGame.Combat;
public enum DamageType { Physical, Magical, True }
public class CombatEvents
{
public event System.Action<DamagePayload>? OnPlayerDamaged;
public void Raise(int attackerId, float amount, DamageType type)
{
// 구조화된 이름으로 전달 — 호출부에서 순서 헷갈릴 일 없음
OnPlayerDamaged?.Invoke((attackerId, amount, type));
}
}
이 구조가 한 파일에만 쓰이고, 이벤트 핸들러에서 .AttackerId, .Amount, .Type 으로 접근할 수 있다면 별칭으로 충분합니다. 다른 파일에서도 쓰기 시작하면 readonly record struct 로 승격하는 신호입니다.
IL 레벨에서 Before/After 비교
앞서 언급한 대로 ilspycmd 로 두 버전의 IL 을 직접 뽑아보면, FindPath 본문의 명령어 시퀀스가 완전히 동일합니다. 아래는 양쪽 함수 시그니처를 나란히 놓은 것입니다.
// PathfindingBefore::FindPath
.method public hidebysig static
class [System.Collections]System.Collections.Generic.List`1<
valuetype [System.Runtime]System.ValueTuple`2<int32, int32>>
FindPath(
valuetype [System.Runtime]System.ValueTuple`2<int32, int32> start,
valuetype [System.Runtime]System.ValueTuple`2<int32, int32> 'goal',
class [System.Collections]System.Collections.Generic.Dictionary`2<
valuetype [System.Runtime]System.ValueTuple`2<int32, int32>,
float32> costMap)
// PathfindingAfter::FindPath
.method public hidebysig static
class [System.Collections]System.Collections.Generic.List`1<
valuetype [System.Runtime]System.ValueTuple`2<int32, int32>>
FindPath(
valuetype [System.Runtime]System.ValueTuple`2<int32, int32> start,
valuetype [System.Runtime]System.ValueTuple`2<int32, int32> 'goal',
class [System.Collections]System.Collections.Generic.Dictionary`2<
valuetype [System.Runtime]System.ValueTuple`2<int32, int32>,
float32> costMap)
한 글자도 다르지 않습니다. 별칭은 성능 트레이드오프가 없습니다. 순전히 사람이 읽기 좋으라고 붙이는 이름표일 뿐입니다.
5. 함정과 주의사항
❌ 함정 1 — 필드 이름으로 공개 API 를 설계했다고 착각
// ❌ 잘못된 기대 — public API 계약으로 Row/Column 이 고정된다고 믿음
using GridCell = (int Row, int Column);
namespace MyGame.Public;
public static class GridApi
{
// 다른 어셈블리에서 이 함수를 호출할 때, Row/Column 이름이 의미가 있을까?
public static GridCell GetCell(int r, int c) => (r, c);
}
다른 .cs 파일이나 다른 어셈블리에서는 using GridCell = ... 선언이 없으므로 GridCell 을 인식하지 못합니다. 호출부는 (int, int) 로 보고 쓰게 됩니다.
// ✅ 올바른 접근 — 공개 API 는 타입으로 고정
namespace MyGame.Public;
public readonly record struct GridCell(int Row, int Column);
public static class GridApi
{
public static GridCell GetCell(int r, int c) => new(r, c);
}
판단 기준: 단일 어셈블리 안, 단일 파일 안, 또는 global using 으로 범위가 정의된 프로젝트 내부라면 별칭으로 충분합니다. 외부 호출자에게 노출되는 API 는 반드시 실제 타입(readonly record struct 등)으로 정의합니다.
❌ 함정 2 — 리플렉션으로 필드 이름에 의존
using UserData = (int Id, string Name);
public static class Serializer
{
public static void Print(UserData u)
{
// ❌ GetFields() 는 Id, Name 을 반환하지 않는다
foreach (var field in u.GetType().GetFields())
{
System.Console.WriteLine(field.Name); // "Item1", "Item2" 출력
}
}
}
// UserData 는 런타임에 ValueTuple`2 이므로, GetFields() 는 Item1/Item2 를 반환
IL_0005: callvirt instance class [System.Runtime]System.Reflection.FieldInfo[]
[System.Runtime]System.Type::GetFields()
// ... 결과로 나오는 FieldInfo 의 Name 은 "Item1", "Item2"
이유는 3번 절에서 확인한 대로, Id, Name 은 TupleElementNamesAttribute 메타데이터에만 있고, 실제 필드 이름은 Item1, Item2 이기 때문입니다. 직렬화나 동적 매핑을 한다면 이 메타데이터를 직접 파싱하거나, 애초에 record struct 를 쓰는 편이 안전합니다.
// ✅ 안전한 접근 — 명시적 record struct
public readonly record struct UserData(int Id, string Name);
public static class SerializerFixed
{
public static void Print(UserData u)
{
foreach (var prop in u.GetType().GetProperties())
System.Console.WriteLine(prop.Name); // "Id", "Name" 출력
}
}
❌ 함정 3 — 부분 클래스에서 다른 파일의 별칭 기대
// File1.cs
using UserId = System.Int32;
public partial class User
{
public UserId Id { get; set; }
}
// File2.cs — UserId 를 똑같이 쓰려고 하면?
public partial class User
{
public UserId GetCopy() => Id; // ❌ CS0246: UserId 형식을 찾을 수 없습니다
}
using 별칭은 파일 스코프입니다. 부분 클래스가 두 파일에 걸쳐 있어도 File2.cs 는 File1.cs 의 별칭을 모릅니다. 같은 타입으로 해석되려면 File2.cs 에도 같은 using 을 선언하거나, 프로젝트 루트에 global using 을 두어야 합니다.
// ✅ 해결 — GlobalUsings.cs 파일을 하나 만들어 전역으로 선언
// GlobalUsings.cs
global using UserId = System.Int32;
❌ 함정 4 — Unity 버전이 C# 12 를 지원하지 않음
Unity 는 C# 언어 버전 도입이 느린 편입니다. 개발 시점에 사용 중인 Unity 버전이 C# 12 를 지원하는지 반드시 확인해야 합니다.
| Unity 버전 | 기본 C# 언어 버전 | using 별칭 확장 사용 가능? |
|---|---|---|
| 2022 LTS | C# 9 | ❌ 튜플/배열/포인터 별칭 불가 |
| 2023.x | C# 9 | ❌ |
| Unity 6 (6000.0+) | C# 9 (기본) ~ C# 10+ | ⚠️ <LangVersion> 덮어쓰기 필요 |
조치: .csproj 파일(또는 Unity Assembly Definition 의 커스텀 옵션)에서 <LangVersion>12</LangVersion> 또는 <LangVersion>latest</LangVersion> 를 명시해도, Roslyn 컴파일러가 해당 버전을 해석할 수 있어야 합니다. 컴파일이 통과해도 IL2CPP 변환 단계에서 새 언어 기능이 문제를 일으킬 수 있으므로 실제 모바일 빌드까지 반드시 테스트합니다.
<!-- Unity 프로젝트의 .csproj 가 자동 생성된 뒤 커스텀하려면 Directory.Build.props 사용 -->
<Project>
<PropertyGroup>
<LangVersion>12</LangVersion>
</PropertyGroup>
</Project>
❌ 함정 5 — 별칭 이름을 기존 타입과 겹치게 짓기
// ❌ System.Drawing.Point 와 의도치 않게 혼용 가능
using Point = (int X, int Y);
// 다른 곳에서 using System.Drawing; 하는 순간 이름 충돌
// ✅ 도메인을 명시하는 이름 선택
using GridCell = (int Row, int Column);
using ScreenCoord = (float X, float Y);
Point, Result, Vector 처럼 표준 라이브러리에 흔한 이름은 별칭에서 피하는 것이 안전합니다.
6. C# 버전별 변화
using 별칭은 C# 1.0 부터 있었지만, 허용되는 우변 타입이 버전마다 늘어났습니다. 핵심 확장은 C# 12 의 "Alias Any Type" 제안입니다.
C# 1.0 ~ C# 11 — 이름 있는 타입만 허용
// ✅ C# 1.0 부터 가능
using Timer = System.Timers.Timer; // 클래스
using IntList = System.Collections.Generic.List<int>; // 구성된 제네릭
// ❌ C# 11 까지 전부 컴파일 에러
// using Point = (int X, int Y); // 튜플
// using IntArray = int[]; // 배열
// using MaybeInt = int?; // nullable
// using IntPtr = int*; // 포인터
IL 관점: C# 11 이하에서는 튜플 타입에 이름을 붙이려면 반드시 struct/class/record 를 선언해야 했습니다. 한 번 선언된 구조체는 IL 에 타입 정의로 올라가므로, 별칭과 달리 .dll 메타데이터가 증가했습니다.
C# 12 (2023) — Alias Any Type
C# 12 부터는 거의 모든 타입에 별칭을 붙일 수 있습니다. ([dotnet/csharplang · Using Alias Directive to Allow to Reference Any Kind of Type](https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/using-alias-types.md))
// ✅ C# 12 부터 모두 가능
using Point = (int X, int Y); // 튜플
using IntMatrix = int[,]; // 다차원 배열
using MaybeInt = int?; // nullable 값 타입
using MaybeName = string?; // nullable 참조 타입 (NRT, Nullable Reference Types 켜진 경우)
using NodePtr = int*; // 포인터 (사용처는 unsafe 필요)
using StringMap = System.Collections.Generic.Dictionary<string, string>; // 기존에도 가능했던 구성된 제네릭
여전히 금지되는 것:
dynamic자체의 별칭 (using D = dynamic;은 제안에서 제외)- 열린 제네릭 (
using L = List<>;— 불가) - 타입 매개변수를 포함하는 별칭 (별칭은 컴파일 타임 상수이지 제네릭이 아니다)
IL 레벨에서 C# 11 과 C# 12 의 차이
같은 의도의 코드를 두 언어 버전으로 작성해보면, C# 12 쪽이 어셈블리 크기가 더 작습니다.
// C# 11 스타일 — 별칭을 쓰고 싶으면 struct 로 우회
public readonly record struct GridCell(int Row, int Column);
public static class PathfinderV11
{
public static GridCell Step(GridCell from, int dr, int dc)
=> new(from.Row + dr, from.Column + dc);
}
// C# 11 — GridCell 이 정식 타입으로 IL 에 등장
.class public sealed sequential ansi serializable beforefieldinit GridCell
extends [System.Runtime]System.ValueType
implements class [System.Runtime]System.IEquatable`1<valuetype GridCell>
{
// ~200 줄 분량의 record 보일러플레이트 (Equals, GetHashCode, ToString, Deconstruct, ...)
// ... 생략 ...
}
// C# 12 스타일 — 별칭 하나로 끝
using GridCell = (int Row, int Column);
public static class PathfinderV12
{
public static GridCell Step(GridCell from, int dr, int dc)
=> (from.Row + dr, from.Column + dc);
}
// C# 12 — 별도 타입 정의 없음, ValueTuple 을 그대로 사용
.class public abstract sealed auto ansi beforefieldinit PathfinderV12
extends [System.Runtime]System.Object
{
.method public hidebysig static
valuetype [System.Runtime]System.ValueTuple`2<int32, int32>
Step(valuetype ValueTuple`2<int32, int32> from, int32 dr, int32 dc) cil managed
{
// ~20 줄 본문만 존재 — 별칭 이름표는 IL 에 없다
}
}
트레이드오프:
- C# 11
record struct는Equals,GetHashCode,Deconstruct,ToString같은 메서드를 갖춘 진짜 타입. 어셈블리에 올라가고 리플렉션에서 보인다. - C# 12
using별칭은 이름표만 붙는 튜플. 메서드 정의 불가, 리플렉션에서 "GridCell" 이라는 이름은 보이지 않는다.
즉, 두 방식은 같은 용도처럼 보여도 배포 단위(어셈블리) 관점에서는 매우 다릅니다. 동작이 필요하면 record struct, 이름만 필요하면 using 별칭.
7. 정리
using별칭은 컴파일 타임 치환입니다. C# 컴파일러가 파일을 읽을 때 이름표를 원본 타입으로 바꿉니다. IL 에는 별칭 이름이 남지 않습니다.- C# 12 의 확장점은 "이름 없는 타입" 입니다. 튜플(
(int X, int Y)), 배열(int[]), 포인터(int*), nullable(int?) 이 모두 별칭 우변에 올 수 있습니다. - 튜플 별칭은
System.ValueTuple<...>로 풀립니다. 요소 이름(X,Y)은TupleElementNamesAttribute메타데이터로 붙으며, 런타임 필드는 여전히Item1,Item2입니다. - 별칭은 타입이 아닙니다.
typeof(Point) == typeof((int X, int Y))가true입니다. 메서드·연산자·정적 멤버를 추가할 수 없고, 공개 API 에 노출하기에 부적합합니다. - 파일 스코프가 원칙입니다. 다른 파일에서 쓰려면 각 파일에 다시 선언하거나
global using으로 프로젝트 전역으로 올립니다. - 리플렉션과 직렬화에서 주의가 필요합니다.
GetFields()는Item1,Item2만 반환합니다. 동적 필드 이름이 중요하면readonly record struct로 승격합니다. - Unity 에서는 언어 버전을 먼저 확인합니다. 2022 LTS · 2023.x 는 C# 9 가 기본이라 이 기능을 쓸 수 없습니다. Unity 6 이상에서
<LangVersion>12</LangVersion>덮어쓴 뒤 IL2CPP 빌드까지 테스트합니다. - 판단 규칙: 이름만 필요한가 →
using별칭, 동작·API 계약이 필요한가 →readonly record struct.