반응형
1) 왜 생겼는가 — “값 형식도 값이 없을 수 있다”
- 문제 배경: 기본값(Value type)은 원래 null을 가질 수 없어요. 예를 들어 int는 0, 1, -5 같은 “숫자”만 가능하고 “값 없음”을 표현할 방법이 없었습니다.
- 필요성: DB의 NULL, 폼 입력의 미입력, 센서값 미수집 같은 “없음” 상태를 정확히 모델링해야 할 때가 많습니다.
- 해결: T?(여기서 T는 값 형식) 를 도입해 null을 담을 수 있게 했습니다. 즉, int?, bool?, DateTime? 처럼 “값이거나(null이 아닐 때) 없다(null일 때)”를 표현합니다.
2) 무엇인가 — T?는 사실 Nullable<T>의 문법 설탕
- 정의: T?는 값 형식 T에 대해 Nullable<T>로 컴파일됩니다.
- 구성: Nullable<T>는
- HasValue : 값이 있는지 여부(bool)
- Value : 실제 값(T) — 단, 값이 없으면 접근 시 예외
- GetValueOrDefault() : 값이 있으면 그 값, 없으면 default(T) 반환
- 주의 (이름이 비슷한 다른 것들)문법 의미
T? (값 형식에만) Nullable<T> (예: int?) T? (참조 형식에) nullable reference type 주석(경고/분석용, 런타임 표현 아님) ?. null-조건부 연산자(안전 호출) ?? / ??= null 병합 / null 병합 할당
3) 언제/어떻게 쓰는가 — 사용 패턴과 미니 예제(MWE)
3-1. 기본 선언과 사용
int? a = null; // 값이 없음을 표현
int? b = 10; // 값이 있음
Console.WriteLine(a.HasValue); // False
Console.WriteLine(b.HasValue); // True
// 안전한 꺼내기
int x = b.GetValueOrDefault(); // 10
int y = a.GetValueOrDefault(); // 0 (int의 default)
// 빈 값에 Value 접근하면 예외! (InvalidOperationException)
// int z = a.Value; // ❌ 조심
3-2. null 병합 연산자 ??로 기본값 주기
int? readCount = null;
// 값이 없으면 0을 사용
int count = readCount ?? 0; // 0
3-3. 패턴 매칭으로 값 추출 (가독성/안전성 ↑)
int? maybeAge = 29;
if (maybeAge is int age) // 값이 있으면 묶어서 꺼냄
{
Console.WriteLine($"성인 여부: {age >= 18}");
}
3-4. 연산 “리프팅(lifting)”
- 산술/비교/논리 연산은 둘 중 하나라도 null이면 결과가 null(또는 규칙에 따라) 이 되도록 자동 “리프팅”됩니다.
int? p = 3;
int? q = null;
int? sum = p + q; // null
bool? cmp = p > q; // null (q가 없음)
3-5. 메서드/DB 모델에서의 실전 사용
public record UserDto(int Id, string Name, DateTime? Birthday);
// 생일이 없을 수도 있는 사용자를 받는다
void SaveUser(UserDto user)
{
// DB 컬럼이 NULL 허용이면 그대로 매핑
var birthday = user.Birthday?.Date; // null이면 여기서도 null
}
3-6. 컬렉션과의 조합
var scores = new List<int?> { 10, null, 30 };
// null을 제외한 평균
double avg = scores.Where(s => s.HasValue).Average(s => s!.Value);
4) 자주 하는 실수와 베스트 프랙티스
✅ 체크리스트
- “참조형 ?”와 “값형 ?” 구분: string?은 정적분석용 주석, int?는 진짜 Nullable<int>.
- Value 직접 접근 지양: HasValue 확인 후 접근하거나 ??, GetValueOrDefault, 패턴 매칭 사용.
- 연산 리프팅 이해: 피연산자 중 null이 섞이면 결과가 null일 수 있음.
- API 경계에서 명확히: DTO/DB 모델에 “없을 수 있음”을 T?로 표현.
- 컬렉션 타입 신중히: List<int?> vs List<int> 의미가 다름.
- 박싱 주의(성능): Nullable<T>를 object로 캐스팅하면 값이 있으면 박싱, 없으면 null 참조가 됩니다.
- 기본값 초기화 전략: ??로 디폴트 제공, 혹은 required/검증 로직 병행.
흔한 실수들
- ?.와 T? 혼동
- obj?.Prop는 “안전 호출” 문법이고, int?는 타입 자체에 null을 허용하는 것—전혀 다른 개념입니다.
- Value 남용으로 예외
- a.Value 전에 반드시 a.HasValue 검사하거나, 더 간단히 a ?? 기본값 사용하세요.
int safe = a ?? -1; // 가장 안전하고 간결
- Nullable Reference Type 경고와 뒤섞기
- 컴파일러의 NRT 경고(예: string?)는 설계 의도와 사용 계약을 명확히 하는 용도입니다. 값 형식의 T?와는 별개로 생각하세요.
- 연산 결과가 null이 되는 걸 간과
- 예: int? sum = price + tax;에서 둘 중 하나라도 null이면 sum은 null. 이후 로직에서 ?? 0으로 기본값을 부여할지 결정하세요.
소소한 팁(가독성/성능/안전)
- 패턴 매칭 우선: if (n is int v)가 HasValue/Value보다 읽기 좋아요.
- 초기 변환 시점 분리: 입력 직후 var amount = input.Amount ?? 0;로 비즈니스 로직에서 null 분기를 줄이세요.
- LINQ에서 Select보다 Where를 먼저: Where(x => x.HasValue).Select(x => x.Value)가 NRE를 방지합니다.
- 박싱 줄이기: 성능 민감 코드에선 object 캐스팅을 피하고 제네릭 경로 유지 (where T : struct)를 고려하세요.
보너스: 미니 종합 예제
using System;
using System.Collections.Generic;
using System.Linq;
public static class Demo
{
public static void Main()
{
int? read = null;
int total = 100 + (read ?? 0); // null이면 0으로 합산
Console.WriteLine(total); // 100
int? maybe = 42;
if (maybe is int value)
{
Console.WriteLine(value * 2); // 84
}
var xs = new List<int?> { 1, null, 3, 5 };
double avg = xs.Where(x => x.HasValue).Average(x => x!.Value);
Console.WriteLine(avg); // 3
}
}
반응형
'C#' 카테고리의 다른 글
“C# readonly: 생성 후 불변을 보장해 버그를 줄이고 성능을 챙기는 설계 키워드” (0) | 2025.10.10 |
---|---|
“IDisposable: 가비지 컬렉터가 다 못하는 ‘자원 반납’을 결정적으로 보장하는 계약” (0) | 2025.10.10 |
C# “??는 ‘null이면 우변’ — 안전한 기본값 지정의 한 줄 공식” (0) | 2025.10.10 |
error. 이벤트핸들러 이름 충돌 (0) | 2025.09.22 |
volatile는 멀티스레드에서 “가벼운 동기화”를 위한 읽기/쓰기 가시성 보장 장치 (0) | 2025.09.19 |
왜 IDisposable을 구현하나요? (0) | 2025.09.19 |
“직접 델리게이트 콜백” → “.NET 표준 이벤트 패턴(Event + EventArgs)” (0) | 2025.09.19 |
sealed를 왜 써야 할까? (0) | 2025.09.19 |