본문 바로가기

C - 파생 가능한 트레이트

derive로 Debug·비교·정렬·복제·해시·기본값 트레이트를 자동 구현할 때 타입이 충족해야 할 조건을 정리합니다.

derive는 코드를 줄여 주는 편의 기능이지만, 어떤 트레이트를 자동으로 구현할지 정하는 순간 그 타입을 비교할 수 있는지, 정렬할 수 있는지, 복제할 수 있는지 같은 성질도 함께 정해집니다. 그래서 이 부록은 무엇을 자동으로 만들 수 있는지와 함께, 그것이 타입에 어떤 뜻을 주는지 확인하는 참고 자료로 읽으면 좋습니다. 아래 그림은 내장 파생 트레이트 아홉 개가 서로 무엇을 요구하는지 보여 줍니다.

Rust의 내장 파생 트레이트 아홉 개와 Copy, Ord, Eq, PartialOrd, PartialEq, Clone 사이의 직접 의존 관계를 나타낸 그래프

Rust · derive · dependency graph

내장 파생 9개의 선행 계약을 연결한다

Copy는 Clone을, Ord는 Eq와 PartialOrd를 요구합니다. Eq와 PartialOrd는 각각 PartialEq에 모이며, 나머지 세 트레이트는 내장 트레이트 선행 조건이 없습니다.

Rust 내장 derive 트레이트 의존 그래프 Copy에서 Clone으로, Ord에서 Eq와 PartialOrd로, Eq와 PartialOrd에서 PartialEq로 이어지는 의존 관계와 독립적인 Debug, Default, Hash를 함께 보여줍니다. Copy Ord Debug Default Hash Clone Eq PartialOrd PartialEq

Copy → Clone

Copy는 Clone의 하위 트레이트입니다. 모든 필드도 Copy여야 하고 타입은 Drop을 구현할 수 없습니다.

Ord → Eq + PartialOrd → PartialEq

Eq와 PartialOrd가 각각 PartialEq를 요구하므로 비교 계약은 한 지점으로 모입니다.

Debug

{:?} 프로그래머 출력을 만듭니다. 사용자 표시용 Display는 내장 파생 대상이 아닙니다.

Default

구조체는 모든 필드가 Default여야 하며, 열거형은 유닛 배리언트 하나를 #[default]로 고릅니다.

Hash

내장 트레이트 상속선은 없지만 해시 키에서는 Eq와 함께 동등한 값의 해시가 같다는 계약을 지킵니다.

  • 화살표는 직접 요구하는 트레이트를 가리킴
  • PartialEq는 두 트레이트가 요구하는 공통 의존점

복제 계약

Clone은 타입별 명시 복제이고 깊은 복사를 보장하지 않습니다. Copy는 저장 위치와 무관한 암시적 비트 복사입니다.

정렬 계약

구조체는 필드 순서, 열거형은 판별값을 우선 비교합니다. 명시적 판별값은 기본 선언 순서를 바꿀 수 있습니다.

목록의 경계

내장 파생은 정확히 아홉 개입니다. 라이브러리는 절차적 매크로로 사용자 정의 파생을 추가할 수 있습니다.

선이 없다는 것은 트레이트가 쓸모없다는 뜻이 아니라 다른 내장 트레이트를 선행 조건으로 요구하지 않는다는 뜻입니다.

Copy는 Clone을 요구하고, Ord는 Eq와 PartialOrd를 요구합니다. Eq와 PartialOrd는 각각 PartialEq를 요구하므로, 비교에 관한 요구는 PartialEq 한곳에 모입니다. Debug, Default, Hash는 다른 내장 트레이트를 먼저 요구하지 않습니다. 선이 없다는 것은 쓸모가 없다는 뜻이 아니라, 다른 내장 트레이트를 먼저 구현하지 않아도 된다는 뜻입니다. 각 트레이트의 세부 사항은 이 부록에서 차례로 봅니다.

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

이 교재의 여러 곳에서 구조체나 열거형의 정의에 붙일 수 있는 derive 속성을 이야기했습니다. derive 속성은 derive 문법으로 표시한 타입에, 기본 구현을 가진 트레이트를 구현하는 코드를 만들어 냅니다.

이 부록에서는 표준 라이브러리에서 derive와 함께 쓸 수 있는 모든 트레이트를 참고 자료로 정리합니다. 각 절에서는 다음을 다룹니다.

다루는 것
이 트레이트를 파생하면 쓸 수 있게 되는 연산자와 메서드
derive가 제공하는 트레이트 구현이 하는 일
그 트레이트를 구현한다는 것이 타입에 대해 뜻하는 것
그 트레이트를 구현할 수 있거나 없는 조건
그 트레이트가 필요한 연산의 예

derive 속성이 제공하는 것과 다른 동작을 원한다면, 각 트레이트를 직접 구현하는 방법은 표준 라이브러리 문서를 참고하세요.

여기 나열한 트레이트는 표준 라이브러리에 정의된 트레이트 가운데 derive로 타입에 구현할 수 있는 것뿐입니다. 표준 라이브러리에 정의된 다른 트레이트에는 알맞은 기본 동작이 없으므로, 이루려는 목적에 맞는 방식으로 직접 구현해야 합니다.

파생할 수 없는 트레이트의 예는 최종 사용자를 위한 서식을 다루는 Display입니다. 최종 사용자에게 타입을 보여 주는 알맞은 방법은 늘 따로 생각해야 합니다. 최종 사용자에게 타입의 어떤 부분을 보여 주어야 할까요? 어떤 부분이 관련 있을까요? 어떤 형식의 데이터가 가장 관련 있을까요? 러스트 컴파일러는 이런 판단을 할 수 없으므로, 알맞은 기본 동작을 제공할 수 없습니다.

이 부록의 파생 가능한 트레이트 목록이 전부는 아닙니다. 라이브러리는 자기 트레이트에 derive를 구현할 수 있으므로, derive를 쓸 수 있는 트레이트의 목록은 사실상 열려 있습니다. derive를 구현하려면 절차적 매크로를 써야 하며, 이것은 매크로에서 다룹니다.

프로그래머 출력을 위한 Debug

Debug 트레이트는 서식 문자열에서 디버그 서식을 쓸 수 있게 해 주며, {} 자리 표시자 안에 :?를 더해 나타냅니다.

Debug 트레이트로 디버깅을 위해 타입의 인스턴스를 출력할 수 있으므로, 여러분과 타입을 쓰는 다른 프로그래머가 프로그램 실행의 특정 시점에 인스턴스를 들여다볼 수 있습니다.

Debug 트레이트는 예를 들어 assert_eq! 매크로를 쓸 때 필요합니다. 이 매크로는 같음 단언이 실패하면 인수로 받은 인스턴스의 값을 출력해, 프로그래머가 두 인스턴스가 왜 같지 않았는지 볼 수 있게 합니다. {:#?}를 쓰면 여러 줄로 보기 좋게 출력합니다.

동등 비교를 위한 PartialEq 및 Eq

PartialEq 트레이트로 타입의 인스턴스를 비교해 같은지 확인할 수 있으며, ==와 != 연산자를 쓸 수 있게 됩니다.

PartialEq를 파생하면 eq 메서드가 구현됩니다. 구조체에 PartialEq를 파생하면, 두 인스턴스는 모든 필드가 같을 때만 같고, 어느 필드든 같지 않으면 같지 않습니다. 열거형에 파생하면, 각 배리언트는 자기 자신과 같고 다른 배리언트와는 같지 않습니다.

PartialEq 트레이트는 예를 들어 assert_eq! 매크로를 쓸 때 필요합니다. 이 매크로는 타입의 두 인스턴스가 같은지 비교할 수 있어야 하기 때문입니다.

Eq 트레이트에는 메서드가 없습니다. 그 목적은 표시한 타입의 모든 값에 대해 그 값이 자기 자신과 같다는 것을 알리는 것입니다. Eq 트레이트는 PartialEq도 구현하는 타입에만 쓸 수 있지만, PartialEq를 구현하는 모든 타입이 Eq를 구현할 수 있는 것은 아닙니다. 한 예는 부동소수점 숫자 타입입니다. 부동소수점 숫자의 구현에서는 숫자가 아님(not-a-number, NaN) 값의 두 인스턴스가 서로 같지 않다고 말합니다.

Eq가 필요한 예는 HashMap<K, V>의 키입니다. 그래야 HashMap<K, V>가 두 키가 같은지 알 수 있습니다.

순서 비교를 위한 PartialOrd 및 Ord

PartialOrd 트레이트로 정렬 목적으로 타입의 인스턴스를 비교할 수 있습니다. PartialOrd를 구현하는 타입은 <, >, <=, >= 연산자와 함께 쓸 수 있습니다. PartialOrd 트레이트는 PartialEq도 구현하는 타입에만 쓸 수 있습니다.

PartialOrd를 파생하면 partial_cmp 메서드가 구현되며, 이 메서드는 주어진 값들이 순서를 가지지 않을 때 None이 되는 Option<Ordering>을 돌려줍니다. 그 타입의 대부분의 값은 비교할 수 있지만 순서가 없는 값의 예는, 부동소수점의 NaN 값입니다. 어떤 부동소수점 숫자와 NaN 부동소수점 값으로 partial_cmp를 부르면 None이 돌아옵니다.

구조체에 파생하면, PartialOrd는 구조체 정의에 필드가 나오는 순서대로 각 필드의 값을 비교해 두 인스턴스를 비교합니다. 열거형에 파생하면, 열거형 정의에서 먼저 선언된 배리언트가 나중에 나열된 배리언트보다 작다고 여깁니다. 다만 배리언트에 판별값(discriminant)을 명시했다면, 그 값이 선언 순서보다 앞서 쓰일 수 있습니다.

PartialOrd 트레이트는 예를 들어 rand 크레이트의 random_range 메서드에 필요합니다. 이 메서드는 범위 표현식으로 지정한 범위 안에서 무작위 값을 만듭니다.

Ord 트레이트로, 표시한 타입의 어떤 두 값이든 유효한 순서가 있다는 것을 알 수 있습니다. Ord 트레이트는 cmp 메서드를 구현하며, 이 메서드는 유효한 순서가 언제나 가능하므로 Option<Ordering>이 아니라 Ordering을 돌려줍니다. Ord 트레이트는 PartialOrd와 Eq도 구현하는 타입에만 쓸 수 있습니다(Eq는 PartialEq를 요구합니다). 구조체와 열거형에 파생하면, cmp는 PartialOrd에서 파생한 partial_cmp의 구현과 같은 방식으로 동작합니다.

Ord가 필요한 예는 값들을 정렬 순서에 따라 저장하는 자료 구조인 BTreeSet<T>에 값을 저장할 때입니다.

값을 복제하기 위한 Clone과 Copy

Clone 트레이트로 값의 깊은 복사본을 명시적으로 만들 수 있으며, 복제 과정에서 임의의 코드를 실행하고 힙 데이터를 복사할 수도 있습니다. Clone에 대한 자세한 내용은 소유권이 뭔가요?를 보세요.

Clone을 파생하면 clone 메서드가 구현되며, 이 메서드는 타입 전체에 대해 구현될 때 타입의 각 부분에 clone을 부릅니다. 그러므로 Clone을 파생하려면 타입의 모든 필드나 값도 Clone을 구현해야 합니다. 다만 복제하는 방식은 타입마다 정하므로, Rc<T>의 clone처럼 같은 바탕 데이터를 가리키는 값을 만들 수도 있어, 언제나 깊은 복사가 된다고 보장하지는 않습니다.

Clone이 필요한 예는 슬라이스에 to_vec 메서드를 부를 때입니다. 슬라이스는 담고 있는 타입의 인스턴스를 소유하지 않지만, to_vec이 돌려주는 벡터는 그 인스턴스를 소유해야 하므로, to_vec은 각 항목에 clone을 부릅니다. 따라서 슬라이스에 담긴 타입은 Clone을 구현해야 합니다.

Copy 트레이트로 스택에 저장된 비트만 복사해 값을 복제할 수 있으며, 임의의 코드는 필요 없습니다. Copy에 대한 자세한 내용은 소유권이 뭔가요?를 보세요.

Copy 트레이트는 프로그래머가 그 메서드를 덮어써 임의의 코드가 실행되지 않는다는 가정을 깨뜨리는 일을 막기 위해, 어떤 메서드도 정의하지 않습니다. 그래서 모든 프로그래머는 값의 복사가 아주 빠를 것이라고 가정할 수 있습니다.

모든 부분이 Copy를 구현하는 어떤 타입에든 Copy를 파생할 수 있습니다. Copy를 구현하는 타입은 Clone도 구현해야 합니다. Copy를 구현하는 타입에는 Copy와 같은 일을 하는 Clone의 사소한 구현이 있기 때문입니다. 또 Drop을 구현하는 타입은 Copy가 될 수 없습니다.

Copy 트레이트가 필요한 경우는 드뭅니다. Copy를 구현하는 타입에는 최적화가 가능해, clone을 부르지 않아도 되므로 코드가 더 간결해집니다.

Copy로 할 수 있는 모든 것은 Clone으로도 할 수 있지만, 코드가 더 느리거나 여러 곳에서 clone을 불러야 할 수 있습니다. 또 Copy를 구현하는지는 공개 API의 일부이므로, 나중에 Copy를 빼면 그 타입을 쓰는 코드가 깨질 수 있습니다.

어떤 값을 고정 크기의 값으로 매핑하기 위한 Hash

Hash 트레이트로 임의 크기의 타입의 인스턴스를 받아, 해시 함수로 그 인스턴스를 고정 크기의 값으로 매핑할 수 있습니다. Hash를 파생하면 hash 메서드가 구현됩니다. 파생한 hash 메서드의 구현은 타입의 각 부분에 hash를 부른 결과를 합치므로, Hash를 파생하려면 모든 필드나 값도 Hash를 구현해야 합니다.

Hash가 필요한 예는 데이터를 효율적으로 저장하기 위해 HashMap<K, V>에 키를 저장할 때입니다. Hash는 Eq를 요구하지 않지만, 해시 키로 쓰는 타입은 k1 == k2이면 hash(k1) == hash(k2)가 되어야 합니다. 그래서 PartialEq나 Eq를 직접 구현해 동등의 기준을 바꿨다면, Hash도 같은 기준으로 맞춰야 합니다. 해시맵은 해시맵의 키와 값에서 다루었습니다.

기본값을 위한 Default

Default 트레이트로 타입의 기본값을 만들 수 있습니다. Default를 파생하면 default 함수가 구현됩니다. 파생한 default 함수의 구현은 타입의 각 부분에 default 함수를 부르므로, Default를 파생하려면 타입의 모든 필드나 값도 Default를 구현해야 합니다. 열거형에 파생할 때는, 기본값으로 쓸 유닛 배리언트 하나에 #[default] 속성을 붙입니다.

#[derive(Default)]
enum Level {
    #[default]
    Info,
    Warn,
    Error,
}

Default::default 함수는 흔히 구조체 정의 및 인스턴스화에서 이야기한 구조체 업데이트 문법과 함께 씁니다. 구조체의 필드 몇 개를 정하고, 나머지 필드에는 ..Default::default()로 기본값을 정해 쓸 수 있습니다.

Default 트레이트는 예를 들어 Option<T> 인스턴스에 unwrap_or_default 메서드를 쓸 때 필요합니다. Option<T>가 None이면 unwrap_or_default 메서드는 Option<T>에 저장된 T 타입의 Default::default의 결과를 돌려줍니다. 아래 표는 아홉 트레이트를 파생했을 때 무엇이 만들어지고, 무엇을 조심해야 하는지 정리한 것입니다.

Rust 내장 파생 트레이트 아홉 개의 생성 동작, 파생 조건, 타입 설계 약속과 주의점을 비교한 표와 모바일 카드

Rust · derive · semantic comparison

derive는 반복 구현보다 타입의 약속을 만든다

필드 조건이 컴파일되더라도 자동 구현의 의미가 도메인 규칙과 맞는지 따로 확인합니다. 비교·복제·해시·기본값은 컬렉션 동작과 공개 API에 이어지는 계약입니다.

내장 파생 트레이트 9개: 동작 · 조건 · 설계 주의
트레이트 자동 구현의 의미 파생 조건 설계 약속과 주의
Debug {:?}와 {:#?}의 프로그래머용 출력을 만듭니다. 모든 필드가 Debug를 구현합니다. 최종 사용자 서식인 Display는 내장 파생되지 않습니다.
Clone 각 부분의 clone을 호출해 값을 명시적으로 복제합니다. 모든 필드가 Clone을 구현합니다. 복제 방식은 타입별이며 같은 기반 데이터를 가리킬 수도 있어 깊은 복사를 보장하지 않습니다.
Copy 대입과 인수 전달에서 저장 위치와 무관한 암시적 비트 복사 의미를 줍니다. 모든 필드가 Copy, 타입이 Clone, Drop 미구현이어야 합니다. 구현 여부가 공개 API 약속이므로 나중에 제거하면 호환성을 깰 수 있습니다.
PartialEq ==, !=와 필드별 동등 비교를 만듭니다. 모든 필드가 PartialEq를 구현합니다. 어떤 필드를 같은 값의 기준으로 볼지 도메인 의미와 맞아야 합니다.
Eq 모든 값이 자기 자신과 같다는 반사적 동등성을 표시합니다. 파생하려면 PartialEq와 모든 필드의 Eq가 필요하며, 모든 값에 반사성이 성립해야 합니다. NaN처럼 자기 자신과 같지 않은 값이 있으면 적용할 수 없습니다.
PartialOrd <, <=, >, >=와 Option<Ordering> 비교를 만듭니다. PartialEq와 모든 필드의 PartialOrd가 필요합니다. 구조체는 필드 순서, 열거형은 판별값을 우선 비교하며 일부 값은 순서가 없을 수 있습니다.
Ord 모든 두 값에 유효한 Ordering을 반환하는 전체 순서를 만듭니다. Eq, PartialOrd와 모든 필드의 Ord가 필요합니다. 열거형의 명시적 판별값은 기본 선언 순서보다 우선할 수 있습니다.
Hash 각 부분을 해셔에 공급하는 hash 구현을 만듭니다. 모든 필드가 Hash를 구현합니다. Eq를 상속하지 않지만 해시 키는 k1 == k2이면 같은 해시를 내야 합니다.
Default 각 부분의 기본값으로 Default::default()를 만듭니다. 구조체의 모든 필드가 Default여야 합니다. 열거형은 #[non_exhaustive]가 아닌 유닛 배리언트 하나에 #[default]를 붙입니다. 빈 값이 아니라 도메인에서 실제로 유효하고 유용한 시작 상태인지 확인합니다.

Debug

{:?} 프로그래머 출력 · 모든 필드 Debug · 사용자용 Display는 별도 구현

Clone

각 필드를 명시 복제 · 모든 필드 Clone · 타입별 복제이며 깊은 복사 보장 없음

Copy

암시적 비트 복사 · 모든 필드 Copy + Clone + Drop 불가 · 공개 API 약속

PartialEq

==와 != · 모든 필드 PartialEq · 동등성 기준이 도메인 의미와 맞아야 함

Eq

반사적 동등성 · 파생 시 PartialEq와 모든 필드의 Eq 필요 · NaN처럼 자기 자신과 다른 값은 부적합

PartialOrd

부분 순서와 Option<Ordering> · PartialEq와 모든 필드의 PartialOrd 필요 · 필드 순서와 열거형 판별값 확인

Ord

전체 순서 · Eq + PartialOrd와 모든 필드의 Ord 필요 · 명시적 판별값이 열거형 순서를 바꿀 수 있음

Hash

필드별 해시 · 모든 필드 Hash · Eq 상속은 아니지만 동등한 키의 해시는 같아야 함

Default

기본 상태 · 구조체의 모든 필드 Default · 열거형은 #[non_exhaustive]가 아닌 유닛 #[default] 선택 · 도메인에 유효한 시작 상태인지 확인

자동 구현을 버릴 때

식별자 일부를 비교에서 빼거나 사용자에게 별도 형식으로 보여 주는 등 도메인 규칙이 필드별 기본 동작과 다르면 수동 구현을 선택합니다.

컬렉션 계약을 함께 보기

HashMap 키는 Eq + Hash, BTreeSet 값은 Ord를 요구합니다. 트레이트 조합은 저장 구조의 정확성과 연결됩니다.

내장 파생 목록은 아홉 개지만 라이브러리는 사용자 정의 derive 매크로를 추가할 수 있습니다. 이 표는 내장 구현의 의미만 다룹니다.

Debug는 모든 필드가 Debug일 때 {:?}와 {:#?}의 프로그래머용 출력을 만들며, 사용자용 Display는 따로 구현합니다. Clone은 각 필드를 명시적으로 복제하되 깊은 복사를 보장하지 않고, Copy는 모든 필드가 Copy이고 타입이 Clone이며 Drop을 구현하지 않아야 하고, 공개 API의 약속이 됩니다. PartialEq는 필드별로 같은지 비교하므로, 어떤 필드를 같음의 기준으로 볼지가 도메인의 뜻과 맞아야 합니다. Eq는 모든 값이 자기 자신과 같아야 하므로 NaN 같은 값이 있으면 쓸 수 없습니다. PartialOrd는 구조체에서 필드 순서, 열거형에서 판별값을 먼저 비교하며 순서가 없는 값이 있을 수 있고, Ord는 모든 두 값에 순서를 줍니다. Hash는 같은 값이 같은 해시를 내야 하고, Default는 빈 값이 아니라 도메인에서 실제로 쓸모 있는 시작 상태인지 확인합니다. 필드 조건을 만족해 컴파일되더라도, 자동으로 만든 구현의 뜻이 도메인의 규칙과 맞는지는 따로 확인해야 합니다.

더 깊이: derive는 제네릭 타입에 어떤 바운드를 붙일까

derive가 만드는 구현은 모든 타입 매개변수에 같은 트레이트의 바운드를 붙입니다. Reference의 derive 설명의 예처럼 struct Foo<T> { a: i32, b: T }에 #[derive(PartialEq, Clone)]을 붙이면, impl<T: PartialEq> PartialEq for Foo<T>와 impl<T: Clone> Clone for Foo<T>에 해당하는 구현이 만들어집니다. 필드 b가 T이므로 이 예에서는 이 바운드가 꼭 필요합니다.

하지만 바운드는 필드가 T를 어떻게 쓰는지와 상관없이 타입 매개변수마다 붙습니다. 예를 들어 struct Shared<T>(Rc<T>)에 #[derive(Clone)]을 붙이면 구현에 T: Clone이 붙으므로, Rc<T>는 어떤 T든 복제할 수 있는데도 Clone이 아닌 T로 만든 Shared<T>는 복제할 수 없습니다. 이럴 때는 derive 대신 impl<T> Clone for Shared<T>를 직접 쓰면 됩니다. 또 내장 derive가 만든 구현에는 #[automatically_derived] 속성이 붙는데, 이 속성은 동작에 영향을 주지 않고 도구와 린트가 자동으로 만든 구현을 알아보는 데 쓰입니다.

정리

  • 내장 파생 트레이트는 Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Default 아홉 가지이며, 라이브러리는 절차적 매크로로 다른 트레이트의 derive를 더할 수 있습니다.
  • Copy는 Clone을, Eq와 PartialOrd는 PartialEq를, Ord는 Eq와 PartialOrd를 요구하고, 모든 필드도 그 트레이트를 구현해야 파생할 수 있습니다.
  • 파생한 비교는 구조체에서 필드 순서, 열거형에서 배리언트의 선언 순서나 판별값을 따르며, NaN이 있는 부동소수점은 Eq와 Ord를 구현하지 않습니다.
  • Copy는 Drop을 구현한 타입에 쓸 수 없고, 공개 API의 일부라 나중에 빼면 쓰는 코드가 깨질 수 있습니다.
  • Display처럼 알맞은 기본 동작이 없는 트레이트는 파생할 수 없으며, 파생한 동작이 도메인의 뜻과 다르면 직접 구현합니다.

연습 문제

1. 파생할 수 있을까

다음 구조체에 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]를 붙이면 어떤 트레이트에서 오류가 나나요? derive는 구조체의 모든 필드가 그 트레이트를 구현해야 쓸 수 있으므로, 두 필드의 타입마다 따져 보세요.

struct Reading {
    sensor: String,
    value: f64,
}
해설 보기

String은 Copy가 아니므로 Copy를 파생할 수 없습니다. f64는 NaN 때문에 Eq와 Hash를 구현하지 않으므로, Eq와 Hash도 파생할 수 없습니다. Debug, Clone, PartialEq는 두 필드가 모두 구현하므로 파생할 수 있습니다.

2. 파생한 순서

다음 열거형에 PartialOrd와 PartialEq를 파생하면, Priority::Low < Priority::High는 참인가요? 판별값을 Low = 3, Medium = 2, High = 1로 명시하면 어떻게 되나요?

enum Priority {
    Low,
    Medium,
    High,
}
해설 보기

판별값이 없으면 먼저 선언한 배리언트가 작으므로 Low < High는 참입니다. 판별값을 Low = 3, High = 1로 명시하면 파생한 비교가 판별값을 따르므로 Low < High는 거짓이 됩니다. 순서를 선언 순서와 다르게 하려는 의도가 아니라면, 판별값을 정할 때 비교 결과가 바뀐다는 점을 조심해야 합니다.

다음 부록 D - 유용한 개발 도구에서는 rustfmt, cargo fix, Clippy, rust-analyzer 같은 개발 도구를 살펴봅니다.