부록 C: 파생 가능한 트레이트
부록 C: 파생 가능한 트레이트 (Derivable Traits)
책 곳곳에서 derive 애트리뷰트를 이야기했었죠. struct나 enum 정의에 붙여서 쓰는 그 애트리뷰트인데, derive는 여러분이 지정한 타입에 기본 구현이 담긴 트레이트를 자동으로 구현해 주는 코드를 생성해 줍니다. 이번 부록에서는 표준 라이브러리에서 derive로 쓸 수 있는 모든 트레이트를 정리해 볼게요. 각 항목에서는 다음을 다룹니다.
- 이 트레이트를 파생하면 어떤 연산자와 메서드를 쓸 수 있는지
derive가 제공하는 트레이트 구현이 실제로 무엇을 하는지- 이 트레이트를 구현한다는 게 해당 타입에 대해 무엇을 뜻하는지
- 이 트레이트를 구현할 수 있는 조건과 없는 조건
- 이 트레이트가 필요한 연산의 예시
derive가 주는 동작과 다른 결과를 원한다면, 표준 라이브러리 문서에서 각 트레이트를 직접 구현하는 방법을 참고하세요.
여기 나열된 트레이트가 표준 라이브러리에서 여러분의 타입에 derive로 구현할 수 있는 전부입니다. 표준 라이브러리에 있는 다른 트레이트들은 기본 동작이 딱히 합리적이지 않아서, 여러분이 하려는 일에 맞게 직접 구현해야 해요.
derive로 만들 수 없는 트레이트의 예로 Display를 들 수 있어요. 이 트레이트는 최종 사용자를 위한 형식 출력을 담당하죠. 어떤 타입을 최종 사용자에게 어떻게 보여줄지는 항상 생각해 봐야 합니다. 사용자가 타입의 어느 부분을 볼 수 있어야 할까요? 어떤 부분이 유용할까요? 어떤 데이터 형식이 가장 적절할까요? Rust 컴파일러는 이런 판단을 할 수 없어서, 여러분을 대신해 적절한 기본 동작을 제공해 줄 수 없어요.
이 부록이 소개하는 파생 가능 트레이트 목록이 전부는 아닙니다. 라이브러리는 자기만의 트레이트에 derive를 구현할 수 있어서, derive를 쓸 수 있는 트레이트 목록은 사실상 열려 있어요. derive를 구현하는 일은 프로시저 매크로를 사용하는 건데, 이 내용은 20장의 "Custom derive Macros" 절에서 다룹니다.
출처: Rust 공식문서
본문
Debug — 프로그래머를 위한 출력
Debug 트레이트는 형식 문자열에서 디버그 출력을 가능하게 해 주는데, {} 자리표시자 안에 :?를 넣는 방식으로 씁니다.
Debug를 구현하면 타입의 인스턴스를 디버깅 목적으로 출력할 수 있어요. 그래서 여러분을 포함해 이 타입을 쓰는 프로그래머들이 프로그램 실행 중 특정 지점에서 인스턴스를 들여다볼 수 있게 됩니다.
예를 들어 assert_eq! 매크로를 쓸 때 Debug가 필요해요. 이 매크로는 동등성 검사가 실패하면 인자로 받은 인스턴스 값을 출력해서, 왜 두 인스턴스가 같지 않았는지 프로그래머가 확인할 수 있게 해 줍니다.
PartialEq와 Eq — 동등성 비교
PartialEq 트레이트는 타입의 인스턴스를 비교해 동등한지 확인할 수 있게 하고, ==와 != 연산자를 쓸 수 있게 해 줍니다.
PartialEq를 파생하면 eq 메서드가 구현됩니다. struct에서 PartialEq를 파생하면 두 인스턴스는 모든 필드가 같을 때만 같고, 어느 필드 하나라도 다르면 같지 않다고 판단해요. enum에서 파생하면 각 variant는 자기 자신과만 같고 다른 variant와는 같지 않습니다.
assert_eq! 매크로와 함께 쓸 때 PartialEq가 필요한데, 이 매크로는 타입의 두 인스턴스를 동등한지 비교할 수 있어야 하거든요.
Eq 트레이트에는 메서드가 없어요. 그 목적은 표시용(signal)이에요. 즉 애노테이션한 타입의 모든 값에 대해 그 값이 자기 자신과 같다는 뜻을 나타내는 거죠. Eq는 PartialEq도 구현하는 타입에만 적용할 수 있어요. 다만 PartialEq를 구현한다고 전부 Eq를 구현할 수 있는 건 아닙니다. 대표적인 예가 부동소수점 숫자 타입인데, 부동소수점 구현은 not-a-number(NaN) 값의 두 인스턴스가 서로 같지 않다고 규정하고 있어요.
Eq가 필요한 경우로는 HashMap<K, V>의 키로 쓸 때가 있어요. 그래야 HashMap<K, V>가 두 키가 같은지 판별할 수 있으니까요.
PartialOrd와 Ord — 순서 비교
PartialOrd 트레이트는 타입의 인스턴스를 정렬 목적으로 비교할 수 있게 합니다. PartialOrd를 구현한 타입은 <, >, <=, >= 연산자와 함께 쓸 수 있어요. PartialOrd는 PartialEq도 구현하는 타입에만 적용할 수 있습니다.
PartialOrd를 파생하면 partial_cmp 메서드가 구현되는데, 이 메서드는 Option<Ordering>을 반환하고, 주어진 값들이 순서를 만들지 못할 때는 None이 됩니다. 대부분의 값은 비교할 수 있음에도 순서를 만들지 못하는 값의 예가 부동소수점의 NaN 값이에요. 어떤 부동소수점 숫자와 NaN 부동소수점 값으로 partial_cmp를 호출하면 None이 반환됩니다.
struct에서 PartialOrd를 파생하면, 구조체 정의에 필드가 나타나는 순서대로 각 필드의 값을 비교해 두 인스턴스를 비교해요. enum에서 파생하면, enum 정의에서 더 앞에 선언된 variant가 더 뒤에 나열된 variant보다 작은 것으로 간주됩니다.
rand 크레이트의 gen_range 메서드 같은 곳에서 PartialOrd가 필요해요. 이 메서드는 범위 표현식이 지정한 범위 안에서 무작위 값을 생성하는데, 순서 비교가 가능해야 하거든요.
Ord 트레이트는 애노테이션한 타입의 어느 두 값에 대해서든 유효한 순서가 항상 존재함을 알 수 있게 합니다. Ord는 cmp 메서드를 구현하는데, 유효한 순서가 항상 존재하므로 Option<Ordering>이 아니라 Ordering을 반환해요. Ord는 PartialOrd와 Eq도 구현하는 타입에만 적용할 수 있습니다(Eq는 다시 PartialEq를 요구하죠). struct와 enum에서 파생하면 cmp는 PartialOrd의 파생 구현에서 partial_cmp가 동작하는 방식과 똑같이 동작합니다.
Ord가 필요한 경우는 값을 BTreeSet<T>에 저장할 때예요. 이 자료구조는 값의 정렬 순서를 기준으로 데이터를 저장하거든요.
Clone과 Copy — 값을 복제하기
Clone 트레이트는 값을 명시적으로 깊은 복사(deep copy)할 수 있게 해 주는데, 이 복제 과정에는 임의의 코드가 실행되고 힙 데이터가 복사되는 일이 포함될 수 있어요. Clone에 대한 자세한 내용은 4장의 "Variables and Data Interacting with Clone" 절을 참고하세요.
Clone을 파생하면 clone 메서드가 구현되는데, 이 메서드가 타입 전체에 대해 구현되면 타입의 각 부분에 대해 clone을 호출합니다. 즉 Clone을 파생하려면 타입의 모든 필드나 값도 Clone을 구현해야 한다는 뜻이에요.
Clone이 필요한 예는 슬라이스에서 to_vec 메서드를 호출할 때예요. 슬라이스는 담고 있는 타입 인스턴스를 소유하지 않지만, to_vec이 반환하는 벡터는 자기 인스턴스를 소유해야 해서 to_vec이 각 항목에 대해 clone을 호출합니다. 그래서 슬라이스에 저장된 타입은 Clone을 구현해야 합니다.
Copy 트레이트는 스택에 저장된 비트만 복사해 값을 복제할 수 있게 합니다. 임의의 코드는 필요 없어요. Copy에 대한 자세한 내용은 4장의 "Stack-Only Data: Copy" 절을 참고하세요.
Copy는 메서드를 정의하지 않아요. 프로그래머가 그 메서드를 오버로드해 임의의 코드가 실행되지 않는다는 가정을 어기는 일을 막기 위해서예요. 그래서 모든 프로그래머는 값을 복사하는 것이 매우 빠르다고 가정할 수 있습니다.
모든 부분이 Copy를 구현하는 타입에는 Copy를 파생할 수 있어요. Copy를 구현하는 타입은 반드시 Clone도 구현해야 하죠. Copy를 구현하는 타입은 Copy와 같은 작업을 수행하는 간단한(trivial) Clone 구현을 갖기 때문입니다.
Copy는 거의 요구되지 않아요. Copy를 구현한 타입에는 최적화가 적용되어 clone을 호출할 필요가 없으니 코드가 더 간결해지죠.
Copy로 할 수 있는 모든 것은 Clone으로도 할 수 있지만, 코드가 더 느려지거나 clone을 써야 하는 부분이 생길 수 있어요.
Hash — 값을 고정 크기 값으로 매핑
Hash 트레이트는 임의 크기의 타입 인스턴스를 해시 함수로 고정 크기의 값에 매핑할 수 있게 합니다. Hash를 파생하면 hash 메서드가 구현되는데, 파생 구현은 타입의 각 부분에서 hash를 호출한 결과를 합칩니다. 즉 Hash를 파생하려면 모든 필드나 값도 Hash를 구현해야 합니다.
Hash가 필요한 예는 HashMap<K, V>에 키를 저장해 데이터를 효율적으로 저장할 때예요.
Default — 기본값 만들기
Default 트레이트는 타입의 기본값을 만들 수 있게 합니다. Default를 파생하면 default 함수가 구현되는데, 파생 구현은 타입의 각 부분에서 default 함수를 호출합니다. 그래서 Default를 파생하려면 타입의 모든 필드나 값도 Default를 구현해야 해요.
Default::default 함수는 5장의 "Creating Instances from Other Instances with Struct Update Syntax" 절에서 다룬 struct 갱신 문법과 함께 자주 쓰입니다. struct의 일부 필드는 직접 설정하고, 나머지 필드는 ..Default::default()로 기본값을 지정해 쓸 수 있어요.
Default가 필요한 예로는 Option<T> 인스턴스에서 unwrap_or_default 메서드를 쓸 때가 있어요. Option<T>가 None이면 unwrap_or_default 메서드는 Option<T>에 저장된 타입 T에 대한 Default::default의 결과를 반환합니다.
더 알아보기
- 부록: 트레이트 관련 용어 정리 및 표준 라이브러리 문서
- 트레이트와 애트리뷰트의 기초는 본문 10장과 5장에서 자세히 다뤄요.
derive가 어떻게 프로시저 매크로로 확장되는지 보고 싶다면 Chapter 20의 "CustomderiveMacros" 절을 살펴보세요.