Null Values

Null Values (null 값)

F#에서는 null을 값이나 변수의 보통 값으로 잘 쓰지 않아요. 하지만 다른 .NET 언어로 만든 타입이나 API를 만나면 null이 예상치 못한 값으로 등장하곤 하죠. 이 글에서는 F# 9 이전과 이후로 나눠서, null을 언제 만나게 되고 어떻게 다뤄야 하는지 함께 살펴볼게요.

출처: Null Values - F# (Microsoft Learn)

본문

F# 9 이전의 null

null은 F#에서 값이나 변수의 보통 값으로 쓰이지 않아요. 다만 특정 상황에서는 이상한 값(abnormal value)처럼 나타나는데요. F#으로 정의한 타입이라면 [AllowNullLiteral](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-allownullliteralattribute.html) 특성을 적용하지 않는 이상 null을 정식 값으로 허용하지 않아요. 반면 다른 .NET 언어로 정의한 타입은 null이 가능한 값이라서, 그런 타입과 상호 운용(interop)할 때 F# 코드에서 null을 만날 수 있어요.

F#으로 정의하고 F#에서만 쓰는 타입이라면, F# 라이브러리만으로 null 값을 만드는 방법은 Unchecked.defaultofArray.zeroCreate를 쓰는 것뿐이에요. 하지만 그 F# 타입을 다른 .NET 언어에서 쓰거나, .NET Framework처럼 F#으로 작성되지 않은 API에서 그 타입을 쓴다면 null이 나타날 수 있어요.

다른 .NET 언어에서 null일 수 있는 참조 변수를 쓸 자리에, F#에서는 option 타입을 쓰면 돼요. 객체가 없다면 null 대신 None을, 객체가 있다면 Some(obj)를 쓰는 거죠. 자세한 내용은 Options 문서를 참고해 주세요. 다만 Some x에서 x가 실제로 null이면 option에 null이 담길 수도 있어요. 그러니 값이 null이라면 반드시 None을 쓰는 게 중요해요.

null 키워드는 F#의 유효한 키워드로, .NET Framework API나 다른 .NET 언어로 작성된 API를 다룰 때는 꼭 써야 해요. null 값이 필요한 상황은 크게 두 가지예요. 하나는 .NET API를 호출하면서 인자로 null을 넘길 때, 다른 하나는 .NET 메서드 호출 결과나 출력 매개변수(out parameter)에 null이 들어왔는지 해석할 때예요.

.NET 메서드에 null을 넘기려면 호출 코드에서 그냥 null 키워드를 쓰면 돼요. 다음 코드가 그 예시예요.

open System

// Pass a null value to a .NET method.
let ParseDateTime (str: string) =
    let (success, res) =
        DateTime.TryParse(str, null, System.Globalization.DateTimeStyles.AssumeUniversal)

    if success then Some(res) else None

.NET 메서드에서 받은 null 값을 해석할 때는 가능하면 패턴 매칭을 쓰세요. 아래 코드는 입력 스트림의 끝을 지나서 읽으려 할 때 ReadLine이 반환하는 null을 패턴 매칭으로 처리하는 모습이에요.

// Open a file and create a stream reader.
let fileStream1 =
    try
        System.IO.File.OpenRead("TextFile1.txt")
    with :? System.IO.FileNotFoundException ->
        printfn "Error: TextFile1.txt not found."
        exit (1)

let streamReader = new System.IO.StreamReader(fileStream1)

// ProcessNextLine returns false when there is no more input;
// it returns true when there is more input.
let ProcessNextLine nextLine =
    match nextLine with
    | null -> false
    | inputString ->
        match ParseDateTime inputString with
        | Some(date) -> printfn "%s" (date.ToLocalTime().ToString())
        | None -> printfn "Failed to parse the input."

        true

// A null value returned from .NET method ReadLine when there is
// no more input.
while ProcessNextLine(streamReader.ReadLine()) do
    ()

F# 타입의 null 값은 다른 방식으로도 생길 수 있어요. 예를 들어 Unchecked.defaultof를 호출하는 Array.zeroCreate를 쓰면 그렇죠. 이런 코드는 null이 밖으로 새지 않도록 잘 감싸 두는 게 중요해요. F# 전용으로 쓰는 라이브러리라면 모든 함수에서 null을 검사할 필요는 없어요. 하지만 다른 .NET 언어와 상호 운용을 위한 라이브러리를 만든다면, C#이나 Visual Basic 코드처럼 null 입력 매개변수를 검사하고 ArgumentNullException을 던지는 검사를 추가해야 할 수도 있어요.

임의의 값이 null인지 확인하려면 다음 코드를 쓰면 돼요.

match box value with
| null -> printf "The value is null."
| _ -> printf "The value is not null."

F# 9부터의 null

F# 9에서는 null을 값으로 가질 수 있는 참조 타입을 다루기 위한 기능이 언어에 추가됐어요. 이 기능은 기본적으로 꺼져 있는데, 켜려면 프로젝트 파일에 다음 속성을 넣어야 해요.

<Nullable>enable</Nullable>

이 속성을 넣으면 F# 컴파일러에 --checknulls+ 플래그가 전달되고, 빌드에 NULLABLE 전처리기 지시문이 설정돼요.

null 허용을 명시적으로 선택하려면(opt-in) 타입 선언에 다음 새 구문을 붙이면 돼요.

type | null

여기서 막대 기호 |는 구문상 논리합(OR)을 뜻해요. 즉 기본 타입과 nullable 참조라는, 서로 겹치지 않는 두 타입 집합의 합집합을 만드는 거예요. 이 기호는 F# 구별된 공용체(discriminated union)에서 여러 case를 선언할 때 쓰는 type AB = A | B(A이거나 B)와 같은 기호예요.

| null 주석(nullable annotation)은 참조 타입을 보통 쓰는 모든 자리에서 사용할 수 있어요.

  • 공용체 타입, 레코드 타입, 사용자 지정 타입의 필드
  • 기존 타입에 대한 타입 별칭(type alias)
  • 제네릭 타입의 타입 적용(type application)
  • let 바인딩, 매개변수, 반환 타입에 대한 명시적 타입 주석
  • 멤버(member), 속성(property), 필드(field) 같은 객체 지향 구성 요소의 타입 주석
type AB = A | B
type AbNull = AB | null

type RecordField = { X: string | null }
type TupleField = string * string | null

type NestedGenerics = { Z : List<List<string | null> | null> | null }

막대 기호 |는 F#에서 다른 용도로도 쓰여서 구문상 모호함이 생길 수 있어요. 그럴 때는 null 주석이 붙은 타입을 괄호로 감싸면 돼요.

// Unexpected symbol '|' (directly before 'null') in member definition
type DUField = N of string | null

같은 타입을 ( ) 괄호 한 쌍으로 감싸면 문제가 해결돼요.

type DUField = N of (string | null)

패턴 매칭에서 |는 서로 다른 매칭 절(clause)을 구분할 때 쓰여요.

match x with
| ?: string | null -> ...

이 코드 조각은 사실상 먼저 string 타입에 대한 타입 검사를 하고, 그 다음 null을 처리하는 별도 절을 두는 다음 코드와 같아요.

match x with
| ?: string 
| null -> ...

[!IMPORTANT] null 관련 추가 기능은 상호 운용을 위해 언어에 더해진 거예요. F# 타입 모델링에서 | null을 쓰는 것은 '정보가 없다'는 뜻을 나타내는 관용적인 방식이 아니에요. 그런 용도라면 (앞서 설명한 대로) option을 쓰세요. null 관련 관례는 스타일 가이드에서 더 자세히 다뤄요.

더 알아보기