Null-conditional Operator
C#에서 null
은 자주 발생하는 문제 중 하나이며, 이를 적절히 처리하지 않으면 NullReferenceException
과 같은 예외가 발생할 수 있습니다. 이를 방지하고 안정적이며 가독성 높은 코드를 작성하기 위해 C#은 null-conditional operator (?.
)와 null-forgiving operator (!
)를 제공합니다. 두 연산자의 동작 원리, 활용법, 성능, 그리고 적절한 사용 시점을 설명합니다.
Null-conditional Operator (?.
)
null-conditional operator는 nullable 참조 형식 또는 nullable 값 형식에서 속성, 메서드, 또는 인덱서를 안전하게 호출하기 위한 연산자입니다. 왼쪽 피연산자가 null이면 결과는 null이 되고, 그렇지 않으면 속성이나 메서드를 정상적으로 호출합니다.
int? length = myString?.Length;
myString이 null이면 length는 null이 됩니다. myString이 null이 아니면 myString.Length가 실행되어 결과가 length에 저장됩니다.
코드 예제
string? name = person?.Name; // person이 null이면 name은 null
int? length = myString?.Length; // myString이 null이면 length는 null
char? firstChar = myString?[0]; // myString이 null이면 firstChar는 null
장점
- 코드 간결화: null 체크 코드를 제거함으로써 코드가 간결해집니다.
// 기존방식
string? name = person != null ? person.Name : null;
// null-conditional operator를 사용
string? name2 = person?.Name;
- 안전성 : null인 경우 예외가 발생하지 않고 호출 실패 시 null을 반환하여 추가적인 예외 처리를 방지합니다.
단점
- null 결과를 처리할 필요가 있습니다. null-conditional operator를 사용하면 결과가 null이 될 수 있으므로, 추가적인 null 처리 코드가 필요합니다.
- null 체크는 조건 분기를 포함하므로 자주 호출되는 경우 성능 병목이 될 수 있습니다.
성능
null-conditional operator는 컴파일된 IL 코드에서 null 체크를 추가하는 분기를 생성합니다.
var length = myString?.Length;
위 코드는 다음 IL 코드로 변환됩니다.
IL_0000: ldloc.0 // myString 로드
IL_0001: brtrue.s IL_0004 // null 체크
IL_0003: ldc.i4.0 // null일 경우 결과는 0
IL_0004: callvirt // Length 호출
null 체크 자체는 가벼운 연산이므로 대부분의 상황에서 성능 영향은 미미하지만, 루프나 성능 민감한 코드에서는 누적 비용이 발생할 수 있습니다. 이런 경우 미리 null 가능성을 제거하거나 null-forgiving operator를 사용할 수 있습니다.
null-conditional operator를 활용
체인 연산
?.
를 체인chain 형태로 사용하면, 중첩된 객체에서 null 가능성을 안전하게 처리할 수 있습니다.
var result = person?.Address?.City?.Name;
위 코드는 다음과 같이 동작합니다:
person
이 null이면result
는 null.Address
가 null이면result
는 null.City
가 null이면result
는 null.- 모든 객체가 null이 아니면
Name
값을 반환.
Null-coalescing Operator (??
)와 결합
?.
와 ??
를 결합하여 null일 때 기본값을 제공할 수 있습니다.
var cityName = person?.Address?.City?.Name ?? "Unknown City";
Name
이 null이면"Unknown City"
를 반환합니다.
Delegate 호출에서 사용
?.
는 delegate를 호출할 때 유용합니다. delegate가 null일 가능성이 있을 때 안전하게 호출할 수 있습니다.
public event EventHandler? MyEvent;
MyEvent?.Invoke(this, EventArgs.Empty); // MyEvent가 null인지 확인 후 호출
?.
를 사용하면 null 체크 없이 이벤트를 안전하게 호출할 수 있습니다.
인덱서와 결합
인덱서를 사용할 때도 null 안전성을 확보할 수 있습니다.
var value = myDictionary?["key"];
myDictionary
가 null이면value
는 null.
Null-forgiving Operator (!
)
null-forgiving operator는 nullable 참조 형식에서 컴파일러의 null 경고를 무시할 수 있도록 합니다. 이는 “이 값은 절대 null이 아니다"라는 개발자의 확신을 컴파일러에 전달합니다. 런타임에는 어떤 동작도 추가하지 않습니다.
public string GetName(Person? person)
{
return person!.Name; // person이 null이 아님을 컴파일러에게 보장
}
코드 예제
string name = person!.Name; // 컴파일러 경고 제거
DoSomething(nullableObject!); // nullableObject는 null이 아님을 보장
장점
- nullable 참조 형 경고를 제거하여 코드 가독성을 향상시킵니다.
- 개발자가 null 가능성을 확신할 수 있는 경우 추가적인 조건문을 줄일 수 있습니다.
단점
- 잘못된 사용 시 NullReferenceException이 발생할 수 있습니다.
- 실제로 null 가능성이 있는 값을 잘못 판단하여 사용하면 문제가 감춰질 수 있습니다.
성능
null-forgiving operator는 런타임에 어떤 영향을 미치지 않습니다. 이는 컴파일러 지시문으로, 최종 IL 코드에는 !
의 흔적이 남지 않습니다.
string name = person!.Name;
위 코드는 다음과 같은 IL로 변환됩니다.
IL_0000: ldloc.0 // person 로드
IL_0001: callvirt // Name 호출
IL_0006: ret
!
는 컴파일 타임에만 영향을 미치므로 성능 상의 비용이 없습니다.
Null-forgiving Operator 활용
제네릭 타입에서 경고 제거
제네릭 타입에서 컴파일러는 nullable 여부를 알 수 없기 때문에 경고를 발생시킬 수 있습니다. !
를 사용해 이를 명시적으로 제거할 수 있습니다.
public void ProcessItem<T>(T item)
{
Console.WriteLine(item!.ToString()); // item은 null이 아님을 보장
}
- 제네릭 타입
T
가 nullable로 추론되어 발생하는 경고를 제거합니다.
패턴 매칭과 결합
패턴 매칭에서 nullable 값을 명시적으로 보장할 수 있습니다.
if (person is not null)
{
Console.WriteLine(person!.Name); // person이 null이 아님을 보장
}
person
이 null이 아님이 확실한 경우,!
를 사용해 컴파일러 경고를 제거합니다.