Nullable 참조 형식
값이 없을 수 있는 자리를 컴파일러가 함께 지키게 합니다.
null 일 수 있는지를 형식에 적습니다
참조 형식은 어느 것이든 null 이 될 수 있었습니다.
string name 이라고 적어도 그 자리에 null 이 들어올 수
있고, 컴파일러는 아무 말도 하지 않았습니다.
1.10의 NullReferenceException 은 대개 그렇게
발생합니다.
string name = FindName(id); // 못 찾으면 null 을 돌려줍니다 Console.WriteLine(name.Length); // 여기서 NullReferenceException
이 코드에는 잘못된 곳이 보이지 않습니다.
FindName 이 null 을 돌려줄 수 있다는 것이 선언
어디에도 적혀 있지 않기 때문입니다. 문서를 읽거나 구현부를 열어 보아야
알 수 있고, 대개는 예외가 발생하고 나서야 알게 됩니다.
nullable 참조 형식은 그것을 형식에 적게 합니다(C# 8.0).
1.3의
int? 와 같은 모양으로, 참조 형식에도 물음표를 붙입니다.
string name; // null 이 들어오지 않습니다 string? nick; // null 일 수 있습니다
물음표가 붙은 것을 확인 없이 사용하면 컴파일러가 경고합니다. 다만 경고일 뿐 실행을 막지는 않습니다. 이 문법이 무엇을 해 주고 무엇은 해 주지 않는지가 이 단원의 절반입니다.
물음표가 붙은 값 다루기
아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.
string name = "홍길동"; string? nick = null; // nick.Length 라고 바로 적으면 CS8602 경고가 납니다. // 확인하고 나면 그 안에서는 경고가 없습니다. if (nick != null) Console.WriteLine(nick.Length); else Console.WriteLine("별명이 없습니다"); // 확인 대신 기본값을 정해도 됩니다(1.4 의 ??) Console.WriteLine((nick ?? "(없음)").ToUpper()); // ?. 는 앞이 null 이면 건너뜁니다. 결과는 int? 가 됩니다. int? len = nick?.Length; Console.WriteLine(len is null ? "길이를 모릅니다" : $"길이 {len}"); // 물음표가 없는 name 은 확인 없이 사용합니다 Console.WriteLine($"{name} / {name.Length}");
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
경고가 하나도 없는 코드입니다.
nick 을 사용하는 세 자리가 모두 null 을 먼저 처리했고,
name 은 물음표가 없으니 확인할 것이 없습니다. 확인을
빠뜨린 자리만 컴파일러가 짚어 준다는 것이 이 문법의 용도입니다.
켜져 있어야 동작합니다
이 검사는 프로젝트 설정으로 켭니다.
1.1에서 본 .csproj 의
Nullable 이 그 자리입니다.
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup>
.NET 6 이후 dotnet new 로 만든 프로젝트에는 이 줄이 이미
들어 있습니다. 파일 하나만 다르게 하려면 코드 안에서 켜고 끕니다.
#nullable disable // 아직 손대지 못한 옛 코드입니다. 경고가 나지 않습니다. #nullable enable // 여기서부터 다시 검사합니다.
이미 있는 코드에 이 검사를 켜면 경고가 수백 개 쏟아집니다. 한꺼번에
고치기 어려우면 프로젝트 전체를 켜 두고
#nullable disable 로 아직 손대지 못한 파일을 잠시 빼
두는 방식으로 옮겨 갑니다.
확인하고 나면 경고가 사라집니다
컴파일러는 코드의 흐름을 따라가며 각 자리에서 이 변수가 null 일 수 있는지를 다시 판단합니다. 그래서 같은 변수라도 확인한 뒤에는 경고하지 않습니다.
string? nick = Find(id); Console.WriteLine(nick.Length); // CS8602: null 가능 참조에 대한 // 역참조입니다.
string? nick = Find(id); if (nick is not null) Console.WriteLine(nick.Length); // 경고 없음 — 이 안에서 nick 은 // null 이 아닙니다
!= null·is not null·??·?.
를 모두 인식하고, return 이나
throw 로 먼저 빠져나가는 것도 알아봅니다.
static int Length(string? text) { if (text is null) return 0; // 여기까지 왔다면 null 이 아닙니다. 경고가 없습니다. return text.Length; }
다만 사이에 값이 바뀌면 다시 처음부터 봅니다. 확인한 뒤에 다른 값을 담았다면 그 자리부터는 또 null 일 수 있는 것으로 판단합니다.
자주 만나는 경고
번호는 다르지만 짚는 것은 하나입니다. null 일 수 있는 값이 null 이 아니어야 하는 자리에 갔다는 것입니다.
| 번호 | 언제 나는가 | 예시 |
|---|---|---|
| CS8602 | null 가능 참조에 대한 역참조입니다 | nick.Length |
| CS8600 | 가능한 null 값을 null 을 허용하지 않는 형식으로 변환 | string s = nick; |
| CS8604 | 가능한 null 참조 인수입니다 | Take(nick) |
| CS8603 | 가능한 null 참조 반환입니다 | return nick; |
| CS8625 | null 리터럴을 null 을 허용하지 않는 참조 형식으로 | Take(null) |
| CS8618 | 생성자를 종료할 때 null 이 아닌 값을 포함해야 합니다 | 채우지 않은 속성 |
고치는 방법도 둘 중 하나입니다. 정말 null 이 올 수 있다면 받는 쪽 형식에 물음표를 붙이고, 올 수 없다면 그것을 코드로 보여 주면 됩니다. 경고를 없애는 것이 목적이 아니라 어느 쪽인지 정하는 것이 목적입니다.
생성자를 마칠 때까지 채워야 합니다
CS8618 은 조금 다른 자리에서 납니다. 물음표 없는 속성을
선언해 놓고 객체를 만드는 동안 아무것도 담지 않으면 만들어진 직후에
그 속성이 null 이기 때문입니다.
class Member { public string Name { get; set; } // CS8618 // null을 허용하지 않는 속성 'Name'은(는) 생성자를 종료할 때 // null이 아닌 값을 포함해야 합니다. }
메우는 방법은 셋입니다. 2.3에서 본 것들입니다.
// ① 기본값을 정해 둡니다 public string Name { get; set; } = ""; // ② 생성자에서 받습니다 public string Name { get; } public Member(string name) => Name = name; // ③ 담지 않으면 못 만들게 합니다(C# 11.0) public required string Name { get; set; } // 정말 없을 수 있는 값이라면 물음표를 붙이는 것이 답입니다 public string? Nick { get; set; }
! 는 마지막 수단입니다
null 허용 연산자 ! 는 이 자리는 null 이
아니라고 컴파일러에게 알리는 표시입니다.
검사를 추가하는 것이 아니라 경고만 없앱니다.
string? nick = null; Console.WriteLine(nick!.Length); // 경고 없음 // 실행하면 // Unhandled exception. System.NullReferenceException: // Object reference not set to an instance of an object.
경고는 사라졌지만 실행하면 1.10 에서 본 그 예외가 그대로 발생합니다.
! 를 붙였다는 것은 내가 책임진다는 뜻이고, 틀리면
컴파일러는 아무것도 해 주지 않습니다.
사용할 자리가 아주 없지는 않습니다. 컴파일러가 알 수 없는 것을 내가
확실히 아는 경우가 있습니다. 그럴 때도 ! 를 흩뿌리지 말고
왜 null 이 아닌지를 주석으로 남기는 편이 낫습니다. 경고가 나올
때마다 ! 를 붙이면 검사를 켜 둔 뜻이 없어집니다.
언제 null 이 아닌지 알려 주기
3.2의
TryGetValue 를 떠올려 보세요. 실패하면
out 변수가 null 이고 성공하면 null 이 아닙니다.
돌려주는 bool 과 out
변수가 이어져 있다는 것은 컴파일러가 스스로 알 수 없습니다.
using System.Diagnostics.CodeAnalysis; static bool TryFind(string key, [NotNullWhen(true)] out string? value) { value = key == "홍길동" ? "gildong" : null; return value != null; } if (TryFind("홍길동", out var found)) Console.WriteLine(found.ToUpper()); // 경고 없음
[NotNullWhen(true)] 이 그 연결을 적어 둔 것입니다.
true 가 나온 가지에서는
value 가 null 이 아니라고 컴파일러가 판단합니다.
int.TryParse 와
Dictionary.TryGetValue 에 이미 이 표시가 붙어 있어서
지금까지 경고 없이 사용할 수 있었습니다.
값 형식의 물음표와는 다릅니다
모양은 같지만 속은 완전히 다릅니다.
int? 는 실제로 다른 형식이고,
string? 는 컴파일러에게만 보이는 표시입니다.
Console.WriteLine(typeof(int?)); // System.Nullable`1[System.Int32] — 실제로 있는 형식입니다 string? s = "가"; Console.WriteLine(s.GetType()); // System.String — 물음표는 남아 있지 않습니다 Console.WriteLine(typeof(string?)); // CS8639: typeof 연산자는 nullable 참조 형식에 사용할 수 없습니다.
따라오는 것이 셋 있습니다.
- 실행 중에는 검사되지 않습니다. 물음표 없는 매개 변수에도 리플렉션이나 역직렬화를 거치면 null 이 들어올 수 있습니다.
- 검사를 켜지 않은 코드에서 넘어오는 값은 모릅니다. 옛 라이브러리를 부르면 컴파일러가 경고도 보증도 하지 않습니다.
- 바깥에서 들어오는 값은 여전히 직접 확인해야 합니다. 입력·설정 파일·데이터베이스에서 온 값이 그렇습니다.
int? 는 실행할 때도 남아 있는 형식이라
HasValue 로 물어볼 수 있고,
string? 는 컴파일할 때만 사용되고 사라지는
표시입니다. 같은 물음표지만 하는 일이 다릅니다.
이 문법, 몇 버전부터 사용할 수 있나요
- 2.0int? — nullable 값 형식
- 6.0?. — null 이면 건너뛰는 연산자
- 8.0nullable 참조 형식 · ! 연산자 · ??=
- 9.0is not null 패턴
- 11.0required — 담지 않으면 못 만들게
C# 8.0의 이 변화는 문법을 추가한 것이 아니라 이미 있던 형식에 뜻을
붙인 것입니다. 그전까지 string 은
null 일 수도 있는 문자열이었고, 8.0부터는 null 이 아닌 문자열이 됩니다.
같은 코드의 뜻이 달라지는 일이라 설정으로 켜고 끄게 만들었습니다.
[NotNullWhen] 같은 표시는 언어가 아니라
.NET Core 3.0의 클래스 라이브러리에 들어 있습니다.
직접 해보기
아래 코드에는 경고가 셋 납니다. 각각 어느 줄에서 왜 나는지 말하고
고쳐보세요. ! 는 사용하지 않습니다.
static string FindNick(Member[] members, string name) { foreach (var m in members) if (m.Name == name) return m.Nick; return null; } class Member { public string Name { get; set; } public string? Nick { get; set; } }
1번의 FindNick 은 그런 회원이 없는 경우와
회원은 있는데 별명을 정하지 않은 경우가 모두 null 이라 구분되지 않습니다.
[NotNullWhen(true)] 을 사용해
TryFind 를 만들고 둘을 나누어 출력해보세요.
- 참조 형식에 물음표를 붙여 null 이 올 수 있는지를 형식에 적습니다(C# 8.0). 확인을 빠뜨린 자리를 컴파일러가 짚어 줍니다.
<Nullable>enable</Nullable>로 켜고,#nullable disable로 파일 단위로 잠시 뺄 수 있습니다.- 컴파일러가 흐름을 따라가며 판단하므로 한 번 확인한 뒤에는 경고하지 않습니다.
!는 경고만 없앨 뿐 검사를 추가하지 않습니다. 틀리면 NullReferenceException 이 그대로 발생합니다.bool과out이 이어져 있으면[NotNullWhen(true)]로 알려 줍니다.string?는 컴파일할 때만 사용되고 사라지는 표시입니다. 바깥에서 들어오는 값은 여전히 직접 확인해야 합니다.
C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.