C# null 연산자

Tip

이 문서는 하나 이상의 프로그래밍 언어를 알고 C#을 학습하는 개발자를 위한 기본 사항 섹션의 일부입니다. 프로그래밍을 처음 접하는 경우 먼저 시작 자습서로 시작 하세요. 전체 연산자 참조는 언어 참조의 멤버 액세스 연산 자 및 null 병합 연산 자를 참조하세요.

C#은 Null로부터 안전한 코드를 간결하게 만드는 여러 연산자를 제공합니다. 코드 전체에서 가드를 중첩하는 if (x != null) 대신 이러한 연산자를 사용하면 단일 식에서 null로부터 안전한 액세스, 대체 값 및 null 테스트를 표현할 수 있습니다.

이 문서에서는 ?.?[] null 조건부 접근, ?? null 병합, ??= null 병합 할당, 및 is null/is not null null 패턴 매칭에 대해 설명합니다.

Null 조건부 멤버 액세스 ?.

연산자는 ?. 개체가 null이 아닌 경우에만 멤버에 액세스합니다. 객체가 null일 때, 전체 식은 null로 평가되며, NullReferenceException를 던지지 않습니다.

string? name = null;

// Without ?., accessing a member on null throws NullReferenceException:
// int len = name.Length; // throws if name is null

// ?. returns null instead of throwing:
int? len = name?.Length;
Console.WriteLine(len.HasValue); // False

name = "C#";
Console.WriteLine(name?.Length); // 2

?. 연산자는 쇼트 회로입니다: 왼쪽의 항이 null인 경우, 오른쪽의 모든 것들을 건너뜁니다. 메서드 호출이 실행되지 않고 부작용이 발생하지 않습니다.

단일 식에서 여러 ?. 연산자를 연결할 수 있습니다. 체인은 처음으로 만나는 null에서 중지됩니다.

string? input = null;

// Chain ?. across multiple method calls — short-circuits at the first null:
string? upper = input?.Trim()?.ToUpperInvariant();
Console.WriteLine(upper ?? "(none)"); // (none)

input = "  hello  ";
Console.WriteLine(input?.Trim()?.ToUpperInvariant()); // HELLO

Null 조건부 인덱서 액세스 ?[]

?[] 연산자는 인덱서 및 배열 액세스에 동일한 단락 동작을 적용합니다. 컬렉션 자체가 null 상태일 수 있는 경우에 사용합니다.

string[]? tags = null;

// ?[] accesses an element only when the collection is non-null
string? first = tags?[0];
Console.WriteLine(first ?? "(none)"); // (none)

tags = ["csharp", "dotnet", "nullable"];
Console.WriteLine(tags?[0]);          // csharp

Null 조건부 연산자 연결

여러 ?. 연산자를 연결하여 잠재적으로 null 참조의 경로를 탐색합니다. 첫 번째 null에서 체인이 단락됩니다.

var order = new Order("ORD-001", null);

// Each ?. short-circuits when null: Customer is null, so Address and City are never accessed
string? city = order.Customer?.Address?.City;
Console.WriteLine(city ?? "(no city)"); // (no city)

var fullOrder = new Order("ORD-002",
    new Customer("Alice", new Address("123 Main St", "Springfield", "IL")));

Console.WriteLine(fullOrder.Customer?.Address?.City); // Springfield

Customernull일 때, AddressCity는 평가되지 않습니다. 전체 식이 반환됩니다 null.

스레드 안전한 대리자 호출

?. 는 대리자를 호출하거나 이벤트를 발생시킬 수 있는 스레드로부터 안전한 깨끗한 방법을 제공합니다. 대리자 식은 한 번만 평가되므로 null 검사와 호출 사이에 다른 스레드가 구독을 취소할 수 있는 창이 없습니다.

EventHandler? clicked = null;

// No subscribers — ?.Invoke does nothing instead of throwing NullReferenceException
clicked?.Invoke(null, EventArgs.Empty);

clicked += (_, _) => Console.WriteLine("Button clicked!");

// With a subscriber — ?.Invoke calls the handler
clicked?.Invoke(null, EventArgs.Empty);
// Output: Button clicked!

이 패턴은 이전 if (clicked != null) clicked(...) 관용구를 대체합니다.

Null 병합 연산자 ??

?? 연산자는 왼쪽 피연산자가 null이 아닐 때 해당 피연산자를 반환하고, 왼쪽 피연산자가 null일 때 오른쪽 피연산자를 반환합니다. 기본값을 제공하는 데 사용합니다.

string? username = null;

// ?? returns the right-hand value when the left-hand is null
string display = username ?? "Guest";
Console.WriteLine(display); // Guest

username = "alice";
display  = username ?? "Guest";
Console.WriteLine(display); // alice

??는 오른쪽 결합을 가지므로 a ?? b ?? ca ?? (b ?? c)로 평가됩니다. null이 아닌 첫 번째 값이 우선합니다. 일반적인 패턴은 ?.??를 체인으로 연결하는 것입니다. ?.를 사용하여 null 가능 체인을 안전하게 탐색한 후, 체인이 ??를 반환하면 null로 기본값을 대체합니다. 전체 예제는 Null 연산자 결합을 참조하세요.

Null 병합 할당 ??=

연산자는 ??= 변수가 있는 경우에만 오른쪽 값을 변수 null에 할당합니다. 지연 초기화에 사용합니다.

List<string>? cache = null;

// ??= assigns only when the variable is null
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

// cache is already non-null, so LoadData() isn't called again
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

static List<string> LoadData() => ["alpha", "beta", "gamma"];

오른쪽 식은 변수가 있는 경우에만 계산됩니다 null. 변수에 이미 값이 있는 경우 오른쪽은 전혀 평가되지 않습니다.

Null 조건부 할당(C# 14)

C# 14부터 할당 대상으로 사용할 ?.?[] 수 있습니다. 할당은 왼쪽 개체가 null이 아닌 경우에만 실행됩니다.

AppConfig? config = new AppConfig();

// Assigns only when config is non-null (C# 14)
config?.Theme = "dark";
Console.WriteLine(config?.Theme); // dark

AppConfig? missing = null;
missing?.Theme = "light";                         // no-op: missing is null
Console.WriteLine(missing?.Theme ?? "(no config)"); // (no config)

왼쪽이 null이 아님이 확인된 경우에만 오른쪽이 평가됩니다.

Null 패턴 일치: is nullis not null

is nullis not null 패턴은 null인지 여부를 테스트합니다.

string? input = null;

// is null is the preferred test — unaffected by operator overloading
if (input is null)
{
    Console.WriteLine("No input provided.");
}

// == null also works, but a custom == operator can change its behavior
if (input == null)
{
    Console.WriteLine("Still no input.");
}

null 검사에는 is null== null보다 선호합니다. 연산자는 == 오버로드될 수 있습니다. 즉, 형식이 사용자 지정 같음 연산자를 정의하면 x == nulltruex가 아닐 때도 null을 반환할 수 있습니다. 패턴은 is null 연산자 오버로드에 관계없이 항상 실제 null 참조를 테스트합니다.

string? value = "hello";

if (value is not null)
{
    Console.WriteLine(value.ToUpper()); // HELLO
}

null 연산자 결합

실제로 이러한 연산자 중 몇 가지를 결합하는 경우가 많습니다. 하나의 식은 깊은 객체 그래프를 안전하게 탐색하고 대체(fallback)를 적용한 다음 결과를 검토합니다.

Order? order = GetPendingOrder();

// Chain ?. for safe traversal, ?? for a fallback, is null for a clear guard
string city = order?.Customer?.Address?.City ?? "unknown";

if (order is null)
{
    Console.WriteLine("No pending order.");
}
else
{
    Console.WriteLine($"Shipping to: {city}");
}
// Output: No pending order.

널 허용 강제 연산자 !

! 후위 연산자는 nullable 경고를 억제합니다. 컴파일러에 "이 식은 확실히 null이 아닙니다"라고 말하도록 추가 ! 합니다. 연산자는 런타임에 영향을 주지 않습니다. 컴파일러의 null 상태 분석에만 영향을 줍니다.

string? name = FindUser("alice");

// Use ! only when you have information the compiler doesn't.
// FindUser guarantees a non-null result for known usernames.
int length = name!.Length;
Console.WriteLine(length); // 5

컴파일러가 알지 못하는 정보가 있을 때만 !를 아껴서 사용하세요. 예를 들어 인수 확인 논리의 유효성을 검사하기 위해 의도적으로 전달하는 null 테스트 또는 계약이 알려진 입력에 대해 null이 아닌 반환을 보장하는 메서드를 호출하는 테스트가 있습니다. 과용하면 ! nullable 참조 형식의 목적이 무효화됩니다. 전체 설명은 Nullable 참조 형식을 참조하세요.

참고하십시오