미래 대비
봉인된 트레이트는 다운스트림 구현으로부터 보호합니다 (C-SEALED)
어떤 트레이트는 그것을 정의한 크레이트 안에서만 구현되도록 의도됩니다. 이런 경우 봉인된 트레이트 패턴을 사용하면 호환성을 깨지 않고 트레이트를 변경할 수 있는 여지를 유지할 수 있습니다.
#![allow(unused)]
fn main() {
/// This trait is sealed and cannot be implemented for types outside this crate.
pub trait TheTrait: private::Sealed {
// Zero or more methods that the user is allowed to call.
fn ...();
// Zero or more private methods, not allowed for user to call.
#[doc(hidden)]
fn ...();
}
// Implement for some types.
impl TheTrait for usize {
/* ... */
}
mod private {
pub trait Sealed {}
// Implement for those same types, but no others.
impl Sealed for usize {}
}
}
비어 있는 비공개 Sealed 슈퍼트레이트는 다운스트림 크레이트에서 이름으로 참조할 수 없으므로, Sealed(따라서 TheTrait)의 구현은 현재 크레이트 안에만 존재함이 보장됩니다. 봉인되지 않은 트레이트라면 일반적으로 호환성을 깨는 변경이 되겠지만, TheTrait에는 호환성을 깨지 않는 릴리스에서도 자유롭게 메서드를 추가할 수 있습니다. 또한 공개적으로 문서화되지 않은 메서드의 시그니처도 자유롭게 변경할 수 있습니다.
다만 봉인된 트레이트에서 공개 메서드를 제거하거나 공개 메서드의 시그니처를 변경하는 것은 여전히 호환성을 깨는 변경이라는 점에 유의하십시오.
사용자가 트레이트를 구현하려다 좌절하는 일이 없도록, 해당 트레이트가 봉인되어 있으며 현재 크레이트 밖에서 구현하도록 의도된 것이 아니라는 점을 rustdoc에 문서화해야 합니다.
예시
구조체는 비공개 필드를 가집니다 (C-STRUCT-PRIVATE)
필드를 공개하는 것은 강한 약속입니다. 이는 표현 방식의 선택을 고정하며, 또한 클라이언트가 필드를 임의로 변경할 수 있으므로 타입이 필드 내용에 대해 어떠한 검증도 제공하거나 불변 조건을 유지할 수 없게 만듭니다.
공개 필드는 C의 정신을 따르는 struct 타입, 즉 복합적이고 수동적인 데이터 구조에 가장 적합합니다. 그렇지 않은 경우에는 필드를 숨기고 대신 게터/세터 메서드를 제공하는 것을 고려하십시오.
newtype은 구현 세부 사항을 캡슐화합니다 (C-NEWTYPE-HIDE)
newtype을 사용하면 클라이언트에게 정확한 약속을 하면서도 표현의 세부 사항을 숨길 수 있습니다.
예를 들어, 복합 이터레이터 타입을 반환하는 함수 my_transform을 생각해 봅시다.
#![allow(unused)]
fn main() {
use std::iter::{Enumerate, Skip};
pub fn my_transform<I: Iterator>(input: I) -> Enumerate<Skip<I>> {
input.skip(3).enumerate()
}
}
클라이언트에게는 이 타입을 숨겨서, 클라이언트가 보는 반환 타입이 대략 Iterator<Item = (usize, T)>가 되도록 하고 싶습니다. newtype 패턴을 사용하면 이렇게 할 수 있습니다.
#![allow(unused)]
fn main() {
use std::iter::{Enumerate, Skip};
pub struct MyTransformResult<I>(Enumerate<Skip<I>>);
impl<I: Iterator> Iterator for MyTransformResult<I> {
type Item = (usize, I::Item);
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
pub fn my_transform<I: Iterator>(input: I) -> MyTransformResult<I> {
MyTransformResult(input.skip(3).enumerate())
}
}
이렇게 newtype을 사용하면 시그니처가 단순해질 뿐 아니라 클라이언트에게 더 적게 약속할 수 있습니다. 클라이언트는 결과 이터레이터가 어떻게 구성되거나 표현되는지 알지 못하므로, 앞으로 클라이언트 코드를 깨뜨리지 않고 표현을 변경할 수 있습니다.
Rust 1.26에서는 impl Trait 기능도 도입되었는데, 이는 newtype 패턴보다 간결하지만 몇 가지 추가적인 트레이드오프가 있습니다. 즉, impl Trait를 사용하면 표현할 수 있는 것이 제한됩니다. 예를 들어, Debug나 Clone, 또는 다른 이터레이터 확장 트레이트의 조합을 구현하는 이터레이터를 반환하는 것은 문제가 될 수 있습니다. 요약하면, 반환 타입으로서의 impl Trait는 내부 API에는 아주 적합할 것이고 공개 API에도 적절할 수 있지만, 모든 경우에 그렇지는 않을 것입니다. 자세한 내용은 Edition Guide의 “impl Trait로 복잡한 타입을 손쉽게 반환하기” 절을 참고하십시오.
#![allow(unused)]
fn main() {
pub fn my_transform<I: Iterator>(input: I) -> impl Iterator<Item = (usize, I::Item)> {
input.skip(3).enumerate()
}
}
데이터 구조체는 파생된 트레이트 바운드를 중복하지 않습니다 (C-STRUCT-BOUNDS)
제네릭 데이터 구조는 파생될 수 있거나 그 밖에 의미상의 가치를 더하지 않는 트레이트 경계를 사용해서는 안 됩니다. derive 속성에 있는 각 트레이트는 별도의 impl 블록으로 확장되며, 이 블록은 해당 트레이트를 구현하는 제네릭 인수에만 적용됩니다.
#![allow(unused)]
fn main() {
// Prefer this:
#[derive(Clone, Debug, PartialEq)]
struct Good<T> { /* ... */ }
// Over this:
#[derive(Clone, Debug, PartialEq)]
struct Bad<T: Clone + Debug + PartialEq> { /* ... */ }
}
파생된 트레이트를 Bad의 경계로 중복해서 지정하는 것은 불필요하며 하위 호환성을 위협합니다. 이를 설명하기 위해, 앞의 예제에 나온 구조체에 PartialOrd를 파생하는 경우를 생각해 봅시다.
#![allow(unused)]
fn main() {
// Non-breaking change:
#[derive(Clone, Debug, PartialEq, PartialOrd)]
struct Good<T> { /* ... */ }
// Breaking change:
#[derive(Clone, Debug, PartialEq, PartialOrd)]
struct Bad<T: Clone + Debug + PartialEq + PartialOrd> { /* ... */ }
}
일반적으로 데이터 구조에 트레이트 경계를 추가하는 것은 호환성을 깨는 변경입니다. 해당 구조체를 사용하는 모든 쪽에서 추가된 경계를 새로 충족해야 하기 때문입니다. derive 속성을 사용해 표준 라이브러리의 트레이트를 더 파생하는 것은 호환성을 깨는 변경이 아닙니다.
다음 트레이트는 데이터 구조의 경계에 절대 사용해서는 안 됩니다.
ClonePartialEqPartialOrdDebugDisplayDefaultErrorSerializeDeserializeDeserializeOwned
Read나 Write처럼 파생할 수 없으면서 구조체 정의에 반드시 필요하지는 않은 그 밖의 트레이트 경계는 애매한 영역에 있습니다. 이러한 경계는 타입의 정의에서 의도한 동작을 더 잘 드러낼 수 있지만, 향후 확장성을 제한하기도 합니다. 그래도 데이터 구조에 의미상 유용한 트레이트 경계를 포함하는 것은 파생 가능한 트레이트를 경계로 포함하는 것보다는 문제가 덜합니다.
예외
구조체에 트레이트 경계가 필요한 예외는 세 가지입니다.
- 데이터 구조가 트레이트의 연관 타입을 참조하는 경우입니다.
- 경계가
?Sized인 경우입니다. - 데이터 구조에 트레이트 경계가 필요한
Dropimpl이 있는 경우입니다. 현재 Rust에서는Dropimpl에 있는 모든 트레이트 경계가 데이터 구조에도 있어야 합니다.
표준 라이브러리의 예
std::borrow::Cow는Borrow트레이트의 연관 타입을 참조합니다.std::boxed::Box는 암시적인Sized경계에서 벗어납니다.std::io::BufWriter는Dropimpl에서 트레이트 경계를 요구합니다.