형 변환과 변환 연산
형 변환과 변환 연산 (Casting and conversions, F#)
이 문서에서는 F#이 지원하는 형 변환(type conversion)에 대해 설명할게요. F#에서는 산술 형 사이의 변환, 열거형(enum)으로의 변환, 그리고 객체 상속 계층 안에서의 업캐스팅·다운캐스팅까지, 상황에 따라 서로 다른 변환 방식을 써요. 각각 언제 쓰고 어떻게 동작하는지 하나씩 살펴볼게요.
출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/casting-and-conversions
본문
산술 형 (Arithmetic Types)
F#은 정수형과 부동소수점 형 같은 다양한 기본 형 사이를 오가는 산술 변환용 연산자를 제공해요. 정수형과 char 변환 연산자에는 검사(check)된 형태와 검사되지 않은(unchecked) 형태가 있는 반면, 부동소수점 연산자와 enum 변환 연산자에는 그런 구분이 없어요. 검사되지 않은 형태는 FSharp.Core.Operators에, 검사된 형태는 FSharp.Core.Operators.Checked에 정의돼 있어요. 검사된 형태는 오버플로를 확인해서, 결과 값이 대상 형의 범위를 벗어나면 런타임 예외를 던져요.
이 연산자들은 각각 목적지 형의 이름과 같은 이름을 가져요. 예를 들어 아래 코드에서 형을 명시적으로 주석으로 달았을 때 byte가 두 가지 다른 의미로 쓰여요. 첫 번째는 형(type)이고, 두 번째는 변환 연산자예요.
let x : int = 5
let b : byte = byte x
F#에 정의된 변환 연산자는 다음과 같아요.
| 연산자 | 설명 |
|---|---|
byte |
8비트 부호 없는 형인 byte로 변환해요. |
sbyte |
부호 있는 byte로 변환해요. |
int16 |
16비트 부호 있는 정수로 변환해요. |
uint16 |
16비트 부호 없는 정수로 변환해요. |
int32, int |
32비트 부호 있는 정수로 변환해요. |
uint32 |
32비트 부호 없는 정수로 변환해요. |
int64 |
64비트 부호 있는 정수로 변환해요. |
uint64 |
64비트 부호 없는 정수로 변환해요. |
nativeint |
네이티브 정수로 변환해요. |
unativeint |
부호 없는 네이티브 정수로 변환해요. |
float, double |
64비트 배정밀도 IEEE 부동소수점 숫자로 변환해요. |
float32, single |
32비트 단정밀도 IEEE 부동소수점 숫자로 변환해요. |
decimal |
System.Decimal로 변환해요. |
char |
유니코드 문자인 System.Char로 변환해요. |
enum |
열거형으로 변환해요. |
내장 기본 형 외에도, 적절한 시그니처를 가진 op_Explicit이나 op_Implicit 메서드를 구현한 형에도 이 연산자들을 쓸 수 있어요. 예를 들어 int 변환 연산자는 형을 매개변수로 받아 int를 반환하는 정적 메서드 op_Explicit을 제공하는 어떤 형과도 동작해요. 일반적으로 메서드는 반환 형으로 오버로드할 수 없지만, op_Explicit과 op_Implicit은 그 예외로 허용돼요.
열거형 (Enumerated Types)
enum 연산자는 변환할 대상 열거형의 형을 나타내는 형 매개변수 하나를 받는 제네릭 연산자예요. 열거형으로 변환할 때 형 추론(type inference)이 목적지 열거형의 형을 알아내려고 해요. 아래 예시에서 변수 col1은 명시적으로 주석이 달리지 않았지만, 뒤의 같음(equality) 비교에서 형이 추론돼요. 그래서 컴파일러가 Color 열거형으로 변환하고 있다는 걸 알아낼 수 있어요. 대신 col2처럼 형 주석을 직접 달 수도 있어요.
type Color =
| Red = 1
| Green = 2
| Blue = 3
// The target type of the conversion cannot be determined by type inference, so the type parameter must be explicit.
let col1 = enum<Color> 1
// The target type is supplied by a type annotation.
let col2 : Color = enum 2
목적지 열거형의 형을 아래 코드처럼 형 매개변수로 명시적으로 지정할 수도 있어요.
let col3 = enum<Color> 3
열거형 캐스트는 열거형의 기반 형이 변환 대상 형과 호환될 때만 동작한다는 점을 기억하세요. 아래 코드는 int32와 uint32가 맞지 않아 컴파일에 실패해요.
// Error: types are incompatible
let col4 : Color = enum 2u
더 자세한 내용은 Enumerations를 참고하세요.
객체 형 캐스팅 (Casting Object Types)
객체 계층 안에서 형을 변환하는 일은 객체 지향 프로그래밍의 기본이에요. 변환에는 크게 두 종류가 있어요. 계층을 위로 캐스팅하는 업캐스팅(upcasting)과 아래로 캐스팅하는 다운캐스팅(downcasting)이에요. 계층을 위로 올리는 캐스팅은 파생 객체 참조에서 기본 객체 참조로 변환하는 거예요. 이 캐스팅은 파생 형의 상속 계층 안에 기본 클래스가 존재하기만 하면 성공하는 게 보장돼요. 반면 계층을 아래로 내리는 캐스팅, 즉 기본 객체 참조에서 파생 객체 참조로 변환하는 것은 객체가 실제로 올바른 목적지(파생) 형이거나 목적지 형에서 파생된 형의 인스턴스일 때만 성공해요.
F#은 이런 변환을 위한 연산자를 제공해요. :> 연산자는 계층을 위로 캐스팅하고, :?> 연산자는 계층을 아래로 캐스팅해요.
업캐스팅 (Upcasting)
많은 객체 지향 언어에서는 업캐스팅이 암시적이지만, F#의 규칙은 조금 달라요. 객체 형의 메서드에 인자를 전달할 때는 업캐스팅이 자동으로 적용돼요. 하지만 모듈의 let 바인딩 함수에서는 매개변수 형이 유연 형(flexible type)으로 선언되지 않는 한 자동 업캐스팅이 일어나지 않아요. 자세한 내용은 Flexible Types를 참고하세요.
:> 연산자는 정적 캐스트를 수행해요. 즉 캐스트의 성공 여부가 컴파일 시점에 결정되죠. :>를 쓰는 캐스트가 컴파일에 성공했다면 유효한 캐스트이고 런타임에 실패할 가능성이 없어요.
upcast 연산자로도 이런 변환을 할 수 있어요. 다음 표현식은 계층을 위로 올리는 변환을 지정해요.
upcast expression
upcast 연산자를 쓰면 컴파일러가 문맥에서 변환 대상 형을 추론하려고 해요. 컴파일러가 목적지 형을 알아내지 못하면 오류를 보고해요. 이때는 형 주석이 필요할 수 있어요.
다운캐스팅 (Downcasting)
:?> 연산자는 동적 캐스트를 수행해요. 즉 캐스트의 성공 여부가 런타임에 결정되죠. :?> 연산자를 쓰는 캐스트는 컴파일 시점에 검사되지 않지만, 런타임에 지정된 형으로 캐스트를 시도해요. 객체가 목적지 형과 호환되면 캐스트가 성공하고, 호환되지 않으면 런타임이 InvalidCastException을 던져요.
downcast 연산자로도 동적 형 변환을 할 수 있어요. 다음 표현식은 프로그램 문맥에서 추론된 형으로 계층을 아래로 내리는 변환을 지정해요.
downcast expression
upcast 연산자와 마찬가지로, 컴파일러가 문맥에서 특정 목적지 형을 추론하지 못하면 오류를 보고해요. 이때도 형 주석이 필요할 수 있어요.
아래 코드는 :>와 :?> 연산자의 사용법을 보여줘요. :?> 연산자는 변환이 성공할 거라고 확신할 때 쓰는 게 좋아요. 변환이 실패하면 InvalidCastException을 던지거든요. 변환이 성공할지 모르겠다면 match 식을 쓰는 형 테스트가 더 나아요. 예외를 만들어내는 오버헤드를 피할 수 있으니까요.
type Base1() =
abstract member F : unit -> unit
default u.F() =
printfn "F Base1"
type Derived1() =
inherit Base1()
override u.F() =
printfn "F Derived1"
let d1 : Derived1 = Derived1()
// Upcast to Base1.
let base1 = d1 :> Base1
// This might throw an exception, unless
// you are sure that base1 is really a Derived1 object, as
// is the case here.
let derived1 = base1 :?> Derived1
// If you cannot be sure that b1 is a Derived1 object,
// use a type test, as follows:
let downcastBase1 (b1 : Base1) =
match b1 with
| :? Derived1 as derived1 -> derived1.F()
| _ -> ()
downcastBase1 base1
제네릭 연산자인 downcast와 upcast는 인자와 반환 형을 결정할 때 형 추론에 의존해요. 그래서 앞선 예시의 let base1 = d1 :> Base1을 let base1: Base1 = upcast d1로 바꿔 쓸 수 있어요.
upcast만으로는 기본 클래스를 알아낼 수 없기 때문에 형 주석이 필요해요.
암시적 업캐스트 변환 (Implicit upcast conversions)
암시적 업캐스트는 다음 상황에서 자동으로 삽입돼요.
- 알려진 명명된 형을 가진 함수나 메서드에 매개변수를 제공할 때. 여기에는 계산 식(computation expressions)이나 슬라이싱 같은 구성 요소가 메서드 호출이 되는 경우도 포함돼요.
- 알려진 명명된 형을 가진 레코드 필드나 프로퍼티에 값을 할당하거나 변경할 때.
if/then/else나match식의 한 분기가 다른 분기나 전체적으로 알려진 형에서 비롯된 알려진 목적지 형을 가질 때.- 리스트, 배열, 시퀀스 식의 한 요소가 알려진 목적지 형을 가질 때.
예를 들어 다음 코드를 살펴볼게요.
open System
open System.IO
let findInputSource () : TextReader =
if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
// On Monday a TextReader
Console.In
else
// On other days a StreamReader
File.OpenText("path.txt")
여기서 조건식의 분기는 각각 TextReader와 StreamReader를 계산해요. 두 번째 분기에서 알려진 목적지 형은 메서드의 형 주석과 첫 번째 분기에서 비롯된 TextReader예요. 덕분에 두 번째 분기에는 업캐스트가 필요 없어요.
추가 암시적 업캐스트가 쓰이는 모든 지점에서 경고를 보여주려면 경고 3388(/warnon:3388 또는 속성 <WarnOn>3388</WarnOn>)을 켤 수 있어요.
암시적 숫자 변환 (Implicit numeric conversions)
F#은 대부분의 경우 숫자 형을 변환 연산자로 명시적으로 넓히는(widening) 방식을 사용해요. 예를 들어 int8이나 int16 같은 대부분의 숫자 형, 또는 float32에서 float64로, 그리고 출발·목적지 형 중 하나라도 알려지지 않은 경우에는 명시적 넓히기가 필요해요.
다만 32비트 정수를 64비트 정수로 넓히는 경우에는 암시적 업캐스트와 같은 상황에서 암시적 넓히기가 허용돼요. 전형적인 API 형태를 생각해볼게요.
type Tensor(…) =
static member Create(sizes: seq<int64>) = Tensor(…)
int64용 정수 리터럴을 쓸 수 있어요.
Tensor.Create([100L; 10L; 10L])
아니면 int32용 정수 리터럴도요.
Tensor.Create([int64 100; int64 10; int64 10])
형 추론 중에 출발·목적지 형이 모두 알려져 있을 때 int32에서 int64로, int32에서 nativeint로, int32에서 double로의 넓히기가 자동으로 일어나요. 그래서 앞선 예시 같은 경우에는 int32 리터럴을 그대로 쓸 수 있어요.
Tensor.Create([100; 10; 10])
암시적 숫자 넓히기가 쓰이는 모든 지점에서 경고를 보여주려면 경고 3389(/warnon:3389 또는 속성 <WarnOn>3389</WarnOn>)를 선택적으로 켤 수도 있어요.
.NET 스타일 암시적 변환 (.NET-style implicit conversions)
.NET API는 형 사이의 암시적 변환을 제공하기 위해 op_Implicit 정적 메서드를 정의할 수 있어요. F# 코드에서 메서드에 인자를 전달할 때 이런 변환은 자동으로 적용돼요. 예를 들어 op_Implicit 메서드를 명시적으로 호출하는 다음 코드를 볼게요.
open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants(XName.op_Implicit "Item")
.NET 스타일의 op_Implicit 변환은 출발 식과 목적지 형에 형이 모두 사용 가능할 때 인자 식에 자동으로 적용돼요.
open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants("Item")
.NET 스타일 암시적 변환이 쓰이는 모든 지점에서 경고를 보여주려면 경고 3395(/warnon:3395 또는 속성 <WarnOn>3395</WarnOn>)를 선택적으로 켤 수 있어요.
.NET 스타일의 op_Implicit 변환은 메서드 인자가 아닌 식에도 암시적 업캐스트와 같은 상황에서 자동으로 적용돼요. 다만 널리 또는 부적절하게 쓰이면 암시적 변환이 형 추론과 잘 맞지 않아 코드를 이해하기 어렵게 만들 수 있어요. 그래서 비(非)인자 위치에서 쓰일 때는 항상 경고가 발생해요.
.NET 스타일 암시적 변환이 메서드 인자가 아닌 식에 쓰이는 모든 지점에서 경고를 보여주려면 경고 3391(/warnon:3391 또는 속성 <WarnOn>3391</WarnOn>)을 켤 수 있어요.
변환 관련 경고 요약 (Summary of warnings related to conversions)
암시적 변환 사용에 대해 다음과 같은 선택적 경고가 제공돼요.
/warnon:3388(추가 암시적 업캐스트)/warnon:3389(암시적 숫자 넓히기)/warnon:3391(메서드 인자가 아닌 위치의op_Implicit, 기본적으로 켜짐)/warnon:3395(메서드 인자 위치의op_Implicit)
더 알아보기 (Learn more)
- F# Language Reference
- Enumerations —
enum변환에 대해 더 알아볼 수 있어요. - Flexible Types — 업캐스팅이 자동으로 일어나는 조건에 대해 더 알아볼 수 있어요.