본문 바로가기
카테고리 없음

C#에서 nullable 지시문, ! (null forgiving operator), ? (nullable annotation), 그리고 _ (discard) 및 = (assignment) 연산자

by 마인쥬리 2025. 6. 3.

 

 

C#에서 nullable 지시문, ! (null forgiving operator), ? (nullable annotation), 그리고 _ (discard) 및 = (assignment) 연산자는 모두 최신 C# 버전에서 중요한 역할을 합니다. 각각에 대해 자세히 설명해 드릴게요.


1. #nullable 지시문

#nullable 지시문은 C# 컴파일러에게 특정 코드 영역에서 null 참조 경고를 처리하는 방식을 지시합니다. C# 8.0부터 도입된 "nullable 참조 형식(nullable reference types)" 기능과 관련이 있습니다. 이 기능은 런타임에 발생할 수 있는 NullReferenceException을 컴파일 타임에 미리 방지하는 데 도움을 줍니다.

사용법:

  • #nullable enable: 이 지시문이 적용된 코드 블록 내에서는 null 참조에 대한 경고가 활성화됩니다. 즉, null이 될 수 있는 참조 형식에 대해 명시적으로 ?를 붙이지 않으면 컴파일러가 경고를 발생시킵니다.
  • #nullable disable: 이 지시문이 적용된 코드 블록 내에서는 null 참조에 대한 경고가 비활성화됩니다. C# 7.x 이전 버전의 동작과 유사하게, 컴파일러는 null 가능성을 엄격하게 확인하지 않습니다.
  • #nullable restore: 이 지시문이 적용된 코드 블록 내에서는 이전에 적용된 #nullable 설정으로 되돌아갑니다. 예를 들어, #nullable disable 후에 #nullable restore를 사용하면 다시 활성화될 수 있습니다 (프로젝트 파일 설정에 따라 다름).
  • #nullable disable warnings: nullability 관련 경고만 비활성화합니다.
  • #nullable enable warnings: nullability 관련 경고를 다시 활성화합니다.

예시:

C#
 
#nullable enable // 이 시점부터 nullability 경고 활성화

public class MyClass
{
    public string Name { get; set; } // 경고: 'Name' 속성은 null을 허용하지 않는 것으로 선언되었지만, 생성자에서 null이 아닌 값으로 초기화되지 않았습니다.
    public string? Description { get; set; } // null을 허용하는 형식

    public MyClass(string name)
    {
        Name = name;
        // Description은 초기화하지 않아도 되지만, 사용 시 null 체크 필요
    }
}

#nullable disable // 이 시점부터 nullability 경고 비활성화

public class AnotherClass
{
    public string Data { get; set; } // 경고 없음 (null 가능성 체크 안 함)
}

#nullable restore // 이 시점부터 이전 설정으로 복원

설정 위치:

프로젝트 파일 (.csproj)에서 <Nullable>enable</Nullable>을 설정하여 프로젝트 전체에 nullability 기능을 활성화할 수 있습니다. #nullable 지시문은 이 프로젝트 설정보다 더 세밀하게 제어할 때 사용합니다.


2. ! (Null-Forgiving Operator / Null-Suppression Operator)

! 연산자는 "null-forgiving operator" 또는 "null-suppression operator"라고 불립니다. 이 연산자는 개발자가 컴파일러에게 "이 표현식은 null이 아님을 확신한다"고 알려주는 역할을 합니다. 즉, 컴파일러의 null 경고를 억제합니다.

사용법:

null이 될 수 있다고 컴파일러가 판단하는 표현식 뒤에 붙입니다.

예시:

C#
 
#nullable enable

string? name = GetUserName(); // GetUserName()은 null을 반환할 수도 있음

// 컴파일러는 name이 null일 가능성이 있다고 경고함
// Console.WriteLine(name.Length); // 경고: 'name'은 null일 수 있습니다.

// '!'를 사용하여 null이 아님을 확신한다고 컴파일러에 알림
Console.WriteLine(name!.Length); // 경고 없음 (하지만 런타임에 name이 null이면 NullReferenceException 발생 가능)

string definitelyNotNull = "Hello";
string anotherString = GetAnotherString();

// 컴파일러가 anotherString이 null이 아니라고 확신할 수 없을 때 사용
// 하지만 개발자가 이 시점에서는 null이 아님을 확신한다면:
DoSomething(anotherString!);

string? GetUserName()
{
    // ... 실제 사용자 이름을 가져오는 로직 (null을 반환할 수도 있음)
    return "John Doe";
}

void DoSomething(string text)
{
    Console.WriteLine(text);
}

string GetAnotherString()
{
    return "This is not null.";
}

주의점:

! 연산자는 런타임에 null 체크를 수행하지 않습니다. 단지 컴파일러의 경고를 무시하도록 지시하는 것뿐입니다. 만약 실제로 값이 null이면 NullReferenceException이 발생합니다. 따라서 개발자가 해당 표현식이 절대 null이 아님을 확신할 때만 신중하게 사용해야 합니다.


3. ? (Nullable Annotation)

? 연산자는 두 가지 주요 맥락에서 사용됩니다.

  • Nullable Value Types (널 허용 값 형식): C# 2.0부터 도입되었습니다. int?, bool?, DateTime?와 같이 값 형식 뒤에 붙여 해당 값이 null을 가질 수 있음을 나타냅니다.
  • Nullable Reference Types (널 허용 참조 형식): C# 8.0부터 도입되었습니다. string?, MyClass?와 같이 참조 형식 뒤에 붙여 해당 참조가 null을 가질 수 있음을 나타냅니다. 이 기능은 #nullable enable 환경에서 활성화됩니다.

사용법 및 예시:

a) Nullable Value Types (널 허용 값 형식):

C#
 
int? nullableInt = null; // null을 할당할 수 있음
nullableInt = 10;

double? nullableDouble = 3.14;

if (nullableInt.HasValue) // 값이 있는지 확인
{
    Console.WriteLine($"Value: {nullableInt.Value}"); // 실제 값에 접근
}

// Nullable<T>의 축약형
// int?는 System.Nullable<int>와 동일합니다.
Nullable<int> anotherNullableInt = 20;

b) Nullable Reference Types (널 허용 참조 형식):

C#
 
#nullable enable

string? message = null; // null을 할당할 수 있는 문자열 참조
string nonNullableString = "Hello"; // null을 허용하지 않는 문자열 참조 (초기화 필요)

// null을 허용하는 매개변수
void PrintMessage(string? text)
{
    if (text != null)
    {
        Console.WriteLine(text.Length); // null 체크 후 안전하게 사용
    }
    else
    {
        Console.WriteLine("Message is null.");
    }
}

PrintMessage(message);
PrintMessage(nonNullableString);

// 반환값이 null일 수 있음을 나타냄
string? GetOptionalData()
{
    return null; // 또는 "Some data"
}

string? data = GetOptionalData();
if (data != null)
{
    Console.WriteLine(data);
}

4. _ (Discard / Discard Parameter)

_ (언더스코어)는 C#에서 "discard"라고 불리며, 값을 의도적으로 무시하거나 사용하지 않을 때 사용됩니다. 여러 문맥에서 활용될 수 있습니다.

사용법 및 예시:

a) Lambda Expression Parameters (람다 식 매개변수):

람다 식에서 특정 매개변수를 사용하지 않을 때 _를 사용합니다.

C#
 
// 첫 번째 매개변수를 사용하지 않을 때
Action<int, int> processNumbers = (x, _) => Console.WriteLine($"Only processing x: {x}");
processNumbers(10, 20); // 출력: Only processing x: 10

// 모든 매개변수를 사용하지 않을 때
Action<string, int> logEvent = (_, _) => Console.WriteLine("Event occurred, ignoring details.");
logEvent("UserLoggedIn", 123); // 출력: Event occurred, ignoring details.

b) Tuples and Deconstruction (튜플 및 분해):

튜플 또는 객체를 분해할 때 특정 요소를 무시하고 싶을 때 사용합니다.

C#
 
// 튜플 분해
(int x, int y, _) = (1, 2, 3); // 세 번째 요소 3은 무시됨
Console.WriteLine($"x: {x}, y: {y}"); // 출력: x: 1, y: 2

// 객체 분해 (Deconstruct 메서드가 정의된 경우)
public class Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public void Deconstruct(out int x, out int y, out int z)
    {
        x = X;
        y = Y;
        z = 0; // 예시를 위해 추가
    }
}

Point p = new Point { X = 5, Y = 10 };
(int pX, _, _) = p; // Y와 Z는 무시
Console.WriteLine($"pX: {pX}"); // 출력: pX: 5

c) Pattern Matching (패턴 매칭):

switch 식이나 is 식에서 특정 패턴을 무시하고 싶을 때 사용합니다.

C#
 
object obj = 123;

if (obj is int _) // obj가 int 타입인지만 중요하고 그 값은 필요 없을 때
{
    Console.WriteLine("obj is an integer.");
}

string input = "hello";
switch (input)
{
    case "hello":
        Console.WriteLine("Hello!");
        break;
    case string _: // 어떤 문자열이든 매치하지만 그 값은 사용하지 않을 때
        Console.WriteLine("It's a string, but I don't care about the value.");
        break;
    case null:
        Console.WriteLine("It's null.");
        break;
}

d) Out Variables (out 변수):

메서드에서 out 매개변수를 통해 반환되는 값 중 필요 없는 값이 있을 때 사용합니다.

C#
 
bool success = int.TryParse("123", out _); // 파싱된 정수 값은 필요 없고 성공 여부만 알면 될 때
Console.WriteLine($"Parse successful: {success}"); // 출력: Parse successful: True

bool TryGetSomething(out int value, out string message)
{
    value = 100;
    message = "Success";
    return true;
}

TryGetSomething(out _, out string msg); // value는 무시하고 message만 사용
Console.WriteLine($"Message: {msg}"); // 출력: Message: Success

5. = (Assignment Operator)

= 연산자는 C#에서 값을 변수나 속성에 할당할 때 사용되는 "할당 연산자"입니다. 가장 기본적이고 자주 사용되는 연산자 중 하나입니다.

사용법 및 예시:

a) Variable Initialization and Assignment (변수 초기화 및 할당):

C#
 
int age = 30; // 변수 초기화
string name; // 변수 선언
name = "Alice"; // 변수에 값 할당

double price = 99.99;
bool isActive = true;

b) Property Assignment (속성 할당):

클래스나 구조체의 속성에 값을 할당할 때 사용합니다.

C#
 
public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Product product = new Product();
product.Name = "Laptop"; // 속성 할당
product.Price = 1200.50m;

c) Method Parameter Default Values (메서드 매개변수 기본값):

메서드 매개변수에 기본값을 지정할 때 사용합니다.

C#
 
void Greet(string userName = "Guest")
{
    Console.WriteLine($"Hello, {userName}!");
}

Greet();          // 출력: Hello, Guest!
Greet("Bob");     // 출력: Hello, Bob!

d) Object Initializers (객체 초기화자):

객체를 생성하면서 속성을 초기화할 때 사용합니다.

C#
 
Product newProduct = new Product
{
    Name = "Mouse", // 객체 초기화자를 사용한 속성 할당
    Price = 25.00m
};

e) Compound Assignment Operators (복합 할당 연산자):

+=, -=, *=, /=, %= 등과 같이 다른 연산자와 결합하여 사용될 수도 있습니다.

C#
 
int count = 5;
count += 3; // count = count + 3; 과 동일 (count는 이제 8)

double value = 10.0;
value *= 2.5; // value = value * 2.5; 와 동일 (value는 이제 25.0)

이러한 문법 요소들은 C#의 현대적인 기능을 이해하고 효과적으로 활용하는 데 필수적입니다. 특히 nullable 지시문과 ! ?는 코드의 안정성과 가독성을 높이는 데 크게 기여합니다.

 

간단 요약 : 

1. #nullable 지시문

  • 목적: C# 8.0부터 도입된 #nullable enable 과 같은 지시문은 널( null ) 참조에 대해 컴파일러가 경고를 내도록 해줘. 즉, 변수나 속성이 null일 수 있는지 추적해서, 잠재적인 NullReferenceException을 줄이도록 도와주는 기능이야.
  • 사용법 예시:이렇게 사용하면 컴파일러가 null 관련 경고를 내려줘서 코드의 안정성을 높이려고 하는 거지.
  •  
    #nullable enable
    string? nullableString = null;   // null값을 허용
    string nonNullableString = "Hello"; // null이 될 수 없는 변수
    
  • csharp
  • 어려워보일 수 있는 이유: 기존 코드에 #nullable 기능이 활성화되면, 기존에 null 처리가 되어 있지 않은 코드들도 경고를 발생시킬 수 있어. 그래서 갑자기 코드에 많은 경고가 뜨면 당황스러울 수 있어.

2. _ = (디스카드(discard) 연산자)

  • 목적: C# 7.0부터 도입된 _는 디스카드(discard) 역할을 해. 즉, 반환되는 값이나 사용하지 않을 변수를 굳이 명시적으로 받을 필요가 없을 때, 그 값을 무시하겠다는 의미야.
  • 사용법 예시:위 코드는 Task.Run이 반환하는 Task 객체를 사용하지 않겠다는 의도를 컴파일러에 명시하는 거야. 만약 변수 이름을 주면 "아, 이 변수를 사용하겠다"라고 해석하는데, 사용하지 않을 거면 _로 처리해서 불필요한 경고를 줄일 수 있어.
  • csharp
    _ = Task.Run(() => ReceiveMessage());
    
  • 어려워하는 이유: 새로운 문법이기 때문에 기존에 익숙했던 방식과 다르게 보일 수 있고, 왜 이런 문법이 필요한지 이해하기 어려울 수 있어.

왜 이런 문법들이 존재하냐면:

  • 코드 안정성: #nullable을 통해 null 관련 버그를 미리 예방할 수 있고, _를 통해 사용하지 않을 값을 명시적으로 처리함으로써 코드의 의도를 명확하게 하고 경고를 줄일 수 있어.
  • 컴파일러의 도움: 이러한 기능들을 통해 컴파일러가 코드의 문제를 더 잘 잡아내게 하고, 개발자가 코드에 집중할 수 있도록 도와주지.

어떻게 대처할까?

  • 적응: 처음에는 생소할 수 있지만, 기능이 왜 존재하는지 이해하고 조금씩 코드를 작성하다 보면 자연스럽게 익숙해질 거야.
  • 옵션 변경: 만약 #nullable이 너무 불편하다면, 프로젝트 파일이나 코드 상단에서 #nullable disable으로 비활성화시킬 수도 있어. 다만, 그러면 null 관련 버그를 미리 잡아내기 어려울 수 있으니 상황에 맞게 선택하면 돼.
  • 참고 문서: Microsoft의 공식 문서나 다양한 블로그 문서들을 참고하면, 각각의 문법이 어떻게 사용되고 어떤 문제가 발생할 수 있는지 더 자세히 알 수 있어.