nullable 참조 형식

Tip

소프트웨어 개발이 새로운가요? 시작 자습서로 시작 합니다.

다른 언어로 경험하신 적 있나요? Kotlin의 nullable 형식, TypeScript strictNullChecks또는 Swift의 선택 사항으로 작업한 경우 모델은 익숙합니다. C#은 별도의 형식 대신 정적 분석 및 경고 진단을 사용합니다. 주석으로 의도 표현Null 상태 분석을 간단히 살펴본 다음, 자습서: nullable 및 null 비허용 참조 형식으로 설계 의도 표현으로 이동하여 해당 기능을 적용해 보세요.

null 허용 참조 형식은 코드에서 System.NullReferenceException이 발생할 가능성을 최소화하는 기능 모음입니다. 어떤 변수를 보유할 것인지, 어떤 변수를 보유 null 하지 않는지 선언하면 해당 선언이 코드에서 사용하는 방식과 일치하지 않을 때 컴파일러가 경고합니다. 프로그램의 런타임 동작은 변경되지 않습니다. Nullable 참조 형식은 전적으로 컴파일 시간 기능입니다.

세 가지 구성 요소가 함께 작동합니다.

  • 변수 어노테이션(string 대비 string?)은 어떤 참조가 null을 허용하도록 의도되었는지를 나타냅니다.
  • null 상태 분석은 코드의 각 지점에서 식의 값이 null이 아님인지 아니면 null일 수 있음인지를 추적합니다.
  • API의 특성은 "이 인수는 nullnull일 수 있지만, 반환 값은 인수가 null인 경우에만 null입니다."와 같은 더 세부적인 규약을 설명합니다.

컴파일러는 이러한 신호를 결합하여 진단을 생성합니다. null을 허용하지 않는 변수에 대한 경고는 해당 변수에 null이 할당될 수 있음을 의미합니다. nullable 변수에 대한 경고는 코드가 null 검사 없이 역참조 할 수 있음을 의미합니다. 역참조 는 변수가 참조하는 값을 사용하는 것을 의미합니다. 예를 들어 메서드를 호출하려면(variable.Method()), 속성(variable.Property) 또는 인덱스(variable[0])를 읽습니다. 값 null 이 있는 변수를 역참조하는 경우 런타임에 예외가 throw됩니다. 두 종류의 경고는 코드의 동작이 명시된 디자인과 일치하지 않음을 의미합니다.

Nullable 컨텍스트

최근 .NET 템플릿에서 만든 프로젝트는 프로젝트 파일에서 <Nullable>enable</Nullable> 설정하므로 이 문서의 지침은 작성된 대로 적용됩니다. 이전 프로젝트에서 작업하는 경우 다음 줄을 열고 .csproj 다음 줄이 포함되어 있는지 <PropertyGroup> 확인합니다. 누락된 경우 추가합니다.

<Nullable>enable</Nullable>

대규모 애플리케이션 마이그레이션에 대한 자세한 내용은 nullable 마이그레이션 전략에 대한 문서를 참조하여 더 많은 설정 및 지시문을 참조하세요.

주석을 사용하여 의도 표현

모든 참조 형식 변수는 기본적으로 null을 허용하지 않습니다 . ?을 추가하여 nullable 참조 형식을 선언합니다:

public static void Annotations()
{
    string required = "always set";   // non-nullable: assigning null produces a warning
    string? optional = null;          // nullable: holding null is allowed

    Console.WriteLine(required.Length);

    if (optional is not null)
    {
        Console.WriteLine(optional.Length);
    }
}

주석은 런타임 형식을 변경하지 않습니다. stringstring? 둘 다 System.String입니다. ?는 컴파일러에 설계 의도를 알립니다. 이 의도는 컴파일러가 생성하는 경고를 셰이프합니다.

  • null을 허용하지 않는 변수의 기본 null 상태null 아님입니다. 컴파일러는 값이 될 null수 있는 값을 할당하면 경고합니다.
  • nullable 변수의 기본 null 상태는maybe-null입니다. 컴파일러는 변수를 먼저 확인하지 않고 역참조하면 경고합니다.

주석을 사용하여 형식 시스템에 필수 및 선택적 값을 표시합니다. 다음 Person 형식은 FirstNameLastName를 null 비허용으로 선언합니다. 모든 사람에게 이 둘은 모두 필요하기 때문입니다. 또한 MiddleName는 모든 사람이 이를 갖는 것은 아니므로 null 허용 가능으로 선언합니다:

public sealed class Person(string firstName, string lastName)
{
    public string FirstName { get; } = firstName;
    public string? MiddleName { get; init; }
    public string LastName { get; } = lastName;

    public override string ToString() => MiddleName is null
        ? $"{FirstName} {LastName}"
        : $"{FirstName} {MiddleName} {LastName}";
}

public static void DesignIntent()
{
    Person p1 = new("Ada", "Lovelace") { MiddleName = "King" };
    Console.WriteLine(p1);
    // Output: Ada King Lovelace

    Person p2 = new("Grace", "Hopper");
    Console.WriteLine(p2);
    // Output: Grace Hopper
}

주석은 .의 ToString구현을 구동합니다. FirstNameLastName는 null을 허용하지 않으므로, 재정의에서는 null 검사를 하지 않고 이를 보간된 문자열({} 자리 표시자에 식을 포함하는 $"..." 구문)에서 직접 사용합니다. MiddleName은 null을 허용하므로 재정의에서는 먼저 이를 null와 비교하고, 값이 있을 때만 포함합니다. 컴파일러는 그 차이를 엄격히 적용합니다. null을 허용하지 않는 값이 필요한 곳에 null일 수도 있는 값을 전달하는 코드는 경고를 발생시키며, null을 허용하지 않는 멤버를 초기화하지 않은 상태로 남겨 두는 생성자도 경고를 발생시킵니다.

Null 상태 분석

컴파일러는 모든 식의 null 상태를 추적합니다. 상태는 다음 두 값 중 하나입니다.

  • null이 아님: 식이 null이 아닌 것으로 알려져 있습니다.
  • maybe-null: 식이 null일 수 있습니다.

컴파일러가 코드를 분석하면 지역 변수의 null 상태가 업데이트됩니다. 이를 변경하는 것은 두 가지입니다: 할당null 검사. 할당 후 변수의 null 상태는 오른쪽의 식과 일치합니다. 식이 null이거나 nullable인 경우 변수는 null일 수 있는 상태가 됩니다. 식이 null이 아닌 리터럴인 경우, 변수는 null이 아닌 상태가 됩니다. null 검사 후 변수의 null 상태는 선택된 분기를 반영하게 됩니다.

public static void NullStateTracking()
{
    string? message = null;

    // Warning: dereference of a possibly null reference.
    Console.WriteLine(message.Length);

    message = "Hello, World!";

    // No warning: the compiler tracks that message is now not-null.
    Console.WriteLine(message.Length);
}

앞의 예제에서 첫 번째 역참조는 message 있으므로 경고를 생성합니다. null이 아닌 리터럴에 할당된 후 컴파일러는 message을 알고 있으므로 두 번째 역참조는 안전합니다.

Null 상태 분석은 if 검사, 패턴 매칭(값의 형태를 검사하는 is null 또는 is { }와 같은 식), 그리고 반복하거나 조기에 반환하는 제어 흐름 전반에 걸쳐 작동합니다.

 public sealed class Node(string name)
 {
     public string Name { get; } = name;
     public Node? Parent { get; init; }
 }

 public static void FlowAnalysis(Node start)
 {
     Node? current = start;
     while (current is not null)
     {
         // Inside the loop, the compiler knows current is not-null.
         Console.WriteLine(current.Name);

         current = current.Parent;
     }
}

분석은 메서드 본문 내부로 들어가 분석하지 않습니다. null 상태를 호출자에게 전달하는 메서드가 필요한 경우 해당 서명에 nullable 분석 특성을 사용합니다.

!로 경고를 재정의하세요

컴파일러보다 더 많은 것을 알고 있는 경우가 있습니다. null 허용 무시 연산자!는 분석 결과가 그렇지 않다고 하더라도 식이 null이 아님을 선언합니다:

public static void NullForgiving()
{
    // "ada" matches a switch arm that returns a non-null string,
    // but the return type is string? so the compiler treats the
    // result as maybe-null.
    string? maybeName = LookUpName("ada");

    // The ! tells the compiler "trust me, this isn't null." We just
    // passed "ada", which the switch maps to "Ada Lovelace".
    int length = maybeName!.Length;
    Console.WriteLine(length); // => 12
}

// Returns string? because the wildcard arm yields null.
private static string? LookUpName(string id) => id switch
{
    "ada" => "Ada Lovelace",
    _ => null,
};

!은(는) 신중하게 사용해야 합니다. 그것이 나타나는 곳마다 컴파일러는 더 이상 여러분을 보호할 수 없습니다. 컴파일러가 자체적으로 올바른 결론에 도달할 수 있도록 null 검사를 추가하거나, 코드를 재구성하거나, 관련 API에 주석을 추가하는 것을 선호합니다.

API 계약을 설명하는 특성

매개 변수 또는 반환 형식의 주석이 항상 충분히 표현되지는 않습니다. 어떤 메서드는 null일 수도 있는 인수를 허용하지만 null이 아닌 결과를 보장할 수 있습니다. 테스트 메서드는 인수가 null이 아닌 경우에만 반환 true 할 수 있습니다. nullable 분석 특성을 사용하여 다음 계약을 전달합니다.

public static bool IsPresent([NotNullWhen(true)] string? value) =>
    !string.IsNullOrEmpty(value);

public static void NullAnalysisAttributes()
{
    string? input = ReadInput();

    if (IsPresent(input))
    {
        // No null-forgiving operator needed: the attribute tells the compiler
        // input is not-null when IsPresent returns true.
        Console.WriteLine(input.Length);
    }
}

private static string? ReadInput() => "hello";

NotNullWhenAttributeIsPresenttrue를 반환할 때 인수가 null이 아님을 컴파일러에 알려줍니다. if 블록 내부에서는 컴파일러가 value를 null 허용 무시 연산자 없이도 null이 아님으로 처리합니다. .NET 5를 기준으로 모든 .NET 런타임 API에 주석이 추가되므로 분석에서 이를 호출하는 모든 코드에 이점을 제공합니다.

알려진 함정

두 가지 패턴에서는 경고 없이 null을 허용하지 않는 참조가 null를 보유하게 될 수 있습니다. 두 패턴 모두 코드의 버그가 아니라 정적 분석의 제한 사항입니다.

기본 구조체

default 또는 new()를 사용하여 null을 허용하지 않는 참조 필드가 있는 구조체를 만들 수 있습니다. 이 방법은 구조체의 필드를 초기화되지 않은 상태로 둡니다.

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

public static void DefaultStructPitfall()
{
    Student s = default;            // No warning, but FirstName and LastName are null.
    Console.WriteLine(s.FirstName?.Length ?? -1);
}

필드는 런타임에 null를 값으로 갖지만 컴파일러는 경고하지 않습니다. 구조체를 사용해야 하는 경우 호출자가 개체 이니셜라이저 또는 호출자가 호출해야 하는 매개 변수가 있는 생성자를 통해 초기화해야 하는 멤버인 필수 멤버를 선호합니다.

참조 및 구조체의 배열

nullable이 아닌 참조 형식의 새 배열에는 각 요소를 할당할 때까지 모든 null 요소가 포함됩니다.

public static void ArrayPitfall()
{
    string[] values = new string[3];      // Elements are null at run time.
    Console.WriteLine(values[0]?.Length ?? -1);

    string[] initialized = ["a", "b", "c"]; // Collection expression initializes every slot.
    Console.WriteLine(initialized[0].Length);
}

구조체 배열에도 동일한 함정이 적용됩니다. 모든 요소는 구조체의 기본값으로 시작하므로 각 요소의 nullable이 아닌 참조 필드가 로 null시작됩니다.

배열을 만드는 과정의 일부로 배열 요소를 초기화합니다. 컬렉션 식 ( [1, 2, 3] 리터럴 구문) 및 대상 형식 new (컴파일러가 형식을 유추할 수 있는 경우 작성 new() )은 전체 초기화를 간결하게 만듭니다.