F# Nullable Reference Types
F# Nullable Reference Types
F#는 원래 null을 거의 쓰지 않는 언어예요. 그런데 다른 .NET 언어로 작성된 라이브러리와 맞물려 일하게 되면 null 값을 만날 수밖에 없죠. 이 글에서는 F#에서 null 값이 어떻게 다뤄져 왔는지 살펴본 다음, F# 9에서 새로 추가된 nullable reference types 기능(타입에 | null을 붙이는 문법)을 이제부터 차근차근 설명해 드릴게요. 핵심은 간단해요. "이 타입은 null일 수 있다"는 사실을 컴파일러에 알려주면, 컴파일러가 null을 안전하게 다루도록 도와주는 겁니다.
출처: Null Values - F# Guide (Microsoft Learn) (원문은 "Null Values" 페이지이며, 이 글은 해당 본문을 번역한 것입니다)
본문
F# 9 이전의 null 값
F#에서는 값이나 변수에 null을 평범하게 쓰지 않아요. 하지만 null은 특정 상황에서 "비정상적인 값"으로 등장하곤 하죠. F#으로 정의된 타입이라면, AllowNullLiteral 특성이 적용되지 않는 한 null이 정상적인 값으로 허용되지 않습니다. 반면 다른 .NET 언어로 정의된 타입이라면 null이 가능한 값이고, 그런 타입과 상호 운용(interop)할 때 F# 코드에서 null 값을 만날 수 있어요.
F#으로 정의되고 F#에서만 쓰이는 타입은, F# 라이브러리를 직접 이용해서 null 값을 만들려면 Unchecked.defaultof나 Array.zeroCreate를 써야만 해요. 그런데 F# 타입이라도 다른 .NET 언어에서 사용되거나, .NET Framework처럼 F#으로 작성되지 않은 API에서 쓰이면 null 값이 발생할 수 있답니다.
다른 .NET 언어에서 "null일 수 있는 참조 변수"를 쓸 자리에, F#에서는 option 타입을 쓸 수 있어요. 객체가 없을 때는 None, 객체가 있을 때는 Some(obj)을 사용합니다. 자세한 내용은 Options 문서를 참고하세요. 잠깐, 주의할 점이 하나 있어요. Some x에서 x가 우연히 null이라면, null 값을 Option에 담을 수도 있어요. 그래서 값이 null일 때는 반드시 None을 쓰는 게 중요합니다.
null 키워드는 F#에서 유효한 키워드이고, .NET Framework API나 다른 .NET 언어로 작성된 API를 다룰 때는 꼭 사용해야 해요. null 값이 필요해지는 상황은 크게 두 가지예요. 첫째, .NET API를 호출하면서 인자로 null을 전달하는 경우. 둘째, .NET 메서드 호출 결과(반환 값이나 출력 매개변수)로 받은 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 값은 다른 방법으로도 만들어질 수 있어요. 예를 들어 Array.zeroCreate는 내부적으로 Unchecked.defaultof를 호출하는데, 이때 null이 생성되죠. 이런 코드는 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 전처리기 지시문을 정의해요. 관련된 플래그와 전처리기 지시문에 대한 내용은 각각 compiler-options와 compiler-directives를 참고하세요.
null 허용 여부를 명시적으로 선택하려면, 타입 선언에 새로운 문법을 접미사로 붙여요.
type | null
막대 기호 |는 문법에서 논리적 OR를 뜻해요. 즉 "원래 타입"과 "nullable 참조"라는 서로 겹치지 않는 두 타입 집합의 합집합을 만드는 거죠. 이 기호는 F#의 구별된 공용체(discriminated union)에서 여러 case를 선언할 때 쓰는 기호와 동일해요. type AB = A | B가 "A 또는 B"를 뜻하듯이 말이에요.
| null nullable 표기는 참조 타입이 평소 쓰이는 모든 자리에서 사용할 수 있어요.
- 공용체 타입, 레코드 타입, 커스텀 타입의 필드
- 기존 타입에 대한 타입 별칭(type alias)
- 제네릭 타입의 타입 적용(type application)
- let 바인딩, 매개변수, 반환 타입에 대한 명시적 타입 표기
- 멤버, 프로퍼티, 필드 같은 객체 지향 프로그래밍 구조에 대한 타입 표기
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)
패턴 매칭에서 |는 서로 다른 매칭 절을 구분하는 데 사용돼요.
match x with
| ?: string | null -> ...
이 코드는 사실 아래처럼, 먼저 string 타입에 대한 타입 테스트를 수행하고 이어서 null을 처리하는 별도의 절을 두는 것과 동등해요.
match x with
| ?: string
| null -> ...
중요
null관련 추가 기능들은 상호 운용(interop)을 목적으로 언어에 추가된 것이에요. "정보가 없다"는 뜻을 나타내기 위해 F# 타입 모델링에| null을 쓰는 것은 관용적(idiomatic)이지 않아요. 그런 용도에는 위에서 설명한 대로 option을 쓰는 게 바람직합니다. null 관련 관례에 대한 더 자세한 내용은 스타일 가이드의 conventions를 읽어보세요.