[PART4.제어문: 분기와 반복(8/13)] 위치 패턴 — Deconstruct 기반 구조 분해 매칭 (C# 8)
요약 위치 패턴(positional pattern)은 객체를 Deconstruct 메서드로 분해한 다음, 꺼낸 값들을 위치 순서로 매칭하는 C# 8 기능입니다. 핵심은 point is (0, var y) 라고 쓰면 컴파일러가 자동으로 point.Deconstruct(out int x, out int y) 호출 코드를 끼워 넣고, 그 결과를 비교 사슬로 검사한다는 점입니다. 직전 글의 튜플 패턴이 이미 분해된 값을 매칭하는 문법이라면, 위치 패턴은 분해 자체를 매칭의 일부로 만드는 문법입니다. 동작은 단순하지만 함정이 네 가지 있습니다. ① Deconstruct 가 없는 타입에는 쓸 수 없습니다. ② record 와 record struct 는 자동으로 Deconstruct 를 만들어 주지만 매개변수 순서가 바뀌면 매칭 결과도 뒤집힙니다. ③ Deconstruct 안에 부수 효과(I/O, 카운터 증가)가 있으면 패턴이 매칭될 때마다 호출됩니다. ④ 클래스에 대해서는 컴파일러가 null 검사를 자동으로 끼워 넣어서, null 인 경우 패턴은 조용히 false 가 됩니다.
목차
1. 들어가며 — 왜 객체를 "꺼내서" 매칭하고 싶은가
직전 글에서 다룬 튜플 패턴은 두 변수의 조합을 한 번에 매칭하는 문법이었습니다. 그런데 실제 코드에서 다루는 데이터는 거의 항상 객체 입니다. Unity 게임에서도 (int x, int y) 같은 날 튜플보다는 Vector2, Vector3, 직접 만든 EnemyState 같은 타입을 다룹니다.
public record Point(int X, int Y);
static string ClassifyOld(Point p)
{
if (p.X == 0 && p.Y == 0) return "origin";
else if (p.X == 0 || p.Y == 0) return "on axis";
else if (p.X > 0 && p.Y > 0) return "Q1";
else return "other";
}
이 코드는 동작하지만 두 가지 문제가 있습니다.
p.X,p.Y가 4번씩 반복됩니다. 필드가 늘어나면 더 길어집니다.- 분기 조건이 "X 가 0 이고 Y 도 0 이면..." 이라는 절차적 표현인데, 실제 의도는 "(0, 0) 이면" 이라는 모양(shape) 매칭입니다.
C# 8 의 위치 패턴은 객체를 모양으로 매칭하는 문법입니다.
static string Classify(Point p) => p switch
{
(0, 0) => "origin",
(_, 0) or (0, _) => "on axis",
(> 0, > 0) => "Q1",
_ => "other"
};
같은 로직이 4 줄로 줄었습니다. 더 중요한 것은 p 가 객체임에도 불구하고 코드가 튜플을 다루는 것처럼 읽힌다는 점입니다. 이 변환을 가능하게 하는 것이 Deconstruct 메서드입니다.
이 글에서 다룰 것 / 다루지 않을 것
- 다룰 것: 위치 패턴 문법,
Deconstruct메서드 작성법,record자동 생성, IL 변환, struct vs class 차이, 함정.- 다루지 않을 것: 튜플 패턴 자체와 패턴 매칭의 기초 — 직전 글들에서 다뤘습니다. 이 글은 "객체를 분해해서 매칭한다" 는 한 가지 주제에 집중합니다.
2. 핵심 개념 — 튜플 패턴과 무엇이 다른가
위치 패턴과 튜플 패턴은 겉보기에 똑같이 (0, var y) 를 씁니다. 하지만 컴파일러가 다루는 대상이 완전히 다릅니다.

차이를 코드로 정리하면
// (1) 튜플 패턴 — 좌측이 ValueTuple<int,int> 일 때
(int, int) tuple = (0, 5);
if (tuple is (0, var y1)) Console.WriteLine($"y1={y1}");
// (2) 위치 패턴 — 좌측이 일반 객체이고 그 객체에 Deconstruct 가 있을 때
Point p = new Point(0, 5);
if (p is (0, var y2)) Console.WriteLine($"y2={y2}");
(1) 의 tuple 은 이미 두 값으로 나뉘어 있는 ValueTuple 구조체입니다. 매칭은 두 값을 그대로 비교하면 끝납니다. (2) 의 p 는 객체 하나입니다. 컴파일러가 매칭 직전에 p.Deconstruct(out int x_temp, out int y_temp) 를 호출해서 두 값을 꺼낸 다음, 그 결과를 비교합니다. 즉 위치 패턴은 "분해 + 비교" 가 한 줄에 묶인 문법입니다.
Deconstruct— 구조 분해 메서드 (deconstructor) 인스턴스 메서드 이름이 정확히Deconstruct이고 모든 매개변수가out으로 선언된 메서드. 컴파일러는 이 메서드를 보면 위치 패턴과 분해 대입(var (a, b) = obj) 양쪽에서 자동으로 호출합니다.
예시:public void Deconstruct(out int x, out int y) { x = X; y = Y; }이 메서드가 있으면obj is (0, var y)가 컴파일됩니다.
3. 한 줄짜리 비유 — 자판기 영수증 vs 자판기
영수증을 받았다면 거기에 "콜라 / 1500 원" 이라고 이미 두 줄로 나뉘어 적혀 있습니다. 보고 그대로 비교하면 됩니다 — 이게 튜플 패턴입니다.
자판기 본체가 있다면 안에 들어 있는 정보를 알기 위해 먼저 분해 버튼을 눌러야 합니다. 누르면 "이 안에는 콜라가 들어 있고 가격은 1500 원입니다" 라고 두 값을 출력합니다. 그다음 출력된 값을 비교합니다 — 이게 위치 패턴입니다.
C# 에서 그 분해 버튼이 바로 Deconstruct 메서드입니다. 컴파일러는 위치 패턴을 만나면 자동으로 이 버튼을 눌러 줍니다.
4. 기본 사용법 — Deconstruct 가 있어야 위치 패턴이 가능하다
가장 작은 예제부터 봅니다. Point 가 위치 패턴을 받을 수 있게 만들려면 두 가지 방법 중 하나를 씁니다.
4-1. 직접 Deconstruct 메서드 작성
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
// 위치 패턴은 이 메서드를 호출한다.
public void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
}
Point p = new Point(0, 5);
if (p is (0, var y)) Console.WriteLine($"x=0, y={y}"); // 출력: x=0, y=5
Deconstruct 의 규칙은 단순합니다.
- 이름은 정확히
Deconstruct - 반환 타입은
void - 모든 매개변수는
out - 매개변수 순서가 곧 위치 패턴의 슬롯 순서
4-2. record / record struct — 자동 생성
record 또는 record struct 를 위치 매개변수로 선언하면 컴파일러가 같은 모양의 Deconstruct 를 자동으로 만들어 줍니다.
public record Point(int X, int Y); // class 형 record
public record struct PointRS(int X, int Y); // value 형 record (C# 10+)
위 한 줄짜리 선언만으로 다음과 동일한 Deconstruct 가 생긴다는 사실을 IL 로 확인할 수 있습니다.
.method public hidebysig instance void Deconstruct (
[out] int32& X, [out] int32& Y
) cil managed
{
IL_0000: ldarg.1 // out 매개변수 X 의 주소를 평가 스택에 적재
IL_0001: ldarg.0
IL_0002: call instance int32 Point::get_X() // 자동 생성된 X 프로퍼티 게터 호출
IL_0007: stind.i4 // 해당 주소에 X 값 저장
IL_0008: ldarg.2 // out 매개변수 Y 의 주소
IL_0009: ldarg.0
IL_000a: call instance int32 Point::get_Y()
IL_000f: stind.i4
IL_0010: ret
}
복잡해 보이지만 본질은 한 줄짜리 C# 과 같습니다 — X = this.X; Y = this.Y;. 자동 생성되는 Deconstruct 는 부수 효과가 없는 단순 게터 호출의 나열 입니다. 그래서 record 위치 패턴은 매번 호출해도 비용이 작습니다.
4-3. point is (0, var y) vs point is Point(0, var y)
같은 위치 패턴이지만 앞에 타입 이름이 붙느냐로 의미가 살짝 달라집니다.
object obj = new Point(0, 5);
// (A) obj 의 컴파일 타임 타입은 object 라서 어떤 Deconstruct 를 부를지 모른다 → 컴파일 에러
// if (obj is (0, var y)) { ... }
// (B) Point 인지 확인 + Deconstruct 호출 + 비교가 한 번에 일어남
if (obj is Point(0, var y)) Console.WriteLine($"y={y}"); // 출력: y=5
규칙은 다음과 같이 정리할 수 있습니다.
| 형태 | 컴파일러가 보는 것 |
|---|---|
p is (0, var y) |
p 의 컴파일 타임 타입에서 Deconstruct 를 찾는다. 그 타입에 없으면 에러. |
p is Point(0, var y) |
먼저 p is Point 인지 검사하고, 그 다음에 Point.Deconstruct 를 호출. 다른 타입이면 매칭 실패. |
object 변수에 들어 있는 객체나 다형 변수처럼 런타임 타입을 함께 검사해야 하는 경우 에는 항상 타입 이름을 붙여야 합니다. 컴파일 타임에 타입이 이미 명확하다면 생략해도 됩니다.
5. 내부 동작 — IL 로 본 위치 패턴의 진짜 모습
위치 패턴이 실제로 어떻게 풀리는지 IL 로 확인합니다. 다음 C# 코드를 컴파일했습니다.
public record Point(int X, int Y); // 자동 Deconstruct
static string Classify(Point p) => p switch
{
(0, 0) => "origin",
(0, var y) => $"x=0, y={y}",
(var x, 0) => $"y=0, x={x}",
_ => "other"
};
Classify 함수의 핵심 IL 은 다음과 같습니다.
.method private hidebysig static string Classify(class Point p)
{
.locals init (
[0] int32, // V_0 — Deconstruct 의 두 번째 out (Y)
[1] int32, // V_1 — Deconstruct 의 첫 번째 out (X)
[2] string // V_2 — switch 의 결과
)
IL_0000: ldarg.0
IL_0001: brfalse.s IL_0070 // ① null 이면 default arm 으로 점프
IL_0003: ldarg.0
IL_0004: ldloca.s 1 // ② V_1 (X 받을 자리) 의 주소
IL_0006: ldloca.s 0 // ③ V_0 (Y 받을 자리) 의 주소
IL_0008: callvirt instance void Point::Deconstruct(int32&, int32&)
// ↑ class 이므로 callvirt — null 검사 + 가상 디스패치
IL_000d: ldloc.1 // ④ V_1 (X) 검사
IL_000e: brtrue.s IL_0015 // X != 0 이면 (var x, 0) arm 으로
IL_0010: ldloc.0 // ⑤ V_0 (Y) 검사
IL_0011: brfalse.s IL_001a // Y == 0 이면 "origin"
IL_0013: br.s IL_0022 // 그 외 → "x=0, y=..." arm
IL_0015: ldloc.0 // ⑥ X != 0 인 경로에서 Y 검사
IL_0016: brfalse.s IL_0049 // Y == 0 이면 (var x, 0) arm
IL_0018: br.s IL_0070 // 그 외 → default ("other")
IL_001a: ldstr "origin" // ⑦ "origin" 분기
IL_001f: stloc.2
IL_0020: br.s IL_0076
// ... (나머지 분기는 생략) ...
IL_0070: ldstr "other" // default arm
IL_0075: stloc.2
IL_0076: ldloc.2
IL_0077: ret
}
이 IL 에서 위치 패턴의 본질이 그대로 보입니다.
- ① null 검사:
brfalse.s한 줄로null처리됩니다.null이면Deconstruct호출은 건너뛰고 곧장 default arm 으로 갑니다. 이 검사는 컴파일러가 자동으로 끼워 넣은 것 입니다. - ② ~ ③ out 변수 준비:
ldloca.s로 지역변수의 주소를 평가 스택에 올립니다. C# 의out int x와 같은 의미입니다. - ③ Deconstruct 호출:
callvirt한 번만 호출됩니다. switch 의 모든 arm 이 이 한 번의 호출 결과를 공유 합니다 — arm 마다 매번 호출하지 않습니다. - ④ ~ ⑥ 비교 사슬: 꺼낸 X, Y 를
brtrue.s/brfalse.s로 비교하는 단순한 분기 트리입니다. 튜플 패턴과 본질이 같습니다.
위치 패턴의 컴파일러 변환 모델
위 IL 을 사람이 읽기 쉬운 C# 으로 풀어 쓰면 다음과 같습니다.
// switch 표현식이 컴파일러 머릿속에서 풀리는 모습
string Classify(Point p)
{
if (p == null) return "other";
int x_temp, y_temp;
p.Deconstruct(out x_temp, out y_temp);
if (x_temp == 0)
{
if (y_temp == 0) return "origin";
return $"x=0, y={y_temp}";
}
else
{
if (y_temp == 0) return $"y=0, x={x_temp}";
}
return "other";
}
위치 패턴은 마법이 아니라 "Deconstruct 한 번 호출 + if 사슬" 의 syntax sugar 입니다. 그래서 비용도 그것과 같습니다.
6. struct vs class — call 과 callvirt 의 차이
같은 위치 패턴이라도 대상이 struct 인지 class 인지에 따라 IL 이 달라집니다. 다음 두 함수를 컴파일해 비교했습니다.
public class PointCls { public int X; public int Y;
public PointCls(int x, int y){X=x;Y=y;}
public void Deconstruct(out int x, out int y){x=X;y=Y;} }
public struct PointSt { public int X; public int Y;
public PointSt(int x, int y){X=x;Y=y;}
public void Deconstruct(out int x, out int y){x=X;y=Y;} }
static string ClassifyClass (PointCls p) => p switch { (0, 0) => "origin", (0, var y) => $"x=0, y={y}", _ => "other" };
static string ClassifyStruct(PointSt p) => p switch { (0, 0) => "origin", (0, var y) => $"x=0, y={y}", _ => "other" };
class 버전 IL:
IL_0000: ldarg.0
IL_0001: brfalse.s IL_0042 // null 이면 default arm
IL_0003: ldarg.0
IL_0004: ldloca.s 2
IL_0006: ldloca.s 0
IL_0008: callvirt instance void PointCls::Deconstruct(int32&, int32&)
// ↑ callvirt — 가상 디스패치 + 호출 직전 null 재검사
struct 버전 IL:
IL_0000: ldarga.s p // p 의 주소 (struct 는 ref 로 호출)
IL_0002: ldloca.s 2
IL_0004: ldloca.s 0
IL_0006: call instance void PointSt::Deconstruct(int32&, int32&)
// ↑ call — 정적 디스패치, null 검사 없음, brfalse 도 없음
두 가지 차이가 있습니다.
callvscallvirt— IL 의 두 가지 메서드 호출 명령call은 컴파일 타임에 결정된 메서드를 그대로 호출합니다(정적 디스패치, null 검사 없음).callvirt는 객체의 실제 타입에서 메서드를 찾아 호출하며(가상 디스패치), 호출 직전에 객체가 null 이면NullReferenceException을 던집니다.
예시:obj.ToString()→ C# 컴파일러는 일반 클래스에는callvirt, 값 타입에는call을 씁니다. 위치 패턴의Deconstruct도 같은 규칙을 따릅니다.
| 항목 | class | struct |
|---|---|---|
| Deconstruct 호출 명령 | callvirt |
call |
| null 검사 | brfalse.s 로 자동 삽입 |
없음 (값 타입은 null 불가) |
| 메서드 디스패치 | 가상 (override 시 자식 메서드 호출) | 정적 (컴파일 타임 결정) |
| 비용 | 약간 높음 (vtable 조회) | 낮음 (직접 호출) |
핫패스에서 위치 패턴을 자주 쓴다면, 작은 데이터는 record struct 로 만드는 편이 IL 한 단계라도 줄어듭니다.
7. 사용자 정의 Deconstruct — 인스턴스 메서드 vs 확장 메서드
Deconstruct 는 확장 메서드 로도 작성할 수 있고, 위치 패턴에서 동작하는지 자주 헷갈리는 부분입니다. 직접 컴파일해 확인했습니다.
public class Foo
{
public int A; public int B;
public Foo(int a, int b) { A = a; B = b; }
// 인스턴스 Deconstruct 가 없음
}
public static class FooExt
{
public static void Deconstruct(this Foo f, out int a, out int b)
{
a = f.A; b = f.B;
}
}
class Program
{
static void Main()
{
var f = new Foo(1, 2);
var (x, y) = f; // 분해 대입 — 동작
if (f is (1, var b)) Console.Write(b); // 위치 패턴 — 동작?
}
}
dotnet build 결과 경고도 에러도 없이 빌드되며 실행 결과는 정상으로 매칭 됩니다. IL 을 디컴파일하면 다음과 같이 풀립니다.
// 컴파일러가 생성한 코드 (디컴파일)
Foo foo = new Foo(1, 2);
foo.Deconstruct(out var a, out var b); // 분해 대입
int value = a; int value2 = b;
Console.WriteLine($"{value},{value2}");
if (foo != null) // 위치 패턴의 자동 null 검사
{
foo.Deconstruct(out b, out var b2); // 확장 메서드를 호출!
if (b == 1) Console.Write(b2);
}
즉 현재 .NET (C# 9 이상)에서는 확장 메서드 Deconstruct 도 위치 패턴에서 정상 동작 합니다. 다만 두 가지 주의가 있습니다.
- 인스턴스 메서드가 우선 합니다. 같은 시그니처의 인스턴스 메서드와 확장 메서드가 모두 있으면 인스턴스 쪽이 호출됩니다 — C# 의 일반 멤버 접근 규칙과 동일합니다.
- 일부 자료(특히 C# 8 초기 글)는 "확장 메서드 Deconstruct 는 위치 패턴에서 동작하지 않는다" 고 적혀 있는데, 이는 초기 컴파일러 동작에 대한 설명입니다. 최신 컴파일러(Roslyn)에서는 정상 동작합니다.
확인 방법: 의심되면dotnet build후ilspycmd로 디컴파일해 보면 됩니다. 이 글의 검증도 그렇게 했습니다.
이 사실 덕분에 Unity 의 Vector3, Transform 같은 수정할 수 없는 외부 타입에도 위치 패턴을 적용 할 수 있습니다.
8. 실전 적용 — Unity 에서 위치 패턴
8-1. Vector3 분해로 축 상태 분류
Unity 의 Vector3 자체에는 Deconstruct 가 없지만 확장 메서드로 추가하면 됩니다.
// 어딘가의 정적 클래스에 한 번만 정의
public static class UnityDeconstructExtensions
{
public static void Deconstruct(this Vector3 v, out float x, out float y, out float z)
{
x = v.x; y = v.y; z = v.z;
}
}
// 사용처
public class PlayerMover : MonoBehaviour
{
void Update()
{
Vector3 v = rb.velocity;
// ❌ Before — if 사슬
string state;
if (v.x == 0 && v.y == 0 && v.z == 0) state = "Idle";
else if (v.y > 0 && v.x == 0 && v.z == 0) state = "PureJump";
else if (v.y < 0) state = "Falling";
else state = "Moving";
// ✅ After — 위치 패턴
string state2 = v switch
{
(0f, 0f, 0f) => "Idle",
(0f, > 0f, 0f) => "PureJump",
(_, < 0f, _) => "Falling",
_ => "Moving"
};
}
}
Vector3.x, .y, .z 가 6 번 반복되던 코드가 4 줄짜리 표로 바뀝니다. 컴파일러는 이 코드를 위 5 절에서 본 IL 과 동일한 패턴으로 풀어내므로 switch 안에서 Deconstruct 는 한 번만 호출됩니다.
8-2. ❌ 부수 효과를 가진 Deconstruct
다음은 신입이 자주 저지르는 실수입니다.
// ❌ Bad — Deconstruct 안에서 카운터 증가
public class Counter
{
public int X; public int Y; public int CallCount;
public void Deconstruct(out int x, out int y)
{
CallCount++; // 부수 효과!
x = X; y = Y;
}
}
var c = new Counter { X = 0, Y = 5 };
string r = c switch
{
(0, 0) => "origin",
(0, var y) => $"y={y}",
_ => "other"
};
// c.CallCount == 1
CallCount 가 1 인 것은 정상입니다 — 5 절 IL 에서 본 것처럼 switch 표현식 안에서 Deconstruct 는 한 번만 호출되기 때문입니다. 그러나 같은 객체로 매칭이 자주 일어나는 코드 (이벤트 핸들러, Update 안의 분기 등)에서는 호출 횟수가 누적됩니다.
// ✅ Good — Deconstruct 는 단순 게터 나열로 둔다
public class Counter
{
public int X; public int Y;
// CallCount 같은 부수 효과는 별도 메서드로 분리
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
원칙: Deconstruct 는 항상 순수 함수(게터 나열)로 작성합니다. 부수 효과는 절대 넣지 않습니다.
8-3. record 자동 Deconstruct 와 매개변수 순서
public record EnemyState(int Hp, int Mp); // (Hp, Mp) 순서
string status = enemy switch
{
(0, _) => "Dead",
(_, 0) => "OutOfMana",
(< 30, _) => "LowHp",
_ => "Healthy"
};
이 코드는 잘 동작합니다. 하지만 누군가가 record 정의를 record EnemyState(int Mp, int Hp) 로 바꾸면, 위 switch 의 의미가 모두 뒤집힙니다 — 컴파일은 그대로 통과합니다. 이 함정은 7 절에서 다시 다룹니다.
9. 함정과 주의사항
9-1. ❌ Deconstruct 가 없는 타입에는 못 쓴다
public class Point2 { public int X; public int Y; } // Deconstruct 없음
Point2 p = new Point2 { X = 0, Y = 5 };
// ❌ 컴파일 에러 CS1061 / CS8129
// if (p is (0, var y)) Console.WriteLine(y);
에러 메시지는 다음과 같습니다.
error CS8129: No suitable 'Deconstruct' instance or extension method was found
for type 'Point2', with 2 out parameters and a void return type.
해결법은 두 가지입니다.
// ✅ (A) Deconstruct 인스턴스 메서드 추가
public class Point2
{
public int X; public int Y;
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
// ✅ (B) 확장 메서드로 추가 — 외부 타입에 유용
public static class Point2Ext
{
public static void Deconstruct(this Point2 p, out int x, out int y)
{ x = p.X; y = p.Y; }
}
9-2. ❌ record 매개변수 순서 변경
// 원래 정의
public record Point(int X, int Y);
string s = p switch
{
(0, var y) => $"x=0, y={y}", // 1번째가 0, 2번째 그대로
(var x, 0) => $"y=0, x={x}",
_ => "other"
};
// ❌ 이후 누군가가 record 를 이렇게 바꿈
public record Point(int Y, int X); // 매개변수 순서를 뒤집음
// switch 는 그대로지만 의미가 완전히 바뀜:
// (0, var y) 는 이제 "Y == 0 이고 X 를 y 라는 이름에 담는다" 가 됨!
위치 패턴은 이름이 아니라 순서로 매칭 합니다. record 의 위치 매개변수 순서는 공개 API 의 일부로 다뤄야 합니다. 이름으로 매칭하고 싶다면 속성 패턴(앞 글에서 다룸) 을 써야 합니다.
// ✅ 순서가 바뀌어도 안전
string s = p switch
{
{ X: 0, Y: var y } => $"x=0, y={y}",
{ X: var x, Y: 0 } => $"y=0, x={x}",
_ => "other"
};
9-3. ❌ Deconstruct 안의 부수 효과
8-2 절에서 본 것과 같은 문제입니다. 한 번 더 정리하면:
// ❌ Bad — 로깅을 Deconstruct 에 넣음
public void Deconstruct(out int x, out int y)
{
Debug.Log($"Deconstruct called on {this}"); // 매 매칭마다 로그가 찍힘
x = X; y = Y;
}
위치 패턴이 들어 있는 함수가 자주 호출되면 로그가 폭주합니다. Deconstruct 는 절대 로깅·I/O·상태 변경을 하지 않는다 는 규약을 팀 전체에서 유지해야 합니다.
9-4. null 자동 검사 — 의도와 다를 수 있음
5 절 IL 에서 본 것처럼 class 위치 패턴은 자동으로 null 검사를 끼워 넣어서, null 이면 패턴이 조용히 false 가 됩니다.
Point p = null;
string r = p switch
{
(0, 0) => "origin",
(0, var y) => $"y={y}",
_ => "other"
};
// r == "other" — NullReferenceException 이 아니라 default arm 으로 떨어짐
이 동작은 안전한 기본값이지만, 때로 null 이 진짜 버그인데 default arm 으로 가려져서 안 보이는 상황 이 생깁니다. null 을 명시적으로 다루고 싶다면:
// ✅ null 패턴을 명시적으로 추가
string r = p switch
{
null => throw new ArgumentNullException(nameof(p)),
(0, 0) => "origin",
(0, var y) => $"y={y}",
_ => "other"
};
10. C# 버전별 변화
| 버전 | 변화 |
|---|---|
| C# 7.0 | Deconstruct 메서드와 분해 대입(var (a, b) = obj) 도입. 단, 패턴 매칭에서는 사용 불가. |
| C# 8.0 | 위치 패턴 도입. obj is (...) 와 switch 표현식에서 Deconstruct 를 호출하는 매칭 가능. |
| C# 9.0 | record 도입 — 위치 매개변수가 있으면 Deconstruct 자동 생성. 위치 패턴이 사실상 표준 도구가 됨. |
| C# 10.0 | record struct 도입 — 값 타입 record 도 자동 Deconstruct. struct 라서 call 사용으로 비용 더 낮음. |
C# 7 분해 대입 vs C# 8 위치 패턴 — 무엇이 새로운가
// C# 7 — 분해 대입 (Deconstruct 호출은 같지만 매칭은 없음)
var (x, y) = point;
if (x == 0 && y == 0) Console.WriteLine("origin");
// C# 8 — 위치 패턴 (Deconstruct 호출 + 비교가 한 줄에)
if (point is (0, 0)) Console.WriteLine("origin");
두 코드는 동일한 Deconstruct 를 호출합니다. 차이는 호출 결과를 어떻게 사용하느냐 입니다. 분해 대입은 결과를 변수에 담을 뿐, 매칭은 따로 해야 합니다. 위치 패턴은 결과를 곧장 비교 합니다.
C# 7 의 Deconstruct 가 없었다면 C# 8 의 위치 패턴도 없었을 것입니다. 같은 메커니즘 위에 매칭이 한 층 더 얹힌 셈입니다.
11. 정리
위치 패턴을 한 문장으로 요약하면 "객체에 Deconstruct 가 있으면, 그 결과를 튜플처럼 매칭할 수 있다" 입니다. 외워야 할 것은 다음 다섯 가지뿐입니다.
- [ ] 위치 패턴 = Deconstruct 호출 + 비교 사슬. 컴파일러가
obj.Deconstruct(out _, out _)를 자동으로 끼워 넣고 결과를 비교합니다. switch 안에서는 한 번만 호출됩니다. - [ ] 튜플 패턴은 이미 분해된 값, 위치 패턴은 객체.
(int, int)변수에는 튜플 패턴, 객체 변수에는 위치 패턴. - [ ]
record/record struct는 자동 Deconstruct. 단, 매개변수 순서 가 매칭에 그대로 반영되므로 순서 변경 = 의미 변경입니다. - [ ] class 는
callvirt+ 자동 null 검사, struct 는call. struct 가 IL 한 단계 짧고 null 검사도 없습니다. 핫패스에서는record struct가 유리합니다. - [ ] Deconstruct 는 순수 함수로. 로깅·I/O·상태 변경 금지. 확장 메서드로 작성해도 위치 패턴에서 정상 동작하므로 외부 타입(Vector3 등)에도 자유롭게 적용할 수 있습니다.
위치 패턴은 패턴 매칭 시리즈의 마지막 큰 조각입니다. 다음 글부터는 분기에서 반복으로 시선을 옮겨 for, foreach, while 같은 반복문의 내부 동작을 봅니다.
'C# 기초' 카테고리의 다른 글
| [PART4.제어문: 분기와 반복(10/13)] foreach — 컬렉션 순회의 기본 (0) | 2026.04.26 |
|---|---|
| [PART4.제어문: 분기와 반복(9/13)] for 반복문 — 인덱스를 직접 다루는 반복 (0) | 2026.04.26 |
| [PART4.제어문: 분기와 반복(7/13)] 튜플 패턴 — 여러 값을 동시에 매칭 (C# 8) (0) | 2026.04.26 |
| [PART4.제어문: 분기와 반복(6/13)] 속성 패턴 — 객체의 프로퍼티 값으로 분기 (C# 8) · 확장 속성 패턴 (C# 10) (0) | 2026.04.26 |
| [PART4.제어문: 분기와 반복(5/13)] 리스트 패턴 — 배열·리스트의 모양으로 분기 (C# 11) (0) | 2026.04.26 |