트레이트 바운드와 수명 바운드

트레이트 바운드와 수명 바운드

제네릭 항목이 타입 파라미터와 수명 파라미터로 어떤 타입과 수명을 쓸 수 있는지 제한할 방법이 필요해요. 그 역할을 하는 것이 트레이트 바운드와 수명 바운드예요. 바운드는 where 절 안의 어떤 타입에도 붙일 수 있고, 일반적인 경우를 위한 더 짧은 형태들도 있어요.

출처: Rust Reference

문법

Bounds → Bound ( + Bound )* +?
Bound  → Lifetime | TraitBound | UseBound

TraitBound →       ( ? | ForLifetimes )? TypePath
           | ( ( ? | ForLifetimes )? TypePath )

LifetimeBounds → ( Lifetime+ )* Lifetime?
Lifetime → LIFETIME_OR_LABEL | 'static | '_

UseBound → use UseBoundGenericArgs
UseBoundGenericArgs → <>
                     | < ( UseBoundGenericArg, )* UseBoundGenericArg,? >
UseBoundGenericArg → Lifetime | IDENTIFIER | Self

좋은 소식은 몇몇 흔한 경우에 더 짧은 형태가 있다는 거예요. 요점은 제네릭 파라미터 선언 뒤에 쓰는 바운드와 where 절에 쓰는 바운드가 같다는 거예요.

  • 제네릭 파라미터 선언 후에 쓰는 바운드: fn f<A: Copy>() {}fn f<A>() where A: Copy {}와 같아요.
  • 트레이트 선언에서 슈퍼트레이트로: trait Circle : Shape {}trait Circle where Self : Shape {}와 같아요.
  • 트레이트 선언에서 연관 타입의 바운드로: trait A { type B: Copy; }trait A where Self::B: Copy { type B; }와 같아요.

항목의 바운드는 그 항목을 사용할 때 만족되어야 해요. 제네릭 항목을 타입 검사하고 대여 검사(borrow check)할 때, 바운드는 어떤 타입에 트레이트가 구현되어 있음을 결정하는 데 쓰일 수 있어요. 예를 들어 Ty: Trait가 있다고 하면

  • 제네릭 함수의 본문 안에서 Ty 값에 Trait의 메서드를 호출할 수 있어요. 마찬가지로 Trait의 연관 상수도 쓸 수 있어요.
  • Trait의 연관 타입을 쓸 수 있어요.
  • T: Trait 바운드를 가진 제네릭 함수와 타입을 TyT로 써서 사용할 수 있어요.
#![allow(unused)]
fn main() {
type Surface = i32;
trait Shape {
    fn draw(&self, surface: Surface);
    fn name() -> &'static str;
}

fn draw_twice<T: Shape>(surface: Surface, sh: T) {
    sh.draw(surface);           // Can call method because T: Shape
    sh.draw(surface);
}

fn copy_and_draw_twice<T: Copy>(surface: Surface, sh: T) where T: Shape {
    let shape_copy = sh;        // doesn't move sh because T: Copy
    draw_twice(surface, sh);    // Can use generic function because T: Shape
}

struct Figure<S: Shape>(S, S);

fn name_figure<U: Shape>(
    figure: Figure<U>,          // Type Figure<U> is well-formed because U: Shape
) {
    println!(
        "Figure of two {}",
        U::name(),              // Can use associated function
    );
}
}

항목의 파라미터나 고차 수명(higher-ranked lifetime)을 사용하지 않는 바운드는 항목이 정의될 때 검사돼요. 그런 바운드가 거짓이면 오류예요.

Copy, Clone, Sized 바운드는 사용 시점에 특정 제네릭 타입에 대해 (사용이 구체적인 타입을 제공하지 않아도) 검사되기도 해요. 가변 참조, 트레이트 객체, 슬라이스에 대한 바운드로 CopyClone을 갖는 것은 오류예요. 트레이트 객체나 슬라이스에 대한 바운드로 Sized를 갖는 것도 오류예요.

#![allow(unused)]
fn main() {
struct A<'a, T>
where
    i32: Default,           // Allowed, but not useful
    i32: Iterator,          // Error: `i32` is not an iterator
    &'a mut T: Copy,        // (at use) Error: the trait bound is not satisfied
    [T]: Sized,             // (at use) Error: size cannot be known at compilation
{
    f: &'a T,
}
struct UsesA<'a, T>(A<'a, T>);
}

트레이트 바운드와 수명 바운드는 트레이트 객체를 이름 짓는 데도 쓰여요.

?Sized

? 는 타입 파라미터나 연관 타입에 대한 암묵적 Sized 트레이트 바운드를 완화하는 데만 쓰여요. ?Sized는 다른 타입의 바운드로는 쓸 수 없어요.

수명 바운드(Lifetime bounds)

수명 바운드는 타입이나 다른 수명에 적용될 수 있어요. 바운드 'a: 'b는 보통 "'a가 'b보다 오래 산다(outlives)"로 읽어요. 'a: 'b'a'b만큼 최소한 오래 지속된다는 뜻이라, &'b ()가 유효할 때마다 &'a ()도 유효해요.

#![allow(unused)]
fn main() {
fn f<'a, 'b>(x: &'a i32, mut y: &'b i32) where 'a: 'b {
    y = x;                      // &'a i32 is a subtype of &'b i32 because 'a: 'b
    let r: &'b &'a i32 = &&0;   // &'b &'a i32 is well formed because 'a: 'b
}
}

T: 'aT의 모든 수명 파라미터가 'a보다 오래 산다는 뜻이에요. 예를 들어 'a가 제약 없는 수명 파라미터라면, i32: 'static&'static str: 'a는 만족되지만 Vec<&'a ()>: 'static은 만족되지 않아요.

고차 트레이트 바운드(Higher-ranked trait bounds)

for<'a>처럼 고차(higher-ranked) 트레이트 바운드도 가능해요.

ForLifetimes → for GenericParams

트레이트 바운드는 수명에 대해 고차일 수 있어요. 그런 바운드는 모든 수명에 대해 참인 바운드를 지정해요. 예를 들어 for<'a> &'a T: PartialEq<i32> 같은 바운드는 아래와 같은 구현을 요구해요.

#![allow(unused)]
fn main() {
struct T;
impl<'a> PartialEq<i32> for &'a T {
    // ...
   fn eq(&self, other: &i32) -> bool {true}
}
}

그리고 나서 어떤 수명의 &'a Ti32와 비교하는 데 쓸 수 있어요. 참조의 수명이 함수의 어떤 가능한 수명 파라미터보다도 짧기 때문에, 여기에는 고차 바운드만 쓸 수 있어요.

#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where for<'a> F: Fn(&'a i32) {
    let zero = 0;
    f(&zero);
}
}

고차 수명은 트레이트 바로 앞에서 지정할 수도 있어요. 유일한 차이는 수명 파라미터의 스코프인데, 전체 바운드가 아니라 이어지는 트레이트의 끝까지만 확장돼요. 아래 함수는 위 함수와 동등해요.

#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where F: for<'a> Fn(&'a i32) {
    let zero = 0;
    f(&zero);
}
}

함의된 바운드(Implied bounds)

타입이 well-formed가 되기 위해 필요한 수명 바운드는 때로 추론되기도 해요.

#![allow(unused)]
fn main() {
fn requires_t_outlives_a<'a, T>(x: &'a T) {}
}

타입 &'a T가 well-formed가 되려면 타입 파라미터 T'a보다 오래 살아야 해요. 함수 시그니처에 &'a T 타입이 들어 있고, 이는 T: 'a가 성립할 때만 유효하므로 이것은 추론돼요.

함의된 바운드는 함수의 모든 파라미터와 반환에 추가돼요. requires_t_outlives_a 안에서는 명시적으로 지정하지 않아도 T: 'a가 성립한다고 가정할 수 있어요.

#![allow(unused)]
fn main() {
fn requires_t_outlives_a_not_implied<'a, T: 'a>() {}

fn requires_t_outlives_a<'a, T>(x: &'a T) {
    // This compiles, because `T: 'a` is implied by
    // the reference type `&'a T`.
    requires_t_outlives_a_not_implied::<'a, T>();
}
}
#![allow(unused)]
fn main() {
fn requires_t_outlives_a_not_implied<'a, T: 'a>() {}
fn not_implied<'a, T>() {
    // This errors, because `T: 'a` is not implied by
    // the function signature.
    requires_t_outlives_a_not_implied::<'a, T>();
}
}

추론되는 것은 수명 바운드뿐이에요. 트레이트 바운드는 여전히 명시적으로 추가해야 해요. 그래서 다음 예시는 오류가 돼요.

#![allow(unused)]
fn main() {
use std::fmt::Debug;
struct IsDebug<T: Debug>(T);
// error: `T` doesn't implement `Debug`
fn doesnt_specify_t_debug<T>(x: IsDebug<T>) {}
}

수명 바운드는 어떤 타입에 대해서도 타입 정의와 impl 블록에 대해 추론돼요.

#![allow(unused)]
fn main() {
struct Struct<'a, T> {
    // This requires `T: 'a` to be well-formed
    // which is inferred by the compiler.
    field: &'a T,
}

enum Enum<'a, T> {
    // This requires `T: 'a` to be well-formed,
    // which is inferred by the compiler.
    //
    // Note that `T: 'a` is required even when only
    // using `Enum::OtherVariant`.
    SomeVariant(&'a T),
    OtherVariant,
}

trait Trait<'a, T: 'a> {}

// This would error because `T: 'a` is not implied by any type
// in the impl header.
//     impl<'a, T> Trait<'a, T> for () {}

// This compiles as `T: 'a` is implied by the self type `&'a T`.
impl<'a, T> Trait<'a, T> for &'a T {}
}

use 바운드(Use bounds)

어떤 바운드 목록은 use<..> 바운드를 포함해서 impl Trait 추상 반환 타입이 어떤 제네릭 파라미터를 포착하는지 제어할 수 있어요. 자세한 내용은 정밀 포착(precise capturing)을 참고하세요.

더 알아보기