연관 아이템(Associated items)

연관 아이템(Associated items)

연관 아이템은 트레잇에 선언되거나 구현(implementation)에서 정의되는 아이템들이에요. 이름이 붙은 이유가 재밌는데, 연관 타입(associate type), 즉 구현의 대상이 되는 타입에 붙어 정의되기 때문이에요.

모듈에서 선언할 수 있는 아이템 종류의 부분집합이라고 생각하면 쉬워요. 구체적으로는 연관 함수(메서드 포함), 연관 타입, 연관 상수 이렇게 세 가지가 있어요.

연관 아이템은 그 아이템이 연관 대상과 논리적으로 연결될 때 특히 유용해요. 예를 들어 Optionis_some 메서드는 Option과 본질적으로 관련 있으니까 연관으로 두는 게 자연스럽죠.

문법은 대략 이렇게 생겼어요.

AssociatedItem → OuterAttribute* ( MacroInvocationSemi
                  | ( Visibility? ( TypeAlias | ConstantItem | Function ) ) )

모든 연관 아이템 종류에는 두 가지 변형이 있어요. 실제 구현을 담는 **정의(definition)**와, 정의의 시그니처를 선언하는 **선언(declaration)**이죠. 바로 이 선언들이 트레잇의 계약(contract)을 구성하고, 제네릭 타입에서 쓸 수 있는 것들을 결정해요.

출처: Rust Reference

본문

연관 함수와 메서드

연관 함수는 어떤 타입에 연관된 함수예요. 연관 함수 선언은 연관 함수 정의의 시그니처를 선언하는데, 함수 아이템처럼 쓰되 함수 본문만 ;로 바꾼 형태예요. 식별자는 함수의 이름이 되고, 제네릭·매개변수 목록·반환 타입·where 절은 연관 함수 선언의 것과 같아야 해요.

연관 함수 정의는 다른 타입에 연관된 함수를 정의해요. 이건 함수 아이템과 똑같이 써요.

흔한 예로 타입과 연관된 값을 반환하는 new라는 연관 함수를 들 수 있어요.

struct Struct {
    field: i32
}

impl Struct {
    fn new() -> Struct {
        Struct {
            field: 0i32
        }
    }
}

fn main () {
    let _struct = Struct::new();
}

연관 함수가 트레잇에 선언되어 있으면, 트레잇 경로에 함수 이름을 붙인 경로로도 호출할 수 있어요. 이 경우 <_ as Trait>::function_name으로 치환돼요. 아래 네 호출은 모두 같은 뜻이에요.

#![allow(unused)]
fn main() {
trait Num {
    fn from_i32(n: i32) -> Self;
}

impl Num for f64 {
    fn from_i32(n: i32) -> f64 { n as f64 }
}

// 이 네 개는 이 경우 모두 동등해요.
let _: f64 = Num::from_i32(42);
let _: f64 = <_ as Num>::from_i32(42);
let _: f64 = <f64 as Num>::from_i32(42);
let _: f64 = f64::from_i32(42);
}

메서드(Methods)

첫 번째 매개변수 이름이 self인 연관 함수를 메서드라고 불러요. x.foo() 같은 메서드 호출 연산자로도, 일반 함수 호출 표기로도 호출할 수 있죠.

self 매개변수의 타입을 명시하면, 아래 문법이 만들어내는 타입 중 하나로 제한돼요. 여기서 'lt는 임의의 수명을 뜻해요.

P = &'lt S | &'lt mut S | Box<S> | Rc<S> | Arc<S> | Pin<P>
S = Self | P

이 문법의 Self 종단은 구현 대상 타입으로 해석되는 타입을 뜻해요. 문맥 타입 별칭 Self나 다른 타입 별칭, 또는 구현 대상 타입으로 해석되는 연관 타입 프로젝션도 포함할 수 있어요.

#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::sync::Arc;
use std::pin::Pin;
// 구조체 `Example`에 구현된 메서드 예시들.
struct Example;
type Alias = Example;
trait Trait { type Output; }
impl Trait for Example { type Output = Example; }
impl Example {
    fn by_value(self: Self) {}
    fn by_ref(self: &Self) {}
    fn by_ref_mut(self: &mut Self) {}
    fn by_box(self: Box<Self>) {}
    fn by_rc(self: Rc<Self>) {}
    fn by_arc(self: Arc<Self>) {}
    fn by_pin(self: Pin<&Self>) {}
    fn explicit_type(self: Arc<Example>) {}
    fn with_lifetime<'a>(self: &'a Self) {}
    fn nested<'a>(self: &mut &'a Arc<Rc<Box<Alias>>>) {}
    fn via_projection(self: <Example as Trait>::Output) {}
}
}

타입을 지정하지 않는 축약 문법도 있어요. 각각의 동등한 형태가 이래요.

축약 동등 형태
self self: Self
&'lifetime self self: &'lifetime Self
&'lifetime mut self self: &'lifetime mut Self

수명은 이 축약에서 보통 생략(ellided)해요.

self 매개변수에 mut를 붙이면, 일반 매개변수에서 mut 식별자 패턴을 쓰는 것처럼 변경 가능한 변수가 돼요.

#![allow(unused)]
fn main() {
trait Changer: Sized {
    fn change(mut self) {}
    fn modify(mut self: Box<Self>) {}
}
}

트레잇의 메서드 예시를 볼게요. 아래 Shape 트레잇은 drawbounding_box 두 메서드를 정의해요. 이 트레잇을 구현한 모든 값들은, 트레잇이 스코프에 있는 동안 drawbounding_box 메서드를 호출할 수 있어요.

#![allow(unused)]
fn main() {
type Surface = i32;
type BoundingBox = i32;
trait Shape {
    fn draw(&self, surface: Surface);
    fn bounding_box(&self) -> BoundingBox;
}
}

이 트레잇을 Circle에 구현하고 사용하는 예시예요.

#![allow(unused)]
fn main() {
type Surface = i32;
type BoundingBox = i32;
trait Shape {
    fn draw(&self, surface: Surface);
    fn bounding_box(&self) -> BoundingBox;
}

struct Circle {
    // ...
}

impl Shape for Circle {
    // ...
  fn draw(&self, _: Surface) {}
  fn bounding_box(&self) -> BoundingBox { 0i32 }
}

impl Circle {
    fn new() -> Circle { Circle{} }
}

let circle_shape = Circle::new();
let bounding_box = circle_shape.bounding_box();
}

2018 에디션 차이

2015 에디션에서는 익명 매개변수로 트레잇 메서드를 선언할 수 있었어요(예: fn foo(u8)). 하지만 이는 2018 에디션부터 폐기되고 오류가 됐어요. 모든 매개변수에는 인자 이름이 있어야 해요.

메서드 매개변수에 붙는 속성은 일반 함수 매개변수와 같은 규칙·제한을 따라요.

연관 타입(Associated types)

연관 타입은 다른 타입에 연관된 타입 별칭이에요. 주의할 점이 있는데, 고유(inherent) 구현에서는 연관 타입을 정의할 수 없고, 트레잇에서 기본 구현을 줄 수도 없어요.

연관 타입 선언은 연관 타입 정의의 시그니처를 선언해요. Assoc은 연관 타입 이름, Params는 타입·수명·const 매개변수의 쉼표 구분 목록, Bounds는 연관 타입이 충족해야 할 트레잇 바운드의 플러스 구분 목록, WhereBounds는 매개변수가 충족해야 할 바운드의 쉼표 구분 목록이에요.

type Assoc;
type Assoc: Bounds;
type Assoc<Params>;
type Assoc<Params>: Bounds;
type Assoc<Params> where WhereBounds;
type Assoc<Params>: Bounds where WhereBounds;

식별자는 선언된 타입 별칭의 이름이에요. 선택적인 트레잇 바운드는 타입 별칭의 구현이 충족해야 해요. 연관 타입에는 암묵적인 Sized 바운드가 있는데, 특별한 ?Sized 바운드로 완화할 수 있어요.

연관 타입 정의는 트레잇을 어떤 타입에 구현할 때 그 타입의 타입 별칭을 정의해요. 연관 타입 선언과 비슷하게 쓰지만, Bounds를 담을 수 없고 대신 Type을 담아야 해요.

type Assoc = Type;
type Assoc<Params> = Type; // 여기 `Type`은 `Params`를 참조할 수 있어요.
type Assoc<Params> = Type where WhereBounds;
type Assoc<Params> where WhereBounds = Type; // 폐기됨, 위 형태를 권장해요.

타입 Item이 트레잇 Trait에서 연관 타입 Assoc을 가진다면, <Item as Trait>::Assoc은 연관 타입 정의에 지정된 타입의 별칭이 되는 타입이에요. 게다가 Item이 타입 매개변수라면 타입 매개변수에서 Item::Assoc을 쓸 수 있어요.

연관 타입은 제네릭 매개변수와 where 절을 포함할 수 있는데, 이런 걸 흔히 제네릭 연관 타입(GAT)이라고 불러요. 타입 Thing이 제네릭 <'a>를 가진 트레잇 Trait에서 연관 타입 Item을 가진다면, <Thing as Trait>::Item<'x>처럼 이름을 지을 수 있어요. 여기 'x는 스코프에 있는 어떤 수명이에요. 이때 impl의 연관 타입 정의에서 'a가 나타나는 곳마다 'x가 사용돼요.

trait AssociatedType {
    // 연관 타입 선언
    type Assoc;
}

struct Struct;

struct OtherStruct;

impl AssociatedType for Struct {
    // 연관 타입 정의
    type Assoc = OtherStruct;
}

impl OtherStruct {
    fn new() -> OtherStruct {
        OtherStruct
    }
}

fn main() {
    // OtherStruct를 <Struct as AssociatedType>::Assoc으로 가리키는 연관 타입 사용
    let _other_struct: OtherStruct = <Struct as AssociatedType>::Assoc::new();
}

제네릭과 where 절을 가진 연관 타입 예시를 볼게요.

struct ArrayLender<'a, T>(&'a mut [T; 16]);

trait Lend {
    // 제네릭 연관 타입 선언
    type Lender<'a> where Self: 'a;
    fn lend<'a>(&'a mut self) -> Self::Lender<'a>;
}

impl<T> Lend for [T; 16] {
    // 제네릭 연관 타입 정의
    type Lender<'a> = ArrayLender<'a, T> where Self: 'a;

    fn lend<'a>(&'a mut self) -> Self::Lender<'a> {
        ArrayLender(self)
    }
}

fn borrow<'a, T: Lend>(array: &'a mut T) -> <T as Lend>::Lender<'a> {
    array.lend()
}

fn main() {
    let mut array = [0usize; 16];
    let lender = borrow(&mut array);
}

연관 타입 컨테이너 예시로 Container 트레잇을 보면, 타입이 메서드 시그니처에서 바로 사용 가능한 걸 볼 수 있어요.

#![allow(unused)]
fn main() {
trait Container {
    type E;
    fn empty() -> Self;
    fn insert(&mut self, elem: Self::E);
}
}

어떤 타입이 이 트레잇을 구현하려면 모든 메서드의 구현을 제공할 뿐 아니라 타입 E도 지정해야 해요. 표준 라이브러리 타입 Vec에 대한 Container 구현을 볼게요.

#![allow(unused)]
fn main() {
trait Container {
    type E;
    fn empty() -> Self;
    fn insert(&mut self, elem: Self::E);
}
impl<T> Container for Vec<T> {
    type E = T;
    fn empty() -> Vec<T> { Vec::new() }
    fn insert(&mut self, x: T) { self.push(x); }
}
}

BoundsWhereBounds의 관계

아래 예시에서 <X as Example>::Output<Y> 같은 연관 타입 참조가 주어지면, 연관 타입 자체는 Ord여야 하고 타입 YDebug여야 해요.

#![allow(unused)]
fn main() {
use std::fmt::Debug;
trait Example {
    type Output<T>: Ord where T: Debug;
}
}

제네릭 연관 타입의 필수 where 절

트레잇의 제네릭 연관 타입 선언은 현재, 트레잇 안의 함수와 GAT가 어떻게 사용되는지에 따라 where 절 목록을 요구할 수 있어요. 이 규칙은 앞으로 느슨해질 수 있는데, 자세한 갱신은 제네릭 연관 타입 이니셔티브 저장소에서 확인할 수 있어요.

간단히 말하면, 이 where 절은 impl에서 연관 타입의 정의를 최대한 허용하기 위해 필요해요. 이렇게 하려면, GAT가 입력이나 출력으로 나타나는 함수에서 (함수나 트레잇의 매개변수를 써서) 성립이 증명될 수 있는 절이면 GAT 자체에도 똑같이 써야 해요.

#![allow(unused)]
fn main() {
trait LendingIterator {
    type Item<'x> where Self: 'x;
    fn next<'a>(&'a mut self) -> Self::Item<'a>;
}
}

위 예시에서 next 함수에서는 &'a mut self가 암시하는 바운드 덕분에 Self: 'a를 증명할 수 있어요. 그래서 GAT 자체에도 동등한 바운드 where Self: 'x를 써야 해요.

트레잇에 GAT를 쓰는 함수가 여러 개 있으면, 서로 다른 함수의 바운드의 합집합(union)이 아니라 교집합(intersection)을 사용해요.

#![allow(unused)]
fn main() {
trait Check<T> {
    type Checker<'x>;
    fn create_checker<'a>(item: &'a T) -> Self::Checker<'a>;
    fn do_check(checker: Self::Checker<'_>);
}
}

이 예시에서는 type Checker<'a>;에 필요한 바운드가 없어요. create_checker에서 T: 'a임을 알지만 do_check에서는 그걸 알 수 없거든요. 그런데 do_check를 주석 처리하면 Checkerwhere T: 'x 바운드가 필요해져요.

연관 타입의 바운드도 필수 where 절을 퍼뜨려요.

#![allow(unused)]
fn main() {
trait Iterable {
    type Item<'a> where Self: 'a;
    type Iterator<'a>: Iterator<Item = Self::Item<'a>> where Self: 'a;
    fn iter<'a>(&'a self) -> Self::Iterator<'a>;
}
}

여기서 where Self: 'aiter 때문에 Item에 필요해요. 그런데 ItemIterator의 바운드에서 사용되므로, where Self: 'a 절이 거기에도 필요한 거예요.

마지막으로, 트레잇의 GAT에서 'static을 명시적으로 사용하는 것은 필요한 바운드로 계산되지 않아요.

#![allow(unused)]
fn main() {
trait StaticReturn {
    type Y<'a>;
    fn foo(&self) -> Self::Y<'static>;
}
}

연관 상수(Associated constants)

연관 상수는 어떤 타입에 연관된 상수예요. 연관 상수 선언은 연관 상수 정의의 시그니처를 선언하는데, const, 식별자, :, 타입, 마지막 ; 순서로 써요. 식별자는 경로에서 쓰는 상수 이름이고, 타입은 정의가 구현해야 할 타입이에요.

연관 상수 정의는 타입에 연관된 상수를 정의해요. 상수 아이템과 똑같이 써요.

연관 상수 정의는 참조될 때만 상수 평가(constant evaluation)를 거쳐요. 게다가 제네릭 매개변수를 포함하는 정의는 단형화(monomorphization) 이후에 평가돼요.

struct Struct;
struct GenericStruct<const ID: i32>;

impl Struct {
    // 바로 평가되지 않는 정의
    const PANIC: () = panic!("compile-time panic");
}

impl<const ID: i32> GenericStruct<ID> {
    // 바로 평가되지 않는 정의
    const NON_ZERO: () = if ID == 0 {
        panic!("contradiction")
    };
}

fn main() {
    // Struct::PANIC을 참조하면 컴파일 오류가 나요.
    let _ = Struct::PANIC;

    // OK, ID가 0이 아니에요.
    let _ = GenericStruct::<1>::NON_ZERO;

    // ID=0으로 NON_ZERO를 평가하다 컴파일 오류가 나요.
    let _ = GenericStruct::<0>::NON_ZERO;
}

기본 예시를 하나 볼게요. 트레잇에 상수 시그니처만 두고 impl에서 값을 채워요.

trait ConstantId {
    const ID: i32;
}

struct Struct;

impl ConstantId for Struct {
    const ID: i32 = 1;
}

fn main() {
    assert_eq!(1, Struct::ID);
}

기본값을 쓰는 예시도 있어요. 트레잇에서 기본값을 주면, impl에서 따로 정의하지 않는 한 그대로 사용돼요.

trait ConstantIdDefault {
    const ID: i32 = 1;
}

struct Struct;
struct OtherStruct;

impl ConstantIdDefault for Struct {}

impl ConstantIdDefault for OtherStruct {
    const ID: i32 = 5;
}

fn main() {
    assert_eq!(1, Struct::ID);
    assert_eq!(5, OtherStruct::ID);
}

더 알아보기

  • 트레잇: 연관 아이템의 선언이 모이는 곳
  • 구현(implementation): 연관 아이템의 정의가 실제로 채워지는 곳
  • 함수와 메서드 호출
  • 타입 별칭과 상수 아이템
  • 제네릭과 where 절