В Rust трейты PartialEq и Eq возникают из философии type safety — все фундаментальные для коллекций операции (HashMap, HashSet) основаны на строгом определении равенства. Трейты определяют, могут ли объекты двух типов сравниваться с помощью == и обязуется ли тип реализовать "полное" равенство (Eq) или "частичное" (PartialEq).
Неправильная реализация PartialEq/Eq приводит к некорректной работе коллекций, где нарушаются базовые математические свойства: сравнение с самим собой, симметрия, транзитивность. Кроме того, некорректный Eq может привести к тому, что объекты "теряются" в коллекциях или возникают неожиданные дубликаты.
PartialEq реализуется для типов, у которых сравнение возможно не для всех значений (например, f32, где NaN != NaN). Eq предназначен только для "строгих" (тотальных) отношений. Пользовательские типы должны реализовывать сравнение в строгом соответствии с этими контрактами.
Пример кода:
#[derive(PartialEq, Eq)] struct Point { x: i32, y: i32, } let p1 = Point { x: 1, y: 2 }; let p2 = Point { x: 1, y: 2 }; assert!(p1 == p2); // true
Ключевые особенности:
Почему для типа f32 стандартная библиотека реализует только PartialEq, но не Eq?
Потому что по IEEE-754 NaN != NaN, т.е. свойство x == x для всех x не соблюдается, а это необходимое свойство для Eq.
Обязательно ли реализовывать Eq явно, если реализован PartialEq вручную?
Да, если тип поддерживает полное равенство (x == x всегда true), необходимо вручную реализовать и Eq, иначе некоторые структуры откажутся компилироваться с вашим типом.
Может ли пользователь реализовать PartialEq "несимметрично" (x == y, но y != x)?
Технически да, но это приводит к некорректной работе коллекций и считается багом.
** Негативный кейс
Пользователь реализовал PartialEq для типа, сравнивающего только по одному полю, остальные игнорируются. В результате HashSet "теряет" дубликаты.
Плюсы:
Минусы:
** Позитивный кейс
Использует #[derive(PartialEq, Eq)] либо реализует собственную версию сравнения, учитывающую всю бизнес-логику, гарантируя требуемые свойства.
Плюсы:
Минусы: