[PART3.연산자와 표현식(6/11)] 삼항(조건) 연산자 — 조건 ? A : B
값을 반환하는 if-else / 표현식 vs 문의 경계 / 중첩의 가독성 함정 / Target-typed (C# 9) / switch 표현식과의 분업 / IL brtrue.s로 본 내부 동작
목차
1. 문제 제기 — "한 줄짜리 if-else"가 정말 필요한가
Unity 신입 개발자가 플레이어 이동 스크립트를 처음 만들면, 쉬프트 키를 누를 때 속도를 두 배로 올리고 싶다는 요구가 거의 100% 나옵니다. 머릿속에 떠오르는 첫 번째 코드는 보통 이렇습니다.
float speed;
if (Input.GetKey(KeyCode.LeftShift))
{
speed = 10f;
}
else
{
speed = 5f;
}
transform.position += transform.forward * speed * Time.deltaTime;
동작에는 문제가 없습니다. 하지만 한 가지 값을 고르기 위해 6줄을 쓰는 건 과하게 느껴집니다. 같은 파일에 비슷한 분기가 열 개쯤 생기면 Update() 메서드가 금세 화면을 넘어갑니다.
C#은 이런 상황을 위해 값을 직접 반환하는 한 줄짜리 분기 문법을 준비해 두었습니다. 바로 삼항 조건 연산자 ?:입니다.
float speed = Input.GetKey(KeyCode.LeftShift) ? 10f : 5f;
6줄이 1줄로 줄었고, 변수 선언과 값 결정이 한 번에 끝났습니다. 하지만 이 간결함에는 대가가 있습니다. 중첩하면 가독성이 급락하고, 양쪽 분기 타입이 어긋나면 "best common type" 오류가 쏟아지고, C# 9 이전에는 null과 함께 쓰는 순간 컴파일이 안 됐습니다.
이 글은 언제 ?:를 쓰고 언제 피해야 하는지를 이해 가능한 규칙으로 풀어놓는 것이 목표입니다. 문법의 핵심은 "값을 내뿜는 표현식"이라는 한마디에 모두 들어 있습니다. 이 한마디가 왜 이토록 중요한지부터 시작하겠습니다.
2. 개념 정의 — 삼항은 "값을 내는 표현식"이다
비유: if-else는 "지시문", 삼항은 "답안지"
if-else와 ?:의 관계는 회의실에서 "A안으로 갈지 B안으로 갈지 판단하고 실행해"라는 지시문(문, statement)과 "A안입니다"라고 적힌 답안지(표현식, expression)의 차이와 같습니다. 지시문은 뭔가를 하게 만들지만 그 자체가 결과가 되진 못합니다. 답안지는 그 자체가 하나의 값이라 다른 자리에 그대로 끼워 넣을 수 있습니다.
?:— 조건(삼항) 연산자 (Conditional / Ternary operator) 조건이 true면 첫 번째 값, false면 두 번째 값을 반환하는 표현식. C#에서 유일하게 피연산자가 3개인 연산자라서 "삼항"이라는 별명이 붙었다.
예시:int max = a > b ? a : b;a와 b 중 큰 값을 max에 대입
구조 시각화

조건 한 개가 두 갈래 중 하나만 실행하고, 그 결과가 같은 자리의 값으로 합류합니다. 주황색 "하나의 결과값"이 표현식의 본질입니다 — 값이 나오기 때문에 이걸 변수에 대입할 수도, 메서드 인자로 넘길 수도, return 뒤에 바로 쓸 수도 있습니다.
기본 C# 코드
public static int TernaryBasic(bool cond)
{
return cond ? 10 : 20;
}
Unity에서는 이런 상황이 생깁니다 — "조건에 따라 값 하나만 고르고 싶은데, 별도 변수나 if 블록을 쓰기는 아깝다."
IL 분석
실제 C# 컴파일러가 위 메서드를 어떻게 번역하는지 확인합니다. IL(Intermediate Language, .NET이 실행 전에 거치는 중간 언어)은 CPU가 직접 실행하지는 않지만, 컴파일러가 C# 코드를 어떻게 이해했는지를 가장 정확히 보여주는 "설계도"입니다.
.method public hidebysig static
int32 TernaryBasic (
bool cond
) cil managed
{
.maxstack 1
.locals init (
[0] int32 // 결과를 담는 지역 변수 1개
)
IL_0000: nop
IL_0001: ldarg.0 // cond 값을 스택에 로드
IL_0002: brtrue.s IL_0008 // cond == true 면 IL_0008 (true 분기)로 점프
IL_0004: ldc.i4.s 20 // false 분기: 20을 스택에
IL_0006: br.s IL_000a // 결과 합류 지점(IL_000a)으로 무조건 점프
IL_0008: ldc.i4.s 10 // true 분기: 10을 스택에
// (이어서 fall-through로 IL_000a로 감)
IL_000a: stloc.0 // 합류: 스택 top을 지역변수[0]에 저장
IL_000b: br.s IL_000d
IL_000d: ldloc.0 // 결과 값을 스택에
IL_000e: ret // 반환
}
세 가지만 봐 두시면 됩니다.
brtrue.s IL_0008— 조건이 true면 true 분기 위치로 점프. "삼항이 어떻게 분기되는가"의 실제 답.- 한쪽만 실행 —
brtrue.s로 점프하면ldc.i4.s 20(false 분기)은 건너뜁니다. 점프하지 않으면 바로 false 분기 코드가 실행되고br.s IL_000a로 true 쪽을 뛰어넘어갑니다. 한 방향만 실행되는 것이 단락 평가(뒤에서 다룸)의 IL 레벨 증거입니다. - 같은 스택 슬롯으로 합류 — true 분기든 false 분기든 모두
IL_000a: stloc.0에 도착합니다. 두 분기의 결과 타입이 같아야 한다는 제약(CS0173)이 여기서 나옵니다. 서로 다른 타입이면stloc.0에 뭘 저장해야 할지 정할 수 없습니다.
3. 내부 동작 — 삼항과 if-else는 IL에서 얼마나 다른가
결론부터: 거의 차이가 없습니다. 하지만 "거의"라는 단서를 이해해 두면 추후 성능 걱정을 이유 없이 하지 않을 수 있습니다.
if-else와 삼항의 IL 비교
// 삼항 버전
public static int TernaryBasic(bool cond) => cond ? 10 : 20;
// if-else 버전
public static int IfElseBasic(bool cond)
{
if (cond) return 10;
else return 20;
}
두 메서드의 IL을 나란히 놓으면 차이가 눈에 들어옵니다.
// === TernaryBasic ===
.locals init ( [0] int32 ) // 지역변수 1개
IL_0001: ldarg.0
IL_0002: brtrue.s IL_0008 // true 분기로 점프
IL_0004: ldc.i4.s 20
IL_0006: br.s IL_000a
IL_0008: ldc.i4.s 10
IL_000a: stloc.0
IL_000d: ldloc.0
IL_000e: ret
// === IfElseBasic ===
.locals init ( [0] bool, [1] int32 ) // 지역변수 2개 (bool이 하나 더)
IL_0001: ldarg.0
IL_0002: stloc.0 // cond를 지역변수에 저장
IL_0003: ldloc.0 // 다시 로드
IL_0004: brfalse.s IL_000c // false 분기로 점프
IL_0006: nop // Debug 빌드의 중단점용 no-op
IL_0007: ldc.i4.s 10
IL_0009: stloc.1
IL_000a: br.s IL_0012
IL_000c: nop
IL_000d: ldc.i4.s 20
IL_000f: stloc.1
IL_0010: br.s IL_0012
IL_0012: ldloc.1
IL_0013: ret
Debug 빌드 기준이라nop과 여분의stloc/ldloc이 보입니다. Release 빌드에서는 JIT 최적화로 이런 잡동사니가 대부분 사라지고 두 버전이 거의 같은 기계어로 수렴합니다.
관찰할 점:
- 분기 명령어 방향이 반대일 뿐 — 삼항은
brtrue.s(true면 점프), if-else는brfalse.s(false면 점프)를 썼습니다. 컴파일러가 "본문 → else" 순으로 코드를 깐 탓이지 본질적인 차이는 아닙니다. - 지역 변수 수가 다름 — if-else 쪽이
bool슬롯을 하나 더 씁니다. 이 역시 Debug 빌드의 부산물이고 Release에서는 사라집니다. - 두 버전 모두 "같은 스택 슬롯으로 합류" — 삼항은
stloc.0, if-else는stloc.1로, 두 분기가 마지막에 한 슬롯에 값을 저장합니다.
성능상 차이는 무시해도 되는 수준입니다. 성능이 아니라 가독성이 선택 기준입니다.
단락 평가 — IL로 본 "한쪽만 실행"
삼항은 &&/||처럼 선택된 분기만 평가합니다. 부수 효과가 있는 호출도 한쪽만 실행됩니다.
public static int TernaryShortCircuit(bool cond)
{
return cond ? GetA() : GetB();
}
public static int GetA() => 1;
public static int GetB() => 2;
Unity에서는 "계산 비용이 높은 쪽을 조건부로만 실행하고 싶다"는 상황이 흔합니다. 예를 들어 FindObjectOfType<T>()처럼 무거운 호출.
.method public hidebysig static
int32 TernaryShortCircuit (bool cond) cil managed
{
IL_0001: ldarg.0
IL_0002: brtrue.s IL_000b
IL_0004: call int32 Program::GetB() // false 분기: GetB()만 call
IL_0009: br.s IL_0010
IL_000b: call int32 Program::GetA() // true 분기: GetA()만 call
IL_0010: stloc.0
IL_0013: ldloc.0
IL_0014: ret
}
call GetA()와 call GetB()는 각각 다른 경로에 놓여 있습니다. true면 IL_000b로 점프해서 GetB를 건너뛰고, false면 GetB를 실행한 뒤 br.s IL_0010으로 GetA를 건너뜁니다. 두 호출이 동시에 일어나는 경로는 존재하지 않습니다. 이것이 단락 평가가 언어 차원의 "규칙"이 아니라 IL 수준의 구조적 보장이라는 증거입니다.
이 보장이 있기 때문에 아래 패턴이 안전합니다:
// enemy가 null이어도 NullReferenceException이 나지 않음
int hp = enemy != null ? enemy.Hp : 0;
enemy가 null일 때 enemy.Hp는 평가조차 되지 않습니다. 실제로 뒤 섹션의 GetEnemyHp IL에서 이를 다시 확인합니다.
4. 실전 적용 — 언제 쓰고 언제 빼는가
4-1. 쓰면 좋은 자리 — 값이 필요한 위치
삼항이 진짜 힘을 내는 곳은 "값이 필요한 자리"입니다. 이런 자리에는 if-else가 들어갈 수 없거나 매우 장황해집니다.
Unity에서 이런 상황이 자주 나옵니다 — 인자로 값을 전달하거나, LINQ 안에서 분기하고 싶을 때.
// Before: 분기 때문에 임시 변수 + if 블록이 생김
float volume;
if (gateOpen)
volume = maxVolume;
else
volume = 0f;
audioSource.volume = volume;
// After: 표현식이라 대입 오른쪽에 그대로
audioSource.volume = gateOpen ? maxVolume : 0f;
LINQ 프로젝션에서도 자연스럽습니다.
// 각 적의 HP를 상태에 따라 두 배로 계산
var displayHps = enemies.Select(e => e.IsElite ? e.Hp * 2 : e.Hp).ToList();
Before/After 쌍 — 속도 곱셈
// Before — if-else로 변수에 담고 사용
public float ComputeSpeedBefore(bool isSprinting, float runSpeed, float walkSpeed)
{
float speed;
if (isSprinting)
speed = runSpeed;
else
speed = walkSpeed;
return speed;
}
// After — 삼항으로 한 줄
public float ComputeSpeedAfter(bool isSprinting, float runSpeed, float walkSpeed)
{
return isSprinting ? runSpeed : walkSpeed;
}
After 버전의 IL은 핵심이 13바이트밖에 안 됩니다.
.method public hidebysig static
float32 ComputeSpeed (
bool isSprinting, float32 runSpeed, float32 walkSpeed
) cil managed
{
IL_0001: ldarg.0 // isSprinting 로드
IL_0002: brtrue.s IL_0007 // true면 runSpeed 분기
IL_0004: ldarg.2 // false: walkSpeed
IL_0005: br.s IL_0008
IL_0007: ldarg.1 // true: runSpeed
IL_0008: stloc.0
IL_000b: ldloc.0
IL_000c: ret
}
ldarg.1(runSpeed)과 ldarg.2(walkSpeed) 둘 중 하나만 스택에 올립니다. 값 계산이 아닌 단순 선택이라, Release 빌드의 JIT은 이걸 종종 분기 없는 조건부 이동(cmov) 한 줄로 합쳐버립니다. Unity 핫패스(Update, FixedUpdate)에서 부담 없이 써도 됩니다.
4-2. null 안전 접근 — 삼항의 고전적 활용
public static int GetEnemyHp(Enemy enemy)
{
return enemy != null ? enemy.Hp : 0;
}
이 패턴은 null 참조 예외를 피하는 Unity 관용구입니다. 실제 IL이 "enemy가 null이면 Hp 접근을 완전히 건너뛴다"는 것을 보여줍니다.
.method public hidebysig static
int32 GetEnemyHp (class Enemy enemy) cil managed
{
IL_0001: ldarg.0
IL_0002: brtrue.s IL_0007 // enemy != null 이면 IL_0007로
IL_0004: ldc.i4.0 // null: 0만 로드하고 끝
IL_0005: br.s IL_000d
IL_0007: ldarg.0 // null 아님: enemy 로드
IL_0008: callvirt instance int32 Enemy::get_Hp() // Hp getter 호출
IL_000d: stloc.0
IL_0010: ldloc.0
IL_0011: ret
}
null일 때 실행되는 명령은 ldc.i4.0(0을 로드)뿐입니다. callvirt는 아예 도달하지 않습니다. 이게 조건 연산자의 단락 평가가 런타임 예외를 막아주는 원리입니다.
주의 — UnityUnityEngine.Object의 null 체크 Unity가 제공하는GameObject,MonoBehaviour,Component같은 타입은==연산자가 오버로딩되어 있어, "파괴된(destroyed) 객체"도 null로 취급합니다. 이 동작은 편리하지만 일반 C# 참조 비교보다 조금 더 느립니다. 반복 호출되는 핫패스에서는 캐싱한 결과를 재사용하거나?.(null 조건 접근) 사용을 검토해야 할 수 있습니다.
4-3. UI 토글
healthText.color = hp > 30 ? Color.white : Color.red;
healthText.text = hp > 0 ? $"HP {hp}" : "<color=red>DEAD</color>";
두 줄 모두 대입 오른쪽에 삼항이 쓰였습니다. 분기에 필요한 건 값뿐이고 동작이 아니므로 삼항이 가장 깔끔합니다.
5. 함정과 주의사항
함정 1 — 중첩 삼항의 가독성
?:의 오른쪽 결합성a ? b : c ? d : e는 컴파일러가a ? b : (c ? d : e)로 해석한다. 왼쪽부터가 아니라 오른쪽부터 묶인다는 뜻.
Unity에서 이런 상황이 나옵니다 — 점수 구간별로 등급을 매기거나, 상태 머신이 3개 이상의 상태로 분기할 때.
// ❌ 한 줄로 욱여넣은 중첩 삼항 — 읽기 어렵고 유지보수 지옥
public string GetGrade(int score)
{
return score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
}
들여쓰기로 "사다리 형태"를 만들면 조금 낫습니다:
// ⚠️ 사다리 형태 — 읽히긴 하지만 조건이 늘어나면 한계
public string GetGrade(int score)
{
return score >= 90 ? "A"
: score >= 80 ? "B"
: score >= 70 ? "C"
: "F";
}
IL을 보면 컴파일러 입장에서는 중첩이 곧 여러 번의 분기입니다.
.method public hidebysig static
string GetGrade (int32 score) cil managed
{
IL_0001: ldarg.0
IL_0002: ldc.i4.s 90
IL_0004: bge.s IL_0025 // score >= 90 이면 "A"로
IL_0006: ldarg.0
IL_0007: ldc.i4.s 80
IL_0009: bge.s IL_001e // score >= 80 이면 "B"로
IL_000b: ldarg.0
IL_000c: ldc.i4.s 70
IL_000e: bge.s IL_0017 // score >= 70 이면 "C"로
IL_0010: ldstr "F" // 모두 false: "F"
IL_0015: br.s IL_001c
IL_0017: ldstr "C"
IL_001c: br.s IL_0023
IL_001e: ldstr "B"
IL_0023: br.s IL_002a
IL_0025: ldstr "A"
IL_002a: stloc.0
IL_002d: ldloc.0
IL_002e: ret
}
bge.s(같거나 크면 분기)가 3번 연달아 나오고, 각 분기가 서로 다른 문자열 리터럴로 흘러간 뒤 공통 합류 지점(stloc.0)에 도달합니다. 기능상 문제는 없지만, 나중에 "80점 이상 B" 구간을 "85점 이상 B"로 바꾸려면 어느 삼항을 건드려야 할지 매번 눈으로 재검색해야 합니다.
// ✅ switch 표현식 (C# 8+)으로 교체 — 한눈에 읽힘
public string GetGrade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
_ => "F"
};
switch 표현식의 핵심은 "왼쪽 열이 조건, 오른쪽 열이 값"이라는 표 구조입니다. 조건이 늘어도 행만 추가되므로 시각적 부담이 일정합니다.
규칙: 분기가 2갈래를 넘어가면 switch 표현식을 우선 고려하세요. 삼항을 중첩해 3갈래 이상을 처리하는 건 거의 항상 나쁜 선택입니다.
함정 2 — 타입 규칙 CS0173
// ❌ int와 string은 공통 타입이 없음
var result = cond ? 10 : "Hello";
// CS0173: Type of conditional expression cannot be determined because
// there is no implicit conversion between 'int' and 'string'
// ❌ 형제 클래스 간에도 공통 타입을 못 찾음 (C# 8까지)
class Animal { }
class Dog : Animal { }
class Cat : Animal { }
Animal a = cond ? new Dog() : new Cat(); // C# 8까지 CS0173
컴파일러는 두 분기를 각각 독립적으로 판별한 뒤, "한쪽에서 다른 쪽으로 암시적 변환이 되는가?"만 확인합니다. Dog ↔ Cat은 서로 변환 불가이므로 공통 부모가 있어도 못 찾습니다.
// ✅ 한쪽을 명시적으로 부모 타입으로 캐스트
Animal a = cond ? (Animal)new Dog() : new Cat();
// ✅ 양쪽 타입이 같은 경우는 문제 없음
double x = cond ? 1 : 2.0; // int → double 암시적 변환 존재 → OK
함정 3 — C# 9 이전: null과의 조합
C# 8까지는 "한쪽이 null 리터럴"인 조건식이 바로 오류였습니다.
// ❌ C# 8까지는 CS0173
int? x = cond ? 10 : null;
이유: 10의 타입은 int, null은 "타입 없음"이라 둘의 공통 타입을 찾지 못합니다. 대입 대상이 int?인 건 컴파일러가 고려하지 않았습니다.
C# 9부터 target-typed conditional expression이 도입되어 해결됐습니다(다음 섹션 참조).
함정 4 — ?:와 ??를 혼동하지 않기
??— 널 병합 연산자 (Null-coalescing operator) 왼쪽이 null이 아니면 그 값, null이면 오른쪽 값을 반환한다. "null일 때 기본값"이라는 단 한 가지 경우에만 특화된 연산자.
예시:string name = input ?? "Guest";input이 null이면 "Guest"
// ❌ 장황한 삼항 (좌변 name이 두 번 등장)
string displayName = name == null ? "Guest" : name;
// ✅ 같은 의미를 ??로
string displayName = name ?? "Guest";
a ?? b는 IL 레벨에서도 a != null ? a : b와 동등합니다. 문법만 짧아지는 게 아니라 의도가 명확해집니다. "null이냐 아니냐만 확인하고 싶다"는 게 코드로 바로 드러납니다.
반대 방향도 마찬가지로 혼동하지 말아야 합니다.
// ❌ "빈 문자열도 기본값으로"를 ??로 하려고 하면 틀림
string display = name ?? "Guest"; // name == "" 이면 ""가 그대로 대입됨
// ✅ 일반 조건이 필요하므로 삼항
string display = string.IsNullOrEmpty(name) ? "Guest" : name;
경계선: "null이냐만 본다" → ??. "null 이외의 일반 조건" → ?:.
6. C# 버전별 변화
C# 1.0 — 기본 삼항 연산자 도입
int max = a > b ? a : b;
C 언어의 ?:를 거의 그대로 계승. 이 시점부터 "양쪽 타입의 best common type"이라는 규칙이 확립됩니다.
C# 8 (2019) — switch 표현식 등장으로 "삼항의 역할 분담"
// C# 8 이전: 3갈래 분기는 중첩 삼항이 거의 유일
string tier = rating >= 90 ? "S" : rating >= 70 ? "A" : rating >= 50 ? "B" : "C";
// C# 8+: switch 표현식이 "다갈래 값 분기"를 흡수
string tier = rating switch
{
>= 90 => "S",
>= 70 => "A",
>= 50 => "B",
_ => "C"
};
이 시점부터 2갈래는 삼항, 3갈래 이상은 switch 표현식이라는 업계 관행이 자리 잡았습니다.
C# 9 (2020) — Target-typed 조건 표현식
C# 9는 조건식에서 가장 아프던 두 가지 실용 문제를 해결했습니다:
Before — C# 8까지 (컴파일 오류)
int? x = cond ? 10 : null;
// CS0173: between 'int' and '<null>'
ReadOnlySpan<byte> data = cond ? arr1 : arr2;
// byte[] 간 공통 타입은 byte[]이지만,
// byte[] → ReadOnlySpan<byte> 변환은 best common type 단계에서 고려되지 않음
Animal a = cond ? new Dog() : new Cat();
// CS0173: between 'Dog' and 'Cat'
After — C# 9+ (전부 OK)
// 모두 C# 9부터 컴파일 통과
int? x = cond ? 10 : null;
ReadOnlySpan<byte> data = cond ? arr1 : arr2;
Animal a = cond ? new Dog() : new Cat();
규칙 차이: C# 8까지는 "두 분기의 공통 타입" → "대입 대상 타입으로 변환" 순서였지만, C# 9부터는 "대입 대상 타입이 먼저 보이면, 각 분기를 그 타입으로 변환 가능한지 확인"이 추가됐습니다.
Target-typed 조건식의 실제 IL을 보면, 컴파일러가 각 분기를 독립적으로 대상 타입(int?)으로 변환해 둔다는 걸 확인할 수 있습니다.
public static int? TargetTypedNullable(bool cond)
{
int? x = cond ? 10 : null;
return x;
}
.method public hidebysig static
valuetype [System.Runtime]System.Nullable`1<int32> TargetTypedNullable (
bool cond
) cil managed
{
.locals init (
[0] valuetype System.Nullable`1<int32>,
[1] valuetype System.Nullable`1<int32>,
[2] valuetype System.Nullable`1<int32>
)
IL_0001: ldarg.0
IL_0002: brtrue.s IL_000f // true면 true 분기로
// ------- false 분기: null을 int? 로 만들어 내기 -------
IL_0004: ldloca.s 1
IL_0006: initobj valuetype System.Nullable`1<int32> // Nullable<int>의 0-초기화(= HasValue=false)
IL_000c: ldloc.1
IL_000d: br.s IL_0016
// ------- true 분기: 10을 int? 로 포장 -------
IL_000f: ldc.i4.s 10
IL_0011: newobj instance void valuetype System.Nullable`1<int32>::.ctor(!0)
IL_0016: stloc.0 // 결과 저장
IL_0017: ldloc.0
IL_001c: ret
}
핵심 관찰:
- false 분기 —
initobj System.Nullable<int32>로 "값 없음" 상태의Nullable<int>를 만들어 스택에 올립니다.null리터럴이int?에 맞춰 자동으로 변환된 것입니다. - true 분기 —
newobj Nullable<int>::.ctor(10)으로 "값 있음" 상태의Nullable<int>를 만듭니다.int값10이int?로 승격된 것이죠. - 양쪽 결과 모두
System.Nullable<int32>타입 — 스택에서 합류하고stloc.0에 저장됩니다.
변환을 컴파일러가 각 분기에 개별적으로 적용해 줬다는 점이 target-typed 조건식의 실체입니다. 이전 버전에서는 두 분기의 공통 타입을 먼저 뽑으려다 실패했습니다.
주의 —var에는 target type이 없다 ``csharp var y = cond ? 10 : null; // ❌ 여전히 CS0173`var`는 "오른쪽 표현식의 타입으로 변수를 만든다"는 의미라 대상 타입이 없습니다. target-typed 조건식은 대상 타입이 미리 정해진 자리(대입 좌변, 반환 타입, 메서드 인자 등)에서만 동작합니다.
7. 정리 — 이것만 기억하세요
- [ ] 삼항은 표현식이다. 값이 필요한 자리(대입 오른쪽,
return, 메서드 인자, LINQ)에 바로 꽂아 넣을 수 있다. 이게 if-else와의 본질적 차이. - [ ] 성능은 if-else와 사실상 동일하다. IL에서
brtrue.s/brfalse.s로 내려가고 같은 스택 슬롯으로 합류. Release 빌드에서는 JIT이 조건부 이동으로 합치는 경우도 흔함. - [ ] 선택된 분기만 평가된다 (단락 평가).
enemy != null ? enemy.Hp : 0이 안전한 이유. IL 상 두 분기가 서로 다른 경로에 놓임. - [ ] 중첩은 2갈래까지, 3갈래 이상이면 switch 표현식. 중첩 삼항은 오른쪽 결합이라
a ? b : c ? d : e는a ? b : (c ? d : e)로 해석됨. - [ ] 두 분기 타입은 공통 타입이 있어야 한다. CS0173 오류는 거의 항상 타입 불일치. 형제 클래스 간에도 C# 8까지는 직접 허용되지 않음.
- [ ] C# 9+의 target-typed 조건식을 기억하라.
int? x = cond ? 10 : null;,Animal a = cond ? new Dog() : new Cat();이 C# 9부터 허용. 단,var에는 적용되지 않음. - [ ]
??와 역할을 분담하라. "null이냐 아니냐"만 본다면??. 일반 조건이면?:. 혼동해서string.IsNullOrEmpty(name) ? "Guest" : name을name ?? "Guest"로 줄여 쓰면 빈 문자열 버그가 생긴다. - [ ] Unity 핫패스에서도 마음껏 써도 된다. 박싱도, 추가 할당도, 숨은 호출도 없다. 단, 양쪽 분기가 무거운 호출이라면 함수 분리를 먼저 고려하라.
'C# 기초' 카테고리의 다른 글
| [PART3.연산자와 표현식(8/11)] 연산자 우선순위와 결합성 — 괄호는 왜 거의 항상 옳은가 (0) | 2026.04.25 |
|---|---|
| [PART3.연산자와 표현식(7/11)] null 관련 연산자 — `?.` · `??` · `??=` · Null 조건부 대입(C# 14) (0) | 2026.04.25 |
| [PART3.연산자와 표현식(5/11)] 증감 연산자 — `++`, `--` (전위 vs 후위) (0) | 2026.04.25 |
| [PART3.연산자와 표현식(4/11)] 대입·복합 대입 연산자 — `=`, `+=`, 그리고 그 뒤에 숨은 것들 (1) | 2026.04.25 |
| [PART3.연산자와 표현식(3/11)] 비트 연산자 — `& | ^ ~ << >> >>>`의 동작 원리와 Unity 실전 (0) | 2026.04.25 |