Nullable 값 형식

Nullable 값 형식 (C# reference)

값 형식인데도 값이 "없을 수 있는" 상황이 필요할 때가 있어요. nullable 값 형식 T?는 기본이 되는 값 형식 T의 모든 값에다 null이라는 값을 하나 더 얹은 형태예요. 예를 들어 bool? 변수에는 true, false, null 이렇게 세 가지를 모두 넣을 수 있죠. 그리고 기본 값 형식 T 자체가 nullable 값 형식일 수는 없어요.

출처: Microsoft Learn

본문

nullable 값 형식은 전부 제네릭 구조체 xref:System.Nullable`1?displayProperty=nameWithType의 인스턴스예요. 기본 형식이 T인 nullable 값 형식은 Nullable<T> 또는 T?처럼 서로 바꿔 쓸 수 있는 형태로 표현할 수 있어요.

보통은 기본 값 형식에 "정의되지 않은 값"을 나타낼 필요가 있을 때 nullable 값 형식을 써요. 가령 bool 변수는 truefalse 둘 중 하나만 가질 수 있는데요, 어떤 응용에서는 변수의 값이 정의되지 않았거나 비어 있을 수도 있죠. 데이터베이스 필드가 truefalse를 담을 수도 있고, 아무 값도 없는 NULL일 수도 있는 그런 상황이 대표적이에요. 그럴 때 bool? 형식을 쓰면 딱 맞아요.

선언과 대입

값 형식은 대응하는 nullable 값 형식으로 암시적으로 변환이 돼요. 그래서 nullable 값 형식 변수에 값을 넣는 건 그냥 기본 값 형식에 값을 넣는 것과 똑같이 하면 되고, 거기에 null도 그대로 대입할 수 있어요. 예를 들어볼게요:

double? pi = 3.14;
char? letter = 'a';

int m2 = 10;
int? m = m2;

bool? flag = null;

// An array of a nullable value type:
int?[] arr = new int?[10];

nullable 값 형식의 기본값은 null을 나타내요. 즉 xref:System.Nullable`1.HasValue?displayProperty=nameWithType 속성이 false를 반환하는 인스턴스인 거죠.

nullable 값 형식 인스턴스 확인하기

nullable 값 형식의 인스턴스가 null인지 확인하고 기본 형식의 값을 꺼내려면, 타입 패턴과 함께 is 연산자를 쓰면 돼요:

int? a = 42;
if (a is int valueOfA)
{
    Console.WriteLine($"a is {valueOfA}");
}
else
{
    Console.WriteLine("a does not have a value");
}
// Output:
// a is 42

nullable 값 형식 변수의 값을 확인하고 꺼내는 데는 다음의 읽기 전용 속성들을 항상 쓸 수 있어요.

값을 화면에 보여주기 전에 변수에 값이 들어 있는지 HasValue 속성으로 확인하는 예제를 볼게요:

int? b = 10;
if (b.HasValue)
{
    Console.WriteLine($"b is {b.Value}");
}
else
{
    Console.WriteLine("b does not have a value");
}
// Output:
// b is 10

HasValue 속성 대신 nullable 값 형식 변수를 null과 직접 비교할 수도 있어요. 다음 예제처럼요:

int? c = 7;
if (c != null)
{
    Console.WriteLine($"c is {c.Value}");
}
else
{
    Console.WriteLine("c does not have a value");
}
// Output:
// c is 7

nullable 값 형식에서 기본 형식으로 변환하기

nullable 값 형식의 값을 기본(비-nullable) 값 형식 변수에 넣고 싶다면, null 자리에는 넣을 값을 지정해 줘야 해요. 그때 null 병합 연산자 ??를 쓰면 되고요, 같은 목적으로 xref:System.Nullable`1.GetValueOrDefault(`0)?displayProperty=nameWithType 메서드도 쓸 수 있어요:

int? a = 28;
int b = a ?? -1;
Console.WriteLine($"b is {b}");  // output: b is 28

int? c = null;
int d = c ?? -1;
Console.WriteLine($"d is {d}");  // output: d is -1

null 대신 기본 값 형식의 default 값을 쓰고 싶다면 xref:System.Nullable`1.GetValueOrDefault?displayProperty=nameWithType 메서드를 이용해요.

nullable 값 형식을 비-nullable 형식으로 명시적으로 캐스팅할 수도 있어요. 다음 예제처럼요:

int? n = null;

//int m1 = n;    // Doesn't compile
int n2 = (int)n; // Compiles, but throws an exception if n is null

이 명시적 캐스트는 실행 시점에 nullable 값 형식의 값이 null이면 xref:System.InvalidOperationException을 던져요.

반대로, 비-nullable 값 형식 T는 대응하는 nullable 값 형식 T?로 암시적으로 변환돼요.

Lifted 연산자

nullable 값 형식 T?는 값 형식 T가 지원하는 미리 정의된 단항·이항 연산자나 오버로드된 연산자를 그대로 지원해요. lifted 연산자라고도 부르는 이 연산자들은 피연산자 중 하나라도 null이면 null을 반환해요. 그 외에는 피연산자의 들어 있는 값을 이용해 결과를 계산하죠. 예를 들면:

int? a = 10;
int? b = null;
int? c = 10;

a++;        // a is 11
a = a * c;  // a is 110
a = a + b;  // a is null

[!NOTE] bool? 형식의 경우 미리 정의된 &| 연산자는 이 섹션에서 설명한 규칙을 따르지 않아요. 피연산자 중 하나가 null이어도 연산 결과가 null이 아닐 수 있거든요. 자세한 내용은 부울 논리 연산자 문서의 Nullable 부울 논리 연산자 섹션을 보세요.

비교 연산자<, >, <=, >=의 경우, 피연산자 중 하나라도 null이면 결과는 false예요. 그 외에는 피연산자의 들어 있는 값을 비교해요. 특정 비교(예: <=)가 false를 반환한다고 해서 반대 비교(>)가 true를 반환한다고 단정하면 안 돼요. 다음 예제는 10이

  • null 이상(>=)도 아니고
  • null 미만(<)도 아님

을 보여줘요:

int? a = 10;
Console.WriteLine($"{a} >= null is {a >= null}");
Console.WriteLine($"{a} < null is {a < null}");
Console.WriteLine($"{a} == null is {a == null}");
// Output:
// 10 >= null is False
// 10 < null is False
// 10 == null is False

int? b = null;
int? c = null;
Console.WriteLine($"null >= null is {b >= c}");
Console.WriteLine($"null == null is {b == c}");
// Output:
// null >= null is False
// null == null is True

같음 연산자==의 경우, 두 피연산자가 모두 null이면 결과는 true, 둘 중 하나만 null이면 false예요. 그 외에는 들어 있는 값을 비교해요.

같지 않음 연산자!=는 두 피연산자가 모두 null이면 false, 둘 중 하나만 null이면 true를 반환하고, 그 외에는 들어 있는 값을 비교해요.

두 값 형식 사이에 사용자 정의 변환이 있다면, 대응하는 nullable 값 형식 사이에서도 같은 변환을 쓸 수 있어요.

Boxing과 unboxing

nullable 값 형식 T?의 인스턴스를 boxing할 때는 다음 규칙이 적용돼요.

값 형식 T의 boxed 값을 대응하는 nullable 값 형식 T?로 unboxing할 수 있어요. 다음 예제처럼요:

int a = 41;
object aBoxed = a;
int? aNullable = (int?)aBoxed;
Console.WriteLine($"Value of aNullable: {aNullable}");

object aNullableBoxed = aNullable;
if (aNullableBoxed is int valueOfA)
{
    Console.WriteLine($"aNullableBoxed is boxed int: {valueOfA}");
}
// Output:
// Value of aNullable: 41
// aNullableBoxed is boxed int: 41

nullable 값 형식 식별하기

xref:System.Type?displayProperty=nameWithType 인스턴스가 constructed nullable 값 형식, 즉 특정 타입 매개변수 T를 가진 xref:System.Nullable`1?displayProperty=nameWithType 형식을 나타내는지 알아내는 방법을 다음 예제가 보여줘요:

Console.WriteLine($"int? is {(IsNullable(typeof(int?)) ? "nullable" : "non nullable")} value type");
Console.WriteLine($"int is {(IsNullable(typeof(int)) ? "nullable" : "non-nullable")} value type");

bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;

// Output:
// int? is nullable value type
// int is non-nullable value type

예제에서 보듯 xref:System.Type?displayProperty=nameWithType 인스턴스를 만들 때는 typeof 연산자를 써요.

인스턴스가 nullable 값 형식인지 알아내겠다고 xref:System.Object.GetType*?displayProperty=nameWithType 메서드로 xref:System.Type 인스턴스를 얻어 앞의 코드로 테스트하면 안 돼요. nullable 값 형식의 인스턴스에 xref:System.Object.GetType*?displayProperty=nameWithType 메서드를 호출하면 그 인스턴스는 xref:System.Objectboxing될 텐데, null이 아닌 nullable 값 형식 인스턴스를 boxing하는 건 기본 형식의 값을 boxing하는 것과 같거든요. 그래서 xref:System.Object.GetType*는 nullable 값 형식의 기본 형식을 나타내는 xref:System.Type 인스턴스를 반환해요:

int? a = 17;
Type typeOfA = a.GetType();
Console.WriteLine(typeOfA.FullName);
// Output:
// System.Int32

또한 인스턴스가 nullable 값 형식인지 알아내는 데 is 연산자를 쓰지 마세요. 다음 예제처럼 is 연산자로는 nullable 값 형식 인스턴스와 기본 형식 인스턴스를 구분할 수 없어요:

int? a = 14;
if (a is int)
{
    Console.WriteLine("int? instance is compatible with int");
}

int b = 17;
if (b is int?)
{
    Console.WriteLine("int instance is compatible with int?");
}
// Output:
// int? instance is compatible with int
// int instance is compatible with int?

그 대신, 첫 번째 예제의 xref:System.Nullable.GetUnderlyingType*?displayProperty=nameWithType 메서드와 typeof 연산자를 조합해 인스턴스가 nullable 값 형식인지 확인해요.

[!NOTE] 이 섹션에서 설명한 메서드들은 nullable 참조 형식에는 적용되지 않아요.

C# 언어 사양

자세한 내용은 C# 언어 사양의 다음 섹션들을 보세요.

더 알아보기