의도 표현

완료됨

이전 단원에서는 C# 컴파일러가 정적 분석을 수행하여 NullReferenceException을 방어하는 방법을 알아보았습니다. Null 허용 컨텍스트를 사용하도록 설정하는 방법도 알아보았습니다. 이 단원에서는 null 허용 컨텍스트 내에서 의도를 명시적으로 표현하는 방법을 자세히 알아봅니다.

변수 선언

Null 허용 컨텍스트를 사용하도록 설정하면 컴파일러가 코드를 파악하는 방법에 대한 가시성이 향상됩니다. null 허용 사용 컨텍스트에서 생성된 경고에 대해 조치를 취할 수 있으며, 이렇게 함으로써 의도를 명시적으로 정의합니다. 예를 들어 FooBar 코드를 계속 검사하면서 선언 및 할당을 자세히 살펴보겠습니다.

// Define as nullable
FooBar? fooBar = null;

FooBar?가 추가된 것을 알 수 있습니다. 이는 컴파일러에 fooBar에 null을 허용한다는 명시적 의도를 알려주는 것입니다. fooBar에 null을 허용할 의도는 없지만 여전히 경고를 피하고 싶은 경우에는 다음을 고려하세요.

// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;

다음은 이 변수를 명시적으로 null로 초기화하는 것을 컴파일러에 null지시하는 null 허용(!) 연산자를 추가하는 예제입니다. 컴파일러는 이 참조가 null이라는 경고를 내보내지 않습니다.

가능한 경우 null을 허용하지 않는 변수를 선언할 때 null이 아닌 변수를 할당하는 것이 좋습니다.

// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");

연산자

이전 단원에서 설명한 대로 C#은 null 허용 참조 형식을 중심으로 개발자의 의도를 표현하는 여러 연산자를 정의합니다.

null 허용(!) 연산자

이전 섹션에서 null 허용 연산자(!)를 소개했습니다. 이 연산자는 컴파일러에 CS8600 경고를 무시하도록 지시합니다. 이는 컴파일러에 사용자의 의도를 알리는 한 가지 방법이지만 “실제로 무엇을 하고 있는지 잘 알고” 있어야 합니다.

Null 허용 컨텍스트를 사용하는 동안 null을 허용하지 않는 형식을 초기화하는 경우 컴파일러에 명시적으로 허용을 요청해야 할 수 있습니다. 예를 들어, 다음 코드를 고려하세요.

#nullable enable

using System.Collections.Generic;

var fooList = new List<FooBar>
{
    new(Id: 1, Name: "Foo"),
    new(Id: 2, Name: "Bar")
};

FooBar fooBar = fooList.Find(f => f.Name == "Bar");

// The FooBar type definition for example.
record FooBar(int Id, string Name);

위의 예제에서는 FooBar fooBar = fooList.Find(f => f.Name == "Bar");가 CS8600 경고를 생성합니다. Findnull을 반환할 수 있기 때문입니다. 이 가능한 null이 이 컨텍스트에서는 null을 허용하지 않는 fooBar에 할당될 수 있습니다. 그러나 우리는 이 가상의 예제에서 Findnull을 반환하지 않을 것임을 알고 있습니다. Null 허용 연산자를 사용하여 컴파일러에 이 의도를 표현할 수 있습니다.

FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;

fooList.Find(f => f.Name == "Bar")의 끝에 있는 !에 유의하세요. 이는 컴파일러에 Find 메서드가 반환한 개체가 null일 수 있으며 이것은 정상이라고 알려줍니다.

null 허용 연산자는 메서드 호출 또는 속성 평가 전에 개체 인라인에 적용할 수도 있습니다. 또 다른 가상 예제를 살펴보겠습니다.

List<FooBar>? fooList = FooListFactory.GetFooList();

// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning

static class FooListFactory
{
    public static List<FooBar>? GetFooList() =>
        new List<FooBar>
        {
            new(Id: 1, Name: "Foo"),
            new(Id: 2, Name: "Bar")
        };
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

앞의 예제에서:

  • GetFooList는 nullable 형식 List<FooBar>?을 반환하는 정적 메서드입니다.
  • fooList에는 GetFooList가 반환한 값이 할당됩니다.
  • fooList에 할당된 값이 null일 수 있으므로 컴파일러에서 fooList.Find(f => f.Name == "Bar");에 대한 경고를 생성합니다.
  • fooListnull이 아니라고 가정하면 Findnull을 반환할 수 있지만 우리는 그러지 않을 것임을 알고 있으므로 null 허용 연산자가 적용됩니다.

fooList에 null 허용 연산자를 적용하여 경고를 비활성화할 수 있습니다.

FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;

참고

null 허용 연산자를 신중하게 사용해야 합니다. 단지 경고를 해제하는 용도로만 사용한다면 컴파일러에 가능한 null 사고를 검색하지 않도록 지시하는 것입니다. 확실한 경우에만 신중하게 사용해야 합니다.

자세한 내용은 !(null 허용) 연산자(C# 참조)를 참조하세요.

Null 병합(??) 연산자

Nullable 형식을 사용하는 경우 현재 값이 null인지 여부를 평가하고 특정 작업을 수행해야 할 수 있습니다. 예를 들어 nullable 형식이 null에 할당되었거나 초기화되지 않은 경우 null이 아닌 값을 할당해야 할 수 있습니다. 이러한 경우에 Null 병합 연산자(??)가 유용합니다.

다음 예제를 참조하세요.

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    salesTax ??= DefaultStateSalesTax.Value;

    // Safely use salesTax object.
}

위의 C# 코드에서:

  • salesTax 매개 변수는 null 허용 IStateSalesTax로 정의되어 있습니다.
  • 메서드 본문에서 salesTax는 null 병합 연산자를 사용하여 조건부로 값이 할당됩니다.
    • 이렇게 하면 salesTaxnull로 전달된 경우에도 값이 할당됩니다.

이는 다음 C# 코드와 기능적으로 동일합니다.

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    if (salesTax is null)
    {
        salesTax = DefaultStateSalesTax.Value;
    }

    // Safely use salesTax object.
}

다음은 null 병합 연산자가 유용할 수 있는 또 다른 일반적인 C# 관용구의 예제입니다.

public sealed class Wrapper<T> where T : new()
{
    private T _source;

    // If given a source, wrap it. Otherwise, wrap a new source:
    public Wrapper(T source = null) => _source = source ?? new T();
}

위의 C# 코드에서:

  • 제네릭 형식 매개 변수가 new()로 제한되는 제네릭 래퍼 클래스를 정의합니다.
  • 생성자는 기본값이 nullT source 매개 변수를 허용합니다.
  • 래핑된 _source는 조건부로 new T()로 초기화됩니다.

자세한 내용은 ?? 및 ??= 연산자(C# 참조)를 확인하세요.

null 조건부(?.) 연산자

Nullable 형식을 사용하는 경우 null 개체의 상태에 따라 조건부로 작업을 수행해야 할 수 있습니다. 예를 들어 이전 단원에서는 FooBar 레코드를 사용하여 null 역참조로 인한 NullReferenceException을 예시했습니다. 이 예외는 ToString이 호출되었을 때 발생했습니다. 동일한 예제이지만 이제는 null 조건부 연산자를 적용하겠습니다.

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

위의 C# 코드에서:

  • fooBar를 조건부로 역참조하여 ToString의 결과를 str 변수에 할당합니다.
    • str 변수는 string?(null 허용 문자열) 형식입니다.
  • 아무 것도 없는 표준 출력에 str 값을 씁니다.
  • Console.Write(null) 호출은 유효하므로 경고가 없습니다.
  • 잠재적으로 null을 역참조할 수 있으므로 Console.Write(str.Length)를 호출하면 경고가 표시됩니다.

이는 다음 C# 코드와 기능적으로 동일합니다.

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

연산자를 결합하여 의도를 더 구체적으로 표현할 수 있습니다. 예를 들어 다음과 같이 ?. 연산자와 ?? 연산자를 연결할 수 있습니다.

FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown

자세한 내용은 ?. 및 ?[](null 조건부) 연산자를 참조하세요.

요약

이 단원에서는 코드에서 null 허용 여부 의도를 표현하는 방법을 알아보았습니다. 다음 단원에서는 배운 내용을 기존 프로젝트에 적용합니다.

지식 점검

1.

string 참조 형식의 default 값은 무엇인가요?

2.

null 역참조의 예상 동작은 무엇인가요?

3.

throw null; C# 코드가 실행되면 어떻게 되나요?

4.

다음 중 null 허용 참조 형식에 대한 가장 정확한 설명은 무엇인가요?