부록 C: 파생 가능한 트레이트

이 책의 여러 부분에서 derive 속성에 대해 다뤘다. 이 속성은 구조체나 열거형 정의에 적용할 수 있으며, derive 문법으로 주석을 단 타입에 대해 기본 구현을 포함한 트레이트를 자동으로 구현하는 코드를 생성한다.

이 부록에서는 표준 라이브러리에서 derive와 함께 사용할 수 있는 모든 트레이트를 참조로 제공한다. 각 섹션에서는 다음 내용을 다룬다:

  • 해당 트레이트를 파생할 때 활성화되는 연산자와 메서드
  • derive가 제공하는 트레이트 구현의 동작 방식
  • 트레이트를 구현하는 것이 타입에 대해 의미하는 바
  • 트레이트를 구현할 수 있는 조건과 불가능한 조건
  • 해당 트레이트가 필요한 연산의 예시

derive 속성이 제공하는 동작과 다른 동작을 원한다면, 각 트레이트에 대한 표준 라이브러리 문서를 참고해 수동으로 구현하는 방법을 확인할 수 있다.

여기에 나열된 트레이트는 표준 라이브러리에서 정의된 것 중 derive를 사용해 타입에 구현할 수 있는 유일한 트레이트들이다. 표준 라이브러리에 정의된 다른 트레이트들은 기본 동작이 명확하지 않으므로, 목적에 맞게 직접 구현해야 한다.

파생할 수 없는 트레이트의 예로는 최종 사용자를 위한 포맷팅을 처리하는 Display가 있다. 타입을 최종 사용자에게 어떻게 표시할지 항상 고려해야 한다. 최종 사용자가 볼 수 있는 부분은 어디인가? 어떤 부분이 관련성이 있는가? 데이터의 어떤 포맷이 가장 적합한가? Rust 컴파일러는 이러한 통찰력을 가지고 있지 않으므로 적절한 기본 동작을 제공할 수 없다.

이 부록에서 제공하는 파생 가능한 트레이트 목록은 완전하지 않다. 라이브러리들은 자체 트레이트에 대해 derive를 구현할 수 있으므로, derive와 함께 사용할 수 있는 트레이트 목록은 사실상 무한하다. derive를 구현하려면 프로시저 매크로를 사용해야 하며, 이는 20장의 “매크로” 섹션에서 다룬다.

프로그래머를 위한 Debug 출력

Debug 트레이트는 포맷 문자열에서 디버그 포맷팅을 가능하게 한다. {} 플레이스홀더 안에 :?를 추가하면 이를 사용할 수 있다.

Debug 트레이트는 디버깅 목적으로 타입의 인스턴스를 출력할 수 있게 해준다. 이를 통해 여러분과 다른 프로그래머들은 프로그램 실행 중 특정 지점에서 해당 인스턴스를 확인할 수 있다.

예를 들어, Debug 트레이트는 assert_eq! 매크로를 사용할 때 필수적이다. 이 매크로는 동등성 검증이 실패할 경우 인자로 주어진 인스턴스의 값을 출력한다. 이를 통해 프로그래머는 두 인스턴스가 왜 동일하지 않은지 확인할 수 있다.

PartialEqEq를 통한 동등성 비교

PartialEq 트레이트는 타입의 인스턴스 간 동등성을 비교할 수 있게 해주며, ==!= 연산자를 사용할 수 있도록 한다.

PartialEq를 파생(derive)하면 eq 메서드가 구현된다. 구조체에 PartialEq를 파생할 경우, 두 인스턴스는 모든 필드가 동일할 때만 동등하다고 판단된다. 필드 중 하나라도 다르면 두 인스턴스는 동등하지 않다. 열거형에 PartialEq를 파생할 경우, 각 변형은 자기 자신과 동등하며 다른 변형과는 동등하지 않다.

PartialEq 트레이트는 예를 들어 assert_eq! 매크로를 사용할 때 필요하다. 이 매크로는 타입의 두 인스턴스를 비교할 수 있어야 하기 때문이다.

Eq 트레이트는 메서드가 없다. 이 트레이트의 목적은 주석이 달린 타입의 모든 값이 자기 자신과 동등함을 나타내는 것이다. Eq 트레이트는 PartialEq를 구현한 타입에만 적용할 수 있지만, PartialEq를 구현한 모든 타입이 Eq를 구현할 수 있는 것은 아니다. 예를 들어 부동소수점 숫자 타입은 NaN 값이 서로 동등하지 않다고 정의되어 있기 때문에 Eq를 구현할 수 없다.

Eq가 필요한 경우의 예로는 HashMap<K, V>의 키가 있다. HashMap<K, V>는 두 키가 동일한지 여부를 판단할 수 있어야 하기 때문이다.

정렬 비교를 위한 PartialOrdOrd

PartialOrd 트레이트는 타입의 인스턴스를 정렬 목적으로 비교할 수 있게 해준다. PartialOrd를 구현한 타입은 <, >, <=, >= 연산자를 사용할 수 있다. PartialOrd 트레이트는 PartialEq를 구현한 타입에만 적용할 수 있다.

PartialOrd를 파생시키면 partial_cmp 메서드가 구현된다. 이 메서드는 Option<Ordering>을 반환하며, 주어진 값들이 정렬 순서를 만들 수 없을 때 None을 반환한다. 예를 들어, 대부분의 값이 비교 가능한 타입이라도, NaN(Not-a-Number) 부동소수점 값은 정렬 순서를 만들 수 없다. 어떤 부동소수점 숫자와 NaN 값을 partial_cmp로 비교하면 None이 반환된다.

구조체에 PartialOrd를 파생시키면, 두 인스턴스를 비교할 때 구조체 정의에서 필드가 나타나는 순서대로 각 필드의 값을 비교한다. 열거형에 PartialOrd를 파생시키면, 열거형 정의에서 먼저 선언된 변형이 나중에 선언된 변형보다 작은 것으로 간주된다.

PartialOrd 트레이트는 예를 들어 rand 크레이트의 gen_range 메서드에서 필요하다. 이 메서드는 범위 표현식으로 지정된 범위 내에서 랜덤한 값을 생성한다.

Ord 트레이트는 주석이 달린 타입의 어떤 두 값에 대해서도 유효한 정렬 순서가 존재함을 보장한다. Ord 트레이트는 cmp 메서드를 구현하며, 이 메서드는 항상 유효한 정렬 순서가 가능하기 때문에 Option<Ordering>이 아닌 Ordering을 반환한다. Ord 트레이트는 PartialOrdEq를 구현한 타입에만 적용할 수 있다. (EqPartialEq를 필요로 한다.) 구조체와 열거형에 Ord를 파생시키면, cmpPartialOrd에서 파생된 partial_cmp 구현과 동일하게 동작한다.

Ord가 필요한 예시 중 하나는 BTreeSet<T>에 값을 저장할 때다. BTreeSet<T>은 값의 정렬 순서를 기준으로 데이터를 저장하는 데이터 구조다.

값 복제를 위한 CloneCopy

Clone 트레이트는 값을 명시적으로 깊은 복사(deep copy)할 수 있게 해준다. 이 복제 과정은 임의의 코드를 실행하거나 힙 데이터를 복사하는 작업을 포함할 수 있다. Clone에 대한 더 자세한 내용은 4장의 “Variables and Data Interacting with Clone”을 참고한다.

Clone을 파생(derive)하면 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와 동일한 작업을 수행하는 간단한 Clone 구현을 가지기 때문이다.

Copy 트레이트는 거의 필요하지 않다. Copy를 구현하는 타입은 최적화가 가능하며, 이는 clone을 호출할 필요가 없음을 의미한다. 따라서 코드가 더 간결해진다.

Copy로 할 수 있는 모든 작업은 Clone으로도 수행할 수 있지만, 코드가 더 느려지거나 일부 위치에서 clone을 사용해야 할 수 있다.

고정 크기 값으로 매핑하기 위한 Hash 트레이트

Hash 트레이트는 임의의 크기를 가진 타입의 인스턴스를 해시 함수를 사용해 고정된 크기의 값으로 매핑할 수 있게 해준다. Hash를 파생(derive)하면 hash 메서드가 구현된다. 파생된 hash 메서드의 구현은 타입의 각 부분에 대해 hash를 호출한 결과를 결합한다. 즉, 모든 필드나 값도 Hash를 구현해야 Hash를 파생할 수 있다.

Hash가 필요한 예시 중 하나는 HashMap<K, V>에서 키를 저장할 때다. 이를 통해 데이터를 효율적으로 저장할 수 있다.

기본값을 위한 Default 트레이트

Default 트레이트는 타입에 대한 기본값을 생성할 수 있게 해준다. Default를 파생(derive)하면 default 함수가 구현된다. 파생된 default 함수는 타입의 각 부분에 대해 default 함수를 호출한다. 즉, 타입의 모든 필드나 값도 Default를 구현해야 Default를 파생할 수 있다.

Default::default 함수는 5장에서 다룬 구조체 업데이트 문법과 함께 자주 사용된다. 구조체의 일부 필드를 커스터마이징한 후, 나머지 필드에 대해 기본값을 설정하고 사용하려면 ..Default::default()를 사용하면 된다.

예를 들어, Option<T> 인스턴스에서 unwrap_or_default 메서드를 사용할 때 Default 트레이트가 필요하다. Option<T>None인 경우, unwrap_or_default 메서드는 Option<T>에 저장된 타입 T에 대한 Default::default의 결과를 반환한다.