구현
구현 (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..=Pn이T0..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>같은 기본 타입의 특수 취급을 이해하면 크레이트 설계 시 충돌을 피할 수 있어요.