구현

구현 (Implementations)

구현(implementation)은 구현 타입(implementing type)에 항목들을 연관시키는 항목이에요. impl 키워드로 정의되며, 구현 중인 타입의 인스턴스에 속하거나 타입에 정적으로 속하는 함수들을 담아요.

출처: Rust Reference

본문

구현에는 두 종류가 있어요.

  • 고유 구현(inherent implementation)
  • 트레이트 구현(trait implementation)

고유 구현

고유 구현은 impl 키워드, 제네릭 타입 선언, **명목 타입(nominal type)**에 대한 경로, where 절, 그리고 중괄호로 묶인 연관할 수 있는 항목(associable item)들의 나열로 이루어져요.

여기서 명목 타입을 구현 타입이라고 하고, 연관할 수 있는 항목들을 구현 타입의 **연관 항목(associated item)**이라고 불러요. 고유 구현은 포함된 항목들을 구현 타입에 연관시켜요.

고유 구현은 **연관 함수(메서드 포함)**와 연관 상수를 담을 수 있어요. 하지만 연관 타입 별칭은 담을 수 없어요.

연관 항목에 대한 경로는, 구현 타입에 대한 아무 경로 뒤에 연관 항목의 식별자를 마지막 경로 구성요소로 붙인 형태예요.

한 타입은 여러 개의 고유 구현을 가질 수 있어요. 구현 타입은 원래 타입 정의와 같은 크레이트 안에 정의되어야 해요.

pub mod color {
 pub struct Color(pub u8, pub u8, pub u8);

 impl Color {
  pub const WHITE: Color = Color(255, 255, 255);
 }
}

mod values {
 use super::color::Color;
 impl Color {
  pub fn red() -> Color {
   Color(255, 0, 0)
  }
 }
}

pub use self::color::Color;
fn main() {
 // Actual path to the implementing type and impl in the same module.
 color::Color::WHITE;

 // Impl blocks in different modules are still accessed through a path to the type.
 color::Color::red();

 // Re-exported paths to the implementing type also work.
 Color::red();

 // Does not work, because use in `values` is not pub.
 // values::Color::red();
}

트레이트 구현

트레이트 구현은 고유 구현과 비슷하지만, 선택적인 제네릭 타입 선언 뒤에 트레이트, 그다음 for 키워드, 그다음 명목 타입에 대한 경로가 온다는 점이 달라요.

여기서 트레이트를 **구현된 트레이트(implemented trait)**라고 해요. 구현 타입은 구현된 트레이트를 구현해요.

트레이트 구현은 구현된 트레이트가 선언한 모든 비-기본(비-default) 연관 항목을 반드시 정의하고, 기본 연관 항목은 재정의할 수 있으며, 그 외의 항목은 정의할 수 없어요.

연관 항목에 대한 경로는 < 뒤에 구현 타입에 대한 경로, as, 트레이트에 대한 경로, >를 하나의 경로 구성요소로 하고, 그 뒤에 연관 항목의 경로 구성요소가 오는 형태예요.

unsafe 트레이트는 트레이트 구현이 unsafe 키워드로 시작하도록 요구해요.

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct Point {x: f64, y: f64};
type Surface = i32;
struct BoundingBox {x: f64, y: f64, width: f64, height: f64};
trait Shape { fn draw(&self, s: Surface); fn bounding_box(&self) -> BoundingBox; }
fn do_draw_circle(s: Surface, c: Circle) { }
struct Circle {
 radius: f64,
 center: Point,
}

impl Copy for Circle {}

impl Clone for Circle {
 fn clone(&self) -> Circle { *self }
}

impl Shape for Circle {
 fn draw(&self, s: Surface) { do_draw_circle(s, *self); }
 fn bounding_box(&self) -> BoundingBox {
  let r = self.radius;
  BoundingBox {
   x: self.center.x - r,
   y: self.center.y - r,
   width: 2.0 * r,
   height: 2.0 * r,
  }
 }
}
}

트레이트 구현 정합성(coherence)

트레이트 구현은 orphan 규칙 검사가 실패하거나 겹치는(overlapping) 구현 인스턴스가 있으면 정합적이지 않다고(incoherent) 간주돼요.

두 트레이트 구현은, 그것이 구현하는 트레이트들의 비어 있지 않은 교집합이 있고 구현을 같은 타입으로 인스턴스화할 수 있을 때 서로 겹친다고 해요.

Orphan 규칙

orphan 규칙은 이렇게 말해요. 트레이트나 구현 안의 타입 중 적어도 하나가 현재 크레이트에 정의되어 있을 때만 트레이트 구현이 허용된다는 거예요. 이 규칙은 서로 다른 크레이트 간의 상충하는 트레이트 구현을 막아서, 정합성을 보장하는 핵심이에요.

orphan 구현은 외부(foreign) 트레이트를 외부 타입에 대해 구현하는 것을 말해요. 이게 자유롭게 허용된다면, 두 크레이트가 같은 타입에 같은 트레이트를 서로 호환되지 않는 방식으로 구현할 수 있어서, 의존성을 추가하거나 업데이트할 때 상충하는 구현 때문에 컴파일이 깨질 수 있는 상황이 생겨요.

orphan 규칙 덕분에 라이브러리 작성자는 자신의 트레이트에 새 구현을 추가해도 하위 코드를 깨뜨릴 걱정 없이 할 수 있어요. 이런 제약이 없다면 라이브러리는 impl<T: Display> MyTrait for T 같은 구현을, 하위 구현과 충돌할 가능성 없이는 추가할 수 없게 돼요.

impl<P1..=Pn> Trait<T1..=Tn> for T0가 주어졌을 때, 다음 중 적어도 하나가 참이어야만 구현이 유효해요.

  • Trait이 로컬(local) 트레이트이거나
  • 타입 T0..=Tn 중 적어도 하나가 로컬 타입이어야 해요. 그런 첫 번째 타입을 Ti라고 해요. 그리고
  • 노출되지 않은(uncovered) 타입 매개변수 P1..=PnT0..Ti(단, Ti 제외)에 나타나면 안 돼요.

제한되는 것은 노출되지 않은 타입 매개변수의 등장뿐이에요.

참고: 정합성의 목적상 기본(fundamental) 타입은 특별해요. Box<T>T는 가려진(covered) 것으로 간주되지 않고, Box<LocalType>은 로컬로 간주돼요.

제네릭 구현

구현은 제네릭 매개변수를 받을 수 있고, 그것은 구현의 나머지 부분에서 사용될 수 있어요. 구현 매개변수는 impl 키워드 바로 뒤에 써요.

#![allow(unused)]
fn main() {
trait Seq<T> { fn dummy(&self, _: T) { } }
impl<T> Seq<T> for Vec<T> {
 /* ... */
}
impl Seq<bool> for u32 {
 /* Treat the integer as a sequence of bits */
}
}

제네릭 매개변수는 다음 중 적어도 한 곳에 한 번 이상 나타날 때 그 구현을 제약(constrain)해요.

  • 구현된 트레이트(있을 경우)
  • 구현 타입
  • 구현을 제약하는 다른 매개변수를 포함하는 타입의 바운드에 연관 타입으로

타입과 const 매개변수는 항상 구현을 제약해야 해요. 수명(lifetime)은 연관 타입에 쓰일 때 구현을 제약해야 해요.

제약 상황의 예시를 볼게요.

#![allow(unused)]
fn main() {
trait Trait{}
trait GenericTrait<T> {}
trait HasAssocType { type Ty; }
struct Struct;
struct GenericStruct<T>(T);
struct ConstGenericStruct<const N: usize>([(); N]);
// T constrains by being an argument to GenericTrait.
impl<T> GenericTrait<T> for i32 { /* ... */ }

// T constrains by being an argument to GenericStruct
impl<T> Trait for GenericStruct<T> { /* ... */ }

// Likewise, N constrains by being an argument to ConstGenericStruct
impl<const N: usize> Trait for ConstGenericStruct<N> { /* ... */ }

// T constrains by being in an associated type in a bound for type `U` which is
// itself a generic parameter constraining the trait.
impl<T, U> GenericTrait<U> for u32 where U: HasAssocType<Ty = T> { /* ... */ }

// Like previous, except the type is `(U, isize)`. `U` appears inside the type
// that includes `T`, and is not the type itself.
impl<T, U> GenericStruct<U> where (U, isize): HasAssocType<Ty = T> { /* ... */ }
}

그리고 제약하지 않는 상황의 예시도 볼게요. 아래 것들은 타입이나 const 매개변수가 제약하지 않으므로 모두 에러예요.

#![allow(unused)]
fn main() {
// The rest of these are errors, since they have type or const parameters that
// do not constrain.

// T does not constrain since it does not appear at all.
impl<T> Struct { /* ... */ }

// N does not constrain for the same reason.
impl<const N: usize> Struct { /* ... */ }

// Usage of T inside the implementation does not constrain the impl.
impl<T> Struct {
 fn uses_t(t: &T) { /* ... */ }
}

// T is used as an associated type in the bounds for U, but U does not constrain.
impl<T, U> Struct where U: HasAssocType<Ty = T> { /* ... */ }

// T is used in the bounds, but not as an associated type, so it does not constrain.
impl<T, U> GenericTrait<U> for u32 where U: GenericTrait<T> {}
}

허용되는, 제약하지 않는 수명 매개변수의 예시는 다음과 같아요.

#![allow(unused)]
fn main() {
struct Struct;
impl<'a> Struct {}
}

허용되지 않는, 제약하지 않는 수명 매개변수의 예시는 다음과 같아요.

#![allow(unused)]
fn main() {
struct Struct;
trait HasAssocType { type Ty; }
impl<'a> HasAssocType for Struct {
 type Ty = &'a Struct;
}
}

구현의 속성

구현은 impl 키워드 앞에 바깥 속성을, 연관 항목을 담는 중괄호 안에 **안쪽 속성(inner attribute)**을 가질 수 있어요. 안쪽 속성은 모든 연관 항목보다 먼저 와야 해요. 여기서 의미가 있는 속성은 cfg, deprecated, doc, 그리고 린트 검사 속성들이에요.

더 알아보기

  • 연관 함수, 메서드, 연관 상수, 연관 타입을 각각 살펴보면 고유 구현과 트레이트 구현에서 무엇을 담을 수 있는지 더 명확해져요.
  • orphan 규칙과 정합성, 그리고 Box<T> 같은 기본 타입의 특수 취급을 이해하면 크레이트 설계 시 충돌을 피할 수 있어요.