F# 업캐스팅과 다운캐스팅

F# 업캐스팅과 다운캐스팅 (Upcasting and Downcasting)

객체 지향 프로그래밍에서 객체 계층 구조 사이의 형식 변환은 아주 기본적인 작업이에요. F#에서는 객체 참조를 계층의 '위'로 올리는 캐스팅(업캐스팅)을 :> 연산자로, 계층의 '아래'로 내리는 캐스팅(다운캐스팅)을 :?> 연산자로 다뤄요. 이 글에서는 이 두 캐스팅이 언제 안전하고 언제 조심해야 하는지, 그리고 어떤 상황에서 형식이 자동으로 캐스팅되는지를 하나씩 살펴볼게요.

출처: Casting and conversions - F# | Microsoft Learn

본문

캐스팅: 객체 형식 변환

객체 계층 구조 사이의 형식 변환에는 두 가지 기본 유형이 있어요. 하나는 계층을 위로 올리는 캐스팅(업캐스팅), 다른 하나는 계층을 아래로 내리는 캐스팅(다운캐스팅)이죠.

업캐스팅은 파생(derived) 객체 참조를 기본(base) 객체 참조로 바꾸는 걸 말해요. 기본 클래스가 파생 클래스의 상속 계층 안에 있기만 하면 이 캐스팅은 반드시 성공해요. 반면 다운캐스팅, 즉 기본 객체 참조를 파생 객체 참조로 바꾸는 것은 객체가 실제로 목표(파생) 형식의 인스턴스이거나 목표 형식에서 파생된 형식의 인스턴스일 때만 성공해요.

F#은 이런 변환을 위한 연산자를 제공해요. :> 연산자가 계층을 위로 캐스팅하고, :?> 연산자가 계층을 아래로 캐스팅해요.

업캐스팅

많은 객체 지향 언어에서는 업캐스팅이 묵시적으로 일어나요. F#은 규칙이 조금 달라요. 객체 형식의 메서드에 인자를 넘길 때는 업캐스팅이 자동으로 적용되지만, 모듈 안의 let 바인딩 함수에서는 매개 변수 형식이 유연한 형식(flexible type)으로 선언되지 않는 한 자동으로 일어나지 않아요. 자세한 내용은 Flexible Types에서 다룰게요.

:> 연산자는 정적(static) 캐스트를 수행해요. 즉 캐스트의 성공 여부가 컴파일 시점에 결정되죠. :>를 쓰는 캐스트가 컴파일을 통과했다면 그건 유효한 캐스트라서 실행 시점에 실패할 일이 없어요.

이런 변환에는 upcast 연산자를 쓸 수도 있어요. 다음 식은 계층을 위로 올리는 변환을 나타내요.

upcast expression

upcast 연산자를 쓰면 컴파일러가 문맥에서 변환할 대상 형식을 추론하려고 해요. 대상 형식을 정하지 못하면 오류를 알리고, 이때는 형식 주석(type annotation)이 필요할 수 있어요.

다운캐스팅

:?> 연산자는 동적(dynamic) 캐스트를 수행해요. 이 말은 캐스트의 성공 여부가 실행 시점에 결정된다는 뜻이에요. :?>를 쓰는 캐스트는 컴파일 시점에 검사되지 않고, 실행 시점에 지정된 형식으로 캐스팅을 시도해요. 객체가 대상 형식과 호환되면 캐스트는 성공하고, 호환되지 않으면 런타임이 InvalidCastException을 던져요.

동적 형식 변환에는 downcast 연산자를 쓸 수도 있어요. 다음 식은 프로그램 문맥에서 추론된 형식으로 계층을 아래로 내리는 변환을 나타내요.

downcast expression

upcast 연산자와 마찬가지로, 컴파일러가 문맥에서 특정 대상 형식을 추론하지 못하면 오류를 알리고, 이때는 형식 주석이 필요할 수 있어요.

다음 코드는 :>:?> 연산자의 사용법을 보여줘요. 이 코드는 :?> 연산자가 변환이 성공할 거라고 확신할 때 가장 적합하다는 점을 강조해요. 변환이 실패하면 InvalidCastException을 던지니까요. 변환이 성공할지 확신이 없다면 match 식을 쓰는 형식 테스트(type test)가 더 나아요. 예외를 생성하는 부담을 피할 수 있으니까요.

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

downcastupcast 같은 제네릭 연산자는 인자 형식과 반환 형식을 결정하기 위해 형식 추론에 의존해요. 그래서 앞의 코드 예제에서 let base1 = d1 :> Base1let base1: Base1 = upcast d1로 바꿔 쓸 수 있어요.

형식 주석이 필요한 이유는, upcast만으로는 기본 클래스를 결정할 수 없기 때문이에요.

묵시적 업캐스팅 변환

묵시적 업캐스팅은 다음 상황에서 삽입돼요.

  • 알려진 명명된 형식을 가진 함수나 메서드에 매개 변수를 제공할 때. 계산 식(computation expression)이나 슬라이싱 같은 구성이 메서드 호출이 되는 경우도 포함해요.
  • 알려진 명명된 형식을 가진 레코드 필드나 속성에 값을 할당하거나 변경할 때.
  • if/then/elsematch 식의 한 분기가 다른 분기나 전체적으로 알려진 형식에서 비롯된 알려진 대상 형식을 가질 때.
  • 목록, 배열, 시퀀스 식의 요소가 알려진 대상 형식을 가질 때.

예를 들어 다음 코드를 볼게요.

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")

여기서 조건식의 분기는 각각 TextReaderStreamReader를 계산해요. 두 번째 분기에서 알려진 대상 형식은 메서드의 형식 주석과 첫 번째 분기에서 비롯된 TextReader예요. 그래서 두 번째 분기에는 업캐스팅이 필요 없어요.

묵시적 업캐스팅이 추가로 사용되는 지점마다 경고를 보고 싶다면 경고 3388(/warnon:3388 또는 속성 3388)을 켜면 돼요.

더 알아보기