PartialEqEq: 동등성 비교 연산자 구현하기

PartialEqEq 트레이트는 사용자 정의 타입에 ==(같음)와 !=(같지 않음) 연산자를 사용할 수 있게 해주는 핵심적인 비교 트레이트입니다. 이들을 구현하면 타입의 인스턴스들이 서로 같은지 여부를 비교할 수 있게 되어, if문, match 표현식, 컬렉션의 contains 메서드 등 다양한 곳에서 활용될 수 있습니다.

PartialEq: 부분 동등성

PartialEq는 ‘부분 동등성(Partial Equality)‘을 의미합니다. 이름에 ‘부분’이 붙은 이유는, 해당 타입의 모든 값이 항상 서로 비교 가능하지는 않을 수 있음을 시사하기 때문입니다.

PartialEq 트레이트는 eq 메서드 하나만 요구합니다 (ne 메서드는 eq의 결과를 반전시키는 기본 구현을 가집니다).

pub trait PartialEq<Rhs: ?Sized = Self> {
    fn eq(&self, other: &Rhs) -> bool;
    // ...
}

PartialEq의 대표적인 예는 부동소수점 타입(f32, f64)입니다. 부동소수점에는 ‘숫자가 아님’을 의미하는 NaN(Not a Number) 값이 있는데, 명세상 NaN == NaN은 항상 false입니다. 즉, 자기 자신과도 동등하지 않은 값이 존재합니다. 이처럼 완벽한 동등성 관계가 성립하지 않기 때문에 부동소수점 타입은 PartialEq만 구현하고, Eq는 구현하지 않습니다.

#[derive(PartialEq)]

대부분의 구조체는 #[derive(PartialEq)] 속성을 통해 PartialEq를 쉽게 구현할 수 있습니다. 이 경우, 두 인스턴스의 모든 필드가 순서대로 동일해야 true를 반환하는 코드가 자동으로 생성됩니다.

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}
 
fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = Point { x: 10, y: 20 };
    let p3 = Point { x: 30, y: 20 };
 
    assert!(p1 == p2);
    assert!(p1 != p3);
    println!("p1과 p2는 같습니다.");
}

Eq: 완전 동등성

Eq는 ‘완전 동등성(Total Equality)‘을 의미하는 마커 트레이트입니다. Eq는 아무런 메서드도 가지지 않으며, 단지 PartialEq로 정의된 동등성 비교가 다음의 수학적 ‘동치 관계(Equivalence Relation)‘를 만족함을 컴파일러에게 알려주는 표식입니다.

  1. 반사성(Reflexive): a == a는 항상 true이다.
  2. 대칭성(Symmetric): a == b이면, b == atrue이다.
  3. 타동성(Transitive): a == b이고 b == c이면, a == ctrue이다.

NaN 때문에 f32f64Eq를 구현하지 않지만, 대부분의 다른 타입(정수, String, 필드가 모두 Eq인 구조체 등)은 Eq를 구현할 수 있습니다.

HashMap의 키나 HashSet의 원소로 사용될 타입은 반드시 EqHash를 구현해야 합니다.

#[derive(Debug, PartialEq, Eq)] // Eq를 파생하려면 PartialEq가 먼저 필요합니다.
struct UserId(u64);
 
fn main() {
    let user1 = UserId(101);
    let user2 = UserId(101);
 
    // Eq가 보장되므로, 완벽한 동등성 비교가 가능합니다.
    assert!(user1 == user2);
}

수동 구현

때로는 특정 필드만으로 동등성을 비교하고 싶을 수 있습니다. 예를 들어, 사용자의 id가 같다면 다른 정보(last_login 등)가 달라도 같은 사용자로 간주하고 싶을 때 PartialEq를 직접 구현할 수 있습니다.

struct User {
    id: u32,
    username: String,
}
 
impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        // id 필드만으로 동등성을 판단합니다.
        self.id == other.id
    }
}
// 이 경우, username이 달라도 id만 같으면 `==`가 true가 되므로
// 반사성, 대칭성, 타동성이 완벽히 보장되지 않을 수 있어 Eq는 구현하지 않는 것이 안전할 수 있습니다.
 
fn main() {
    let user1 = User { id: 1, username: "Alice".to_string() };
    let user2 = User { id: 1, username: "Alicia".to_string() };
    assert!(user1 == user2); // id가 같으므로 true
}

결론

PartialEqEq는 러스트의 타입 시스템에 비교의 의미를 부여하는 중요한 트레이트입니다. #[derive]를 통해 대부분의 경우 쉽게 구현할 수 있으며, 이를 통해 사용자 정의 타입이 언어의 기본 연산자와 자연스럽게 통합되어 코드의 가독성과 표현력을 높여줍니다.