impl Trait

impl Trait

impl Trait이름은 없지만 구체적이고 특정 트레이트를 구현하는 타입을 지정하는 방법이에요. 이 글에서는 impl Trait가 어디에 쓰이는지, 그리고 캡처(capturing)라는 개념까지 차근차근 다뤄볼게요.

출처: Rust Reference

본문

impl Trait는 두 종류의 위치에 나타날 수 있어요. 하나는 **인자 위치(argument position)**로, 익명 타입 매개변수로 작동하고, 다른 하나는 **반환 위치(return position)**로, 추상 반환 타입으로 작동해요.

#![allow(unused)]
fn main() {
trait Trait {}
impl Trait for () {}

// argument position: anonymous type parameter
fn foo(arg: impl Trait) {
}

// return position: abstract return type
fn bar() -> impl Trait {
}
}

구문은 다음과 같아요.

ImplTraitType → impl Bounds?
ImplTraitTypeOneBound → impl TraitBound?

트레이트 바운드가 적어도 하나 있어야 하고, use<..> 바운드와 opt-out 바운드(예: ?Sized)는 각각 하나를 넘을 수 없어요.

익명 타입 매개변수 (Anonymous type parameters)

📝 참고: 이를 흔히 "인자 위치의 impl Trait"이라고 불러요. (여기서는 "매개변수(parameter)"라는 용어가 더 정확하지만, 기능 개발 당시 쓰이던 표현이 "인자 위치(argument position)의 impl Trait"이라 구현 곳곳에 남아 있어요.)

함수는 impl 다음에 트레이트 바운드 집합을 붙여 매개변수를 익명 타입으로 선언할 수 있어요. 호출자는 익명 타입 매개변수가 선언한 바운드를 만족하는 타입을 제공해야 하고, 함수는 익명 타입 매개변수의 트레이트 바운드를 통해 이용할 수 있는 메서드만 사용할 수 있어요.

예를 들어 다음 두 형태는 거의 동등해요.

#![allow(unused)]
fn main() {
trait Trait {}

// generic type parameter
fn with_generic_type<T: Trait>(arg: T) {
}

// impl Trait in argument position
fn with_impl_trait(arg: impl Trait) {
}
}

즉, 인자 위치의 impl Trait<T: Trait> 같은 제네릭 타입 매개변수의 문법적 설탕(syntactic sugar)이에요. 단, 타입이 익명이고 GenericParams 목록에 나타나지 않는다는 차이가 있어요.

📝 참고: 함수 매개변수에서 제네릭 타입 매개변수와 impl Trait는 정확히 동등하지 않아요. <T: Trait> 같은 제네릭 매개변수에서는 호출자가 호출 지점에서 GenericArgs를 사용해 T의 제네릭 인자를 명시적으로 지정할 수 있어요. 예: foo::<usize>(1). 한쪽에서 다른 쪽으로 매개변수를 바꾸는 것은 제네릭 인자 개수를 바꾸기 때문에, 함수 호출자에게 **호환성이 깨지는 변경(breaking change)**이 될 수 있어요.

추상 반환 타입 (Abstract return types)

📝 참고: 이를 흔히 "반환 위치의 impl Trait"이라고 불러요.

함수는 impl Trait를 사용해 추상 반환 타입을 반환할 수 있어요. 이 타입은 호출자가 지정된 Trait이 선언한 메서드만 사용할 수 있도록 다른 구체 타입을 대신해 서 있는 타입이에요.

함수의 각 가능한 반환 값은 같은 구체 타입으로 해석되어야 해요.

반환 위치의 impl Trait는 함수가 박싱되지 않은(unboxed) 추상 타입을 반환하게 해줘요. 특히 클로저와 이터레이터에서 유용하죠. 클로저는 고유하고 이름을 적을 수 없는 타입을 가지는데, 예전에는 함수에서 클로저를 반환하는 유일한 방법이 트레이트 객체를 쓰는 것이었어요.

#![allow(unused)]
fn main() {
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
}

이 방식은 힙 할당과 동적 디스패치로 인한 성능 손실이 있을 수 있어요. 클로저의 타입을 완전히 지정할 수 없고 Fn 트레이트만 사용할 수 있었기 때문에, 트레이트 객체가 필요했던 거죠. 하지만 impl Trait를 쓰면 더 간단하게 쓸 수 있어요.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}

덕분에 박싱된 트레이트 객체의 단점도 피할 수 있어요.

마찬가지로 이터레이터의 구체 타입은 체인에 있는 이전 이터레이터들의 타입을 모두 포함하다 보니 매우 복잡해질 수 있어요. impl Iterator를 반환하면 함수가 반환 타입의 바운드로 Iterator 트레이트만 노출하고, 관련된 다른 이터레이터 타입을 전부 명시하지 않아도 돼요.

트레이트와 구현에서의 반환 위치 impl Trait (Return-position impl Trait in traits)

트레이트의 함수도 impl Trait를 **익명 연관 타입(anonymous associated type)**을 위한 구문으로 사용할 수 있어요.

트레이트의 연관 함수 반환 타입에 있는 모든 impl Trait는 익명 연관 타입으로 desugar돼요. 구현(implementation)의 함수 시그니처에 나타나는 반환 타입이 그 연관 타입의 값을 결정해요.

캡처 (Capturing)

각 반환 위치 impl Trait 추상 타입 뒤에는 어떤 숨겨진 구체 타입이 있어요. 이 구체 타입이 제네릭 매개변수를 사용하려면, 그 제네릭 매개변수가 추상 타입에 **캡처(capture)**되어 있어야 해요.

자동 캡처: 반환 위치 impl Trait 추상 타입은 현재 스코프의 모든 제네릭 매개변수를 자동으로 캡처해요. 여기에는 제네릭 타입, const, 수명 매개변수(더 높은 등급의 것 포함)가 모두 포함돼요.

2024 에디션 차이: 2024 에디션 이전에는, 자유 함수와 inherent impl의 연관 함수·메서드에서, 추상 반환 타입의 바운드에 나타나지 않는 제네릭 수명 매개변수는 자동으로 캡처되지 않았어요.

정밀 캡처 (Precise capturing): 반환 위치 impl Trait 추상 타입이 캡처하는 제네릭 매개변수 집합은 use<..> 바운드로 명시적으로 제어할 수 있어요. use<..> 바운드가 있으면, 그 안에 나열된 제네릭 매개변수만 캡처돼요. 예를 들어:

#![allow(unused)]
fn main() {
fn capture<'a, 'b, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> {
  //                                      ~~~~~~~~~~~~~~~~~~~~~~~
  //                                     Captures `'a` and `T` only.
  (x, y)
}
}

use<..> 바운드에는 여러 제약이 있어요.

  • 현재 바운드 목록에 use<..> 바운드는 하나만 있을 수 있어요.
  • 현재 스코프의 타입·const 제네릭 매개변수를 모두 포함해야 해요.
  • 추상 타입의 다른 바운드에 나타나는 수명 매개변수를 모두 포함해야 해요.
  • use<..> 바운드 안에서 수명 매개변수는 모든 타입·const 제네릭 매개변수보다 앞에 와야 하고, 생략된 수명('_)은 impl Trait 반환 타입 안에서 허용되는 위치라면 나타날 수 있어요.
  • 모든 스코프 타입 매개변수를 이름으로 포함해야 하기 때문에, 인자 위치 impl Trait를 사용하는 아이템의 시그니처에는 use<..> 바운드를 사용할 수 없어요(스코프에 익명 타입 매개변수가 있으니까요).
  • 트레이트 정의의 연관 함수에 있는 use<..> 바운드는 트레이트의 모든 제네릭 매개변수(트레이트의 암시적 Self 제네릭 타입 매개변수 포함)를 포함해야 해요.

제네릭과 반환 위치 impl Trait의 차이

인자 위치에서 impl Trait는 의미상 제네릭 타입 매개변수와 매우 비슷해요. 하지만 반환 위치에서는 둘 사이에 큰 차이가 있어요. impl Trait에서는 제네릭 타입 매개변수와 달리 함수가 반환 타입을 선택하고, 호출자는 반환 타입을 선택할 수 없어요.

다음 함수는 호출자가 반환 타입 T를 결정하고, 함수가 그 타입을 반환해요.

#![allow(unused)]
fn main() {
trait Trait {}
fn foo<T: Trait>() -> T {
    // ...
panic!()
}
}

반면 다음 함수는 호출자가 반환 타입을 결정할 수 없어요. 함수가 반환 타입을 선택하되, Trait을 구현할 것만 약속해요.

#![allow(unused)]
fn main() {
trait Trait {}
impl Trait for () {}
fn foo() -> impl Trait {
    // ...
}
}

제약 (Limitations)

impl Traitnon-extern 함수의 매개변수나 반환 타입으로만 나타날 수 있어요. let 바인딩의 타입, 필드 타입, 타입 별칭 안에는 나타날 수 없어요.

더 알아보기 (Learn more)