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 Trait는 non-extern 함수의 매개변수나 반환 타입으로만 나타날 수 있어요. let 바인딩의 타입, 필드 타입, 타입 별칭 안에는 나타날 수 없어요.
더 알아보기 (Learn more)
- 타입 (Types) — 타입 시스템 전체 개관
- 트레이트 객체 —
impl Trait와 대비되는dyn Trait - 제네릭 (Generics) — 제네릭 매개변수의 규칙