타입 강제 변환

타입 강제 변환 (Type coercions)

&mut 값을 함수 파라미터로 넘기면 자동으로 &로 바뀔 때가 있어요. 이건 명시적 캐스트와는 다른, Rust가 자동으로 수행하는 타입 변환이에요. 이를 타입 강제 변환(type coercion) 이라고 해요. 어떤 곳에서 일어나고, 어떤 타입들이 서로 강제될 수 있는지 살펴볼게요.

출처: Rust Reference

본문

타입 강제 변환은 값의 타입을 바꾸는 암묵적 연산(implicit operation) 이야. 특정 위치에서 자동으로 일어나고, 실제로 강제되는 타입은 매우 제한적이에요.

강제 변환이 허용하는 어떤 변환이든 타입 캐스트 연산자 as 로 명시적으로도 수행할 수 있어요.

강제 변환은 원래 RFC 401에서 정의됐고 RFC 1558에서 확장됐어요.

강제 변환 위치 (Coercion sites)

강제 변환은 프로그램의 특정 강제 변환 위치(coercion sites) 에서만 일어날 수 있어요. 보통 원하는 타입이 명시적이거나, 명시적인 타입으로부터 전파를 통해 (타입 추론 없이) 유도될 수 있는 곳이죠. 가능한 강제 변환 위치는 다음과 같아요.

let — 명시적 타입이 주어진 let 문. 예를 들어 다음에서 &mut 42&i8 타입으로 강제돼요.

#![allow(unused)]
fn main() {
let _: &i8 = &mut 42;
}

staticconst 아이템 선언 — (let 문과 비슷해요.)

함수 호출의 인자 — 강제되는 값은 실제 인자이고, 형식 매개변수(formal parameter)의 타입으로 강제돼요.

fn bar(_: &i8) { }

fn main() {
    bar(&mut 42);
}

메서드 호출의 경우 수신자(self 매개변수) 타입은 다르게 강제되는데, 자세한 내용은 메서드 호출 표현식 문서를 참고하세요.

구조체, 유니온, 열거형 변형 필드의 인스턴스화

struct Foo<'a> { x: &'a i8 }

fn main() {
    Foo { x: &mut 42 };
}

함수 결과 — 세미콜론으로 끝나지 않는 블록의 마지막 줄이거나, return 문의 어떤 표현식. 예를 들어 다음에서 x&dyn Display 타입으로 강제돼요.

#![allow(unused)]
fn main() {
use std::fmt::Display;
fn foo(x: &u32) -> &dyn Display {
    x
}
}

할당 표현식의 할당값 피연산자(assigned value operand) — 예를 들어 다음에서 y&i8 타입으로 강제돼요.

#![allow(unused)]
fn main() {
let mut x = &0i8;
let y = &mut 42i8;
x = y;
}

이런 위치 중 하나의 표현식이 강제 전파 표현식(coercion-propagating expression) 이라면, 그 표현식의 관련 하위 표현식들도 강제 변환 위치가 돼요. 전파는 이 새로운 위치들에서 재귀적으로 일어나요. 전파 표현식과 관련 하위 표현식은 다음과 같아요.

  • 배열 리터럴 — 배열이 [U; n] 타입일 때, 배열 리터럴의 각 하위 표현식은 타입 U로 강제되는 위치예요.
  • 반복 구문의 배열 리터럴 — 배열이 [U; n]이면, 반복되는 하위 표현식이 타입 U로 강제되는 위치예요.
  • 튜플 — 튜플이 타입 (U_0, U_1, ..., U_n)으로 강제되는 위치일 때, 각 하위 표현식은 각각의 타입(예: 0번째 하위 표현식은 U_0)으로 강제되는 위치예요.
  • 괄호로 싸인 하위 표현식 (e) — 표현식이 타입 U라면 하위 표현식은 U로 강제되는 위치예요.
  • 블록 — 블록이 타입 U라면, 블록의 마지막 표현식(세미콜론으로 끝나지 않는다면)이 U로 강제되는 위치예요. 여기에는 if/else 같은 제어 흐름 문의 일부인 블록도 포함돼요(블록의 타입이 알려져 있다면).

강제 변환 타입 (Coercion types)

다음 타입들 사이에는 강제 변환이 허용돼요.

  • TUTU의 하위 타입일 때 (반사적 경우)
  • T_1T_3T_1T_2로, T_2T_3로 강제될 때 (전이적 경우). 참고로 이건 아직 완전히 지원되지는 않아요.
  • &mut T&T
  • *mut T*const T
  • &T*const T
  • &mut T*mut T
  • &T 또는 &mut T&UTDeref<Target = U>를 구현할 때. 예:
use std::ops::Deref;

struct CharContainer {
    value: char,
}

impl Deref for CharContainer {
    type Target = char;

    fn deref<'a>(&'a self) -> &'a char {
        &self.value
    }
}

fn foo(arg: &char) {}

fn main() {
    let x = &mut CharContainer { value: 'y' };
    foo(x); //&mut CharContainer is coerced to &char.
}
  • &mut T&mut UTDerefMut<Target = U>를 구현할 때
  • TyCtor(T)TyCtor(U)TyCtor(T)&T, &mut T, *const T, *mut T, Box<T> 중 하나이고, UT로부터 비크기 강제 변환(unsized coercion)으로 얻어질 수 있을 때
  • 함수 아이템 타입 → 함수 포인터 (fn pointers)
  • 비캡처 클로저 → 함수 포인터
  • ! → 임의의 T

비크기 강제 변환 (Unsized coercions)

다음 강제 변환들은 타입을 비크기(unsized) 타입으로 변환하는 것과 관련이 있어서 비크기 강제 변환이라고 불러요. 위에서 설명한 것처럼 다른 강제 변환이 허용되지 않는 몇몇 경우에도 허용되고, 강제 변환이 일어날 수 있는 다른 어디에서도 일어날 수 있어요.

UnsizeCoerceUnsized 두 트레이트가 이 과정을 돕고, 라이브러리에서 사용할 수 있게 노출해요. 다음 강제 변환들은 내장(built-in)이고, T가 그중 하나로 U에 강제될 수 있다면 T에 대한 Unsize<U> 구현이 제공돼요.

  • [T; n][T]
  • Tdyn UTU + Sized를 구현하고 U가 dyn 호환(dyn compatible)일 때
  • dyn Tdyn UUT의 슈퍼트레이트 중 하나일 때. 이는 오토 트레이트를 버리는 것, 즉 dyn T + Autodyn U를 허용해요. 또 주 트레이트(principal trait)가 오토 트레이트를 슈퍼트레이트로 갖는다면 오토 트레이트를 추가하는 것, 즉 trait T: U + Send {}가 주어졌을 때 dyn Tdyn T + Send 또는 dyn U + Send 강제 변환도 허용해요.
  • Foo<..., T, ...>Foo<..., U, ...> — 다음 조건이 모두 성립할 때:
    • Foo가 구조체일 것
    • TUnsize<U>를 구현할 것
    • Foo마지막 필드의 타입이 T를 포함할 것
    • 그 필드가 타입 Bar<T>라면, Bar<T>Unsize<Bar<U>>를 구현할 것
    • T가 다른 어떤 필드의 타입에도 포함되지 않을 것

추가로, 타입 Foo<T>TUnsize<U> 또는 CoerceUnsized<Foo<U>>를 구현할 때 CoerceUnsized<Foo<U>>를 구현할 수 있어요. 이는 Foo<U>로의 비크기 강제 변환을 제공하게 해 줘요.

참고: 비크기 강제 변환의 정의와 그 구현은 안정화됐지만, 트레이트 자체는 아직 안정적이지 않아서 안정화된 Rust에서는 직접 사용할 수 없어요.

최소 상계 보강제 변환 (Least upper bound coercions)

어떤 문맥에서는 컴파일러가 여러 타입을 함께 강제해서 가장 일반적인 타입을 찾아야 해요. 이를 "최소 상계(LUB, Least Upper Bound)" 강제 변환이라고 해요. LUB 강제 변환은 다음 상황에서만 사용돼요.

  • 일련의 if 분기들의 공통 타입 찾기
  • 일련의 match arm들의 공통 타입 찾기
  • 배열 요소들의 공통 타입 찾기
  • 레이블 블록 표현식에서 break 피연산자들과 최종 블록 피연산자 사이의 공통 타입 찾기
  • break 표현식을 가진 loop 표현식에서 break 피연산자들 사이의 공통 타입 찾기
  • 여러 return 문을 가진 클로저의 반환 타입 찾기
  • 여러 return 문을 가진 함수의 반환 타입 타입 체크

각 경우마다 서로 강제되어야 하는 타입들의 집합 T0..Tn이 있고, 그 목표 타입 T_t는 처음에는 알 수 없어요.

LUB 강제 변환은 반복적으로 계산돼요. 목표 타입 T_tT0에서 시작해요. 각 새 타입 Ti에 대해 다음을 고려해요.

  • Ti가 현재 목표 타입 T_t로 강제될 수 있다면, 아무것도 바꾸지 않아요.
  • 그렇지 않으면 T_tTi로 강제될 수 있는지 확인해요. 가능하다면 T_tTi로 바꿔요. (이 확인은 지금까지 고려된 모든 소스 표현식이 암묵적 강제 변환을 가지는지에도 조건이 걸려 있어요.)
  • 그것도 아니라면 T_tTi공통 슈퍼타입(mutual supertype) 을 계산해서 그것을 새 목표 타입으로 삼아요.

예를 들어 볼게요.

#![allow(unused)]
fn main() {
let (a, b, c) = (0, 1, 2);
// For if branches
let bar = if true {
    a
} else if false {
    b
} else {
    c
};

// For match arms
let baw = match 42 {
    0 => a,
    1 => b,
    _ => c,
};

// For array elements
let bax = [a, b, c];

// For closure with multiple return statements
let clo = || {
    if true {
        a
    } else if false {
        b
    } else {
        c
    }
};
let baz = clo();

// For type checking of function with multiple return statements
fn foo() -> i32 {
    let (a, b, c) = (0, 1, 2);
    match 42 {
        0 => a,
        1 => b,
        _ => c,
    }
}
}

이 예시들에서 ba* 타입들은 LUB 강제 변환으로 찾아져요. 그리고 함수 foo를 처리할 때 컴파일러는 a, b, c의 LUB 강제 변환 결과가 i32인지 확인해요.

이 설명은 분명 비공식적이에요. 더 정밀하게 만드는 것은 Rust 타입 체커를 더 정밀하게 명세하려는 전반적인 노력의 일부로 진행될 예정이에요.

더 알아보기 (Learn more)