2.9. impl
impl 블록은 어떤 타입에 메서드를 부착하거나, 어떤 타입에 트레잇을 구현하는 컨테이너입니다.
2.9.1. 두 형태
impl 블록의 문법은 다음과 같이 두 가지 형태를 가집니다.
impl_block ::= "impl" generics? type_expr "{" impl_item* "}"
| "unsafe"? "impl" generics? type_expr "for" type_expr where_clause? "{" impl_item* "}"
| "impl" generics? "!" type_expr "for" type_expr where_clause? "{" "}"
impl_item ::= fn_decl
| "type" identifier "=" type_expr ";"
| const_decl
impl < 의 < 는 뒤에 제네릭 매개변수가 올 때만 (<>, <T>, <T:, <T,, <T =, <const N) generics 를 엽니다. 그 밖이면 대상 타입의 시작입니다. 예: inherent impl <A as P>::Out { .. }.
고유 impl (inherent). 메서드 부착
먼저 고유 impl 의 예시부터 살펴보겠습니다.
impl Point {
fn origin() -> Point { Point { x: 0.0f64, y: 0.0f64 } }
fn distance(self, other: Point) -> f64 { ... }
}
이 형태는 impl 타입 { ... } 의 모습을 가지며, 본체는 함수 정의의 모음으로 이루어집니다. 그리고 이렇게 정의된 함수는 그 타입의 메서드가 됩니다.
고정 배열과 튜플에도 고유 impl 을 둘 수 있습니다. 표준 라이브러리는 이것으로 배열에 len() / is_empty() 를 줍니다.
impl<T, const N: usize> [T; N] {
fn len(self) -> usize { N }
}
impl<A, B> (A, B) {
fn swap(self) -> (B, A) { (self.1, self.0) }
}
트레잇 impl. 트레잇 구현
다음으로 트레잇 impl 의 예시를 살펴보겠습니다.
impl Describe for Point {
fn describe(self) -> str { f"({self.x}, {self.y})" }
}
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point { ... }
}
이 형태는 impl 트레잇 for 타입 { ... } 의 모습을 가집니다. 본체에는 trait 가 요구한 메서드의 본체와 연관 타입의 구체 정의가 옵니다. 한편 트레잇이 기본 메서드를 제공한 항목은 본체에서 생략할 수 있으며, 그러면 기본 본체를 사용합니다.
impl 메서드는 트레잇 선언의 매개변수 개수와, receiver 를 뺀 각 매개변수에 같은 ref 를 가져야 하며, 선언이 값을 돌려줄 때 정확히 값을 돌려줘야 합니다. 그렇지 않으면 impl 에서 에러이며, 그 에러는 선언을 보여 줍니다. impl 메서드의 throws 는 트레잇 메서드의 throws 와 같아야 합니다. impl 이 throws 를 새로 붙이거나 에러 타입을 넓힐 수 없습니다 (4.14.11).
Send / Sync 는 자동 marker 트레잇이며 두 가지 수동 선언이 있습니다 (stdlib 2.13.9).
- opt-in:
unsafe impl Send for X {}.unsafe impl은Send/Sync에만 쓸 수 있고, 이 두 트레잇의 수동 구현에는 반드시 필요합니다. - opt-out:
impl !Send for X {},impl<T> !Send for Cell<T> {}. 자동 구현을 막으며, 본체는 비어 있어야 합니다.!는Send/Sync에만 쓸 수 있습니다.
impl 안의 상수
impl 블록 안의 const N: T = v; 는 그 타입의 연관 상수입니다. 바깥에서는 Type::N 으로, impl 안에서는 Self::N 으로 읽습니다. 트레잇이 선언한 연관 상수 (2.8.3a) 는 트레잇 impl 에서 값을 정합니다.
impl Circle {
const UNIT: f64 = 1.0;
fn unit() -> Circle { Circle { r: Self::UNIT } }
}
let r = Circle::UNIT;
impl 의 대상이나 트레잇 인자는 projection (<T as Tr>::X) 이거나 뒤에 선언된 항목을 가리켜도 됩니다. 이것은 모든 항목을 등록한 뒤에 해소합니다.
impl 의 대상은 제네릭이든 아니든 타입 별칭을 가리킬 수 있습니다. 별칭은 펼쳐지며, 그 impl 은 별칭이 가리키는 타입으로 적은 것과 같습니다: type Al<T> = S<T>; 이면 impl<T> Al<T> { .. } 는 impl<T> S<T> { .. } 이고 impl<T> Tr for Al<T> 는 impl<T> Tr for S<T> 입니다. 인자 일부를 고정한 별칭 (type Names<V> = Dict<str, V>;) 은 그 인스턴스만 덮는 impl 을 줍니다.
2.9.2. 메서드의 self 관례
본체의 함수가 인스턴스 메서드가 되려면 첫 매개변수가 self (또는 ref self) 여야 합니다.
impl Point {
fn origin() -> Point { ... } // 정적 - Type::method() 호출
fn distance(self, other: Point) -> f64 { ... } // 인스턴스 - instance.method() 호출
fn replace_with(ref self, other: Point) { ... } // ref self - caller 의 변수 재할당 가능
}
self 매개변수의 정확한 의미와 ref self 의 동작은 함수 사양에서 정의합니다. 본 문서에서는 self 유무가 정적 메서드와 인스턴스 메서드를 구분한다는 점까지만 짚고 넘어가겠습니다.
2.9.3. 한 타입에 대한 여러 impl 블록
같은 타입에 대해 고유 impl 은 여러 블록으로 나누어도 됩니다.
impl Point {
fn origin() -> Point { ... }
}
impl Point {
fn distance(self, other: Point) -> f64 { ... }
}
이 형태는 시각적이고 조직적인 묶음일 뿐이며, 결과는 한 본체에 모은 것과 같습니다.
다만 같은 타입에 대해 같은 트레잇의 impl 은 한 번만 가능합니다.
impl Display for Point { ... }
impl Display for Point { ... } // ERROR - 중복 구현
2.9.4. 제네릭 impl
impl 도 제네릭으로 선언할 수 있습니다.
impl<T> List<T> {
fn len(self) -> usize { ... }
}
impl<T> Add for Pair<T> where T: Add<T> {
type Output = Pair<T::Output>;
fn add(self, rhs: Pair<T>) -> Pair<T::Output> { ... }
}
impl 뒤에 <...> 로 타입 매개변수를 선언하며, 이 매개변수는 대상 타입과 트레잇 인자, 연관 타입 어디에든 등장할 수 있습니다. 또한 where 절로 추가 제약을 둘 수 있는데, 이는 트레잇 선언과 동일한 방식입니다.
제네릭 impl 은 그 매개변수가 만들 수 있는 대상의 인스턴스를 덮습니다: impl<V> P<str, V> 는 P<str, i32> 에는 메서드를 주지만 P<i32, i32> 에는 주지 않습니다.
2.9.5. Orphan 규칙
다른 패키지에서 정의된 트레잇을 또 다른 패키지에서 정의된 타입에 구현하는 것은 금지합니다.
// 트레잇 Display, 타입 i32 가 모두 표준 라이브러리(다른 패키지) 정의
impl Display for i32 { ... } // ERROR - 둘 다 외부 패키지
따라서 트레잇이 현재 패키지 정의이거나, 아니면 대상 타입이 현재 패키지 정의이거나 둘 중 하나는 반드시 충족되어야 합니다. 내장 타입 (원시 타입, 튜플, 배열, 함수 타입) 은 rok::core 에서 정의된 것으로 칩니다.
이 규칙의 단위는 소스 단위 의 패키지입니다. 그러므로 같은 패키지 안의 다른 모듈끼리는 자유롭게 impl 할 수 있습니다.
이 규칙은 같은 (트레잇, 타입) 쌍에 대해 충돌하는 impl 이 두 패키지에서 동시에 만들어지지 않도록 하기 위한 장치이며, 일관성을 보장하기 위한 것입니다.
일관성 (coherence)
한 (트레잇, 타입) 쌍에는 impl 이 하나뿐이며, 특수화 (specialization) 는 없습니다.
- 제네릭
impl은 쓰이는 타입마다 단형화됩니다. - 구체
impl은 그 구체 타입이 제네릭impl의 바운드를 만족할 때만 제네릭impl과 겹칩니다. - 제네릭
impl두 개는 대상 (과 트레잇 인자) 이 단일화 (unify) 되면 겹치며, 선언 시점에 에러로 보고합니다. 단일화 뒤에도 제네릭으로 남는 매개변수의 바운드는 둘을 떼어 놓지 않습니다. 두 바운드를 모두 만족하는 타입이 아직 없어도 에러입니다 (Rust 의 coherence 와 같음). - 겹침을 따질 때
T?와T | none은 같은 타입입니다. - 부정 추론은 알려진 타입에만 합니다.
- 단일화로 구체 타입이 된 매개변수는 그 바운드를 검사합니다.
impl<U: Num> X for Pair<U, i32>와impl<U> X for Pair<str, U>는 단일화하면U = str인데str: Num이 아니므로 겹치지 않습니다. - 알려진 타입 생성자에 대한 바운드 (
List<T>: Iterator) 는, 그 트레잇의 impl 중 그 생성자나 맨 매개변수를 대상으로 하는 것이 없으면 둘을 떼어 놓습니다. 그래서impl<I: Iterator> IntoIterator for I와impl<T> IntoIterator for List<T>가 함께 있을 수 있습니다.
- 단일화로 구체 타입이 된 매개변수는 그 바운드를 검사합니다.
impl<T: Display> Describe for List<T> { ... }
impl<T: Hash> Describe for List<T> { ... } // ERROR - i32 가 Display 와 Hash 를 모두 만족
impl<T: Shape> Area for T { ... }
impl<T: Color> Area for T { ... } // ERROR - T 와 T 가 단일화됨 (바운드 무관)
impl<T: Display> Describe for T { ... }
impl Describe for i32 { ... } // ERROR - i32: Display 라서 위 impl 과 겹침
2.9.6. 메서드 가시성
고유 impl 의 메서드 가시성은 기본이 모듈 비공개입니다. 같은 모듈 안에서는 자유롭게 호출할 수 있으며, 외부에 노출하려면 pub 을 명시해야 합니다.
impl Account {
pub fn balance(self) -> f64 { self.amount } // 외부 공개
fn _internal_check(self) { ... } // 모듈 내부만
}
이러한 결정은 struct 와 enum 의 가시성 모델과 일치합니다. impl 블록 자체는 시각적 묶음일 뿐 가시성 단위가 아니며, 가시성 단위는 어디까지나 모듈입니다.
한편 트레잇 impl 의 메서드는 그 트레잇의 가시성을 따르므로, 따로 표시할 수 없습니다.
2.9.7. 고유 impl vs 트레잇 impl 의 메서드 충돌
같은 메서드 이름이 고유 impl 과 트레잇 impl 양쪽에 있으면, 호출 지점에서 모호하지 않은 방식으로 선택해야 합니다. 정확한 해소 규칙은 메서드 에서 정의합니다.
2.9.8. 동일 메서드 두 번 정의 금지
같은 타입에 같은 이름의 메서드를 두 번 정의하면 컴파일 에러입니다. 한 impl 블록 안이든 여러 고유 impl 블록 (2.9.3) 에 걸쳐서든, 한 트레잇 impl 안에서든 마찬가지입니다 (오버로딩 없음). 서로 다른 트레잇에서 온 같은 이름의 메서드는 허용됩니다 (2.9.7).