Ошибка Rust E0369: binary operation cannot be applied to type (нет оператора)
E0369 значит, что ты применил оператор вроде ==, < или + к типу, который пока не умеет с ним работать. В Rust операторы не встроены в твои собственные структуры и перечисления. Каждый оператор приходит из трейта (trait), и твой тип получает его, только когда ты выводишь (derive) или реализуешь этот трейт. Обычно исправление — одна строка #[derive(...)], и компилятор даже подсказывает, какая именно. Другие ошибки из этой серии собраны на странице Ошибки компилятора Rust.
Сообщение об ошибке
Заголовок раздела «Сообщение об ошибке»error[E0369]: binary operation `==` cannot be applied to type `Point` --> src/main.rs:9:10 |9 | if a == b { // ❌ Point doesn't know how to compare itself | - ^^ - Point | | | Point |note: an implementation of `PartialEq` might be missing for `Point` --> src/main.rs:1:1 |1 | struct Point { | ^^^^^^^^^^^^ must implement `PartialEq`help: consider annotating `Point` with `#[derive(PartialEq)]` |1 + #[derive(PartialEq)]2 | struct Point { |Компилятор пишет: бинарную операцию == нельзя применить к типу Point, и, похоже, для Point не хватает реализации PartialEq. Вот программа, которая выдаёт эту ошибку:
struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; if a == b { // ❌ Point doesn't know how to compare itself println!("Same point!"); }}Комментарий говорит: Point не знает, как сравнивать себя. Первая строка сообщения меняется в зависимости от оператора, но ошибка та же:
binary operation `<` cannot be applied to type `Version`: не хватаетPartialOrd(то же для>,<=,>=).cannot add `Point` to `Point`: не хватаетAdd. Ещё бывает «cannot subtract», «cannot multiply» и так далее.cannot add `&str` to `&str`: ты пытался склеить два строковых среза через+.
Что это значит
Заголовок раздела «Что это значит»Подписи под оператором показывают тип с каждой стороны, здесь Point и Point. Затем note: указывает на твою структуру и говорит, что она должна реализовать PartialEq («must implement PartialEq»). Иначе говоря: «Ты просишь сравнить два Point, но никто не объяснил мне, что значит равны для Point».
Представь спортивного судью, которому дали два предмета и спросили: «Какой больше?». Для чисел ответ очевиден. А для двух рюкзаков судье сначала нужно правило: по весу? по объёму? Rust — как раз такой аккуратный судья.
Почему Rust так делает
Заголовок раздела «Почему Rust так делает»Каждый оператор — это замаскированный вызов метода. a == b вызывает PartialEq::eq, a < b вызывает PartialOrd::partial_cmp, а a + b вызывает Add::add. Числа, String и многие стандартные типы уже реализуют эти трейты. А у твоей новой структуры поначалу нет ни одного.
Rust не угадывает, потому что догадка иногда оказалась бы неверной. Должны ли два User быть равны, когда совпадают их ID, или только когда совпадают все поля? Должно ли Point + Point складывать координаты — и есть ли вообще смысл складывать точки? Решаешь ты: выводишь трейт (стандартное сравнение поле за полем) или пишешь свой impl. Трейты разобраны на странице Распространённые трейты.
Как исправить
Заголовок раздела «Как исправить»Исправление 1: #[derive(PartialEq)] для == и !=
Заголовок раздела «Исправление 1: #[derive(PartialEq)] для == и !=»Выбирай это для == и !=, когда «равны» значит «все поля равны». Чаще всего это правильное исправление, и именно его предлагает rustc. Если добавить ещё и Debug, структуру можно будет выводить на экран:
#[derive(Debug, PartialEq)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; let c = Point { x: 5, y: 0 }; if a == b { println!("Точки совпадают!"); } println!("{:?} != {:?}: {}", a, c, a != c);}Вывод:
Точки совпадают!Point { x: 1, y: 2 } != Point { x: 5, y: 0 }: trueКаждое поле тоже должно уметь сравниваться. Если у типа поля нет PartialEq, выведи его и для этого типа. С перечислениями всё так же: #[derive(PartialEq)] на enum Status — и status == Status::Banned компилируется.
Исправление 2: #[derive(PartialOrd)] для < и >
Заголовок раздела «Исправление 2: #[derive(PartialOrd)] для < и >»Выбирай это, когда у твоего типа есть естественный порядок. PartialOrd требует PartialEq, поэтому rustc предлагает #[derive(PartialEq, PartialOrd)]. Выведенная версия сравнивает поля сверху вниз: сначала major, и только если они равны — minor. Так что порядок полей в структуре важен:
#[derive(Debug, PartialEq, PartialOrd)]struct Version { major: u32, minor: u32,}
fn main() { let installed = Version { major: 1, minor: 4 }; let required = Version { major: 1, minor: 7 }; if installed < required { println!("Пожалуйста, обнови {:?}", installed); }
let next = Version { major: 2, minor: 0 }; println!("2.0 новее, чем 1.7: {}", next > required);}Вывод:
Пожалуйста, обнови Version { major: 1, minor: 4 }2.0 новее, чем 1.7: trueЕсли ты хочешь вызвать .sort() у Vec с твоим типом, понадобятся ещё Eq и Ord. Подробнее в уроке Сортировка.
Исправление 3: impl std::ops::Add для +
Заголовок раздела «Исправление 3: impl std::ops::Add для +»Выбирай это, когда сложение двух значений твоего типа имеет понятный смысл, например сложение координат или денег. Для математических операторов derive нет, поэтому ты пишешь короткий impl. Output — это тип, который возвращает +:
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } }}
fn main() { let start = Point { x: 1, y: 2 }; let step = Point { x: 3, y: 4 }; let end = start + step; println!("{:?}", end); println!("{}", end == Point { x: 4, y: 6 });}Вывод:
Point { x: 4, y: 6 }trueSub, Mul, Div и Neg работают так же, все они лежат в std::ops. Благодаря Copy здесь start и step можно использовать и после +. Подробнее в уроках Распространённые трейты и Продвинутые трейты.
Исправление 4: склеиваем строки через + или format!
Заголовок раздела «Исправление 4: склеиваем строки через + или format!»Выбирай это, когда ошибка говорит cannot add `&str` to `&str`. Для + со строками нужна владеющая String слева и &str справа. Подсказка rustc говорит об этом по-своему: «create an owned String from a string reference» (создай владеющую String из ссылки на строку), то есть first.to_owned() + second. Если у тебя две String, заимствуй правую: s1 + &s2. А format! работает всегда и ничего не перемещает:
fn main() { let first = String::from("Привет, "); let second = String::from("Ferris");
let shout = format!("{first}{second}!"); // заимствует обе строки let joined = first + &second; // first перемещается в joined let short = "Хай, ".to_owned() + "краб"; // слева &str: сначала делаем владеющую строку
println!("{shout}"); println!("{joined}"); println!("{short}"); println!("{second} всё ещё здесь");}Вывод:
Привет, Ferris!Привет, FerrisХай, крабFerris всё ещё здесь+ забирает владение левой String и дописывает к ней текст, поэтому после этой строки first использовать нельзя. Если это сбивает с толку, просто бери format!. Подробнее в уроке Строки.
Где это обычно встречается
Заголовок раздела «Где это обычно встречается»Сравнение в обобщённом коде. Классическая функция largest падает с binary operation `>` cannot be applied to type `&T`, потому что T может быть любым типом, даже таким, у которого нет порядка:
fn largest<T>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { // ❌ T might not be comparable largest = item; } } largest}Исправление: добавь ограничение трейта, ровно как предлагает rustc: fn largest<T: PartialOrd>(list: &[T]) -> &T. Подробнее в уроке Обобщения.
Проверка значения перечисления. if status == Status::Banned не компилируется, пока у перечисления нет #[derive(PartialEq)]. Если ты не можешь изменить перечисление (оно из другого крейта), используй matches!(status, Status::Banned) или match.
Склеивание двух String. first + second с двумя String — это не E0369, а E0308 («expected &str, found String») с подсказкой help: consider borrowing here, то есть first + &second. То же семейство, то же исправление, что и в исправлении 4.
Сравнение двух разных типов. count == limit с i32 и f64 даёт E0308, а name == 'F' со String и char даёт E0277 («can’t compare String with char»). Простое правило: E0369 бывает, когда у левого типа вообще нет версии этого оператора, а E0308 или E0277 — когда версия есть, но не для такого правого типа. Исправление: преобразуй одну из сторон (count as f64 == limit, name == "F").
Связанные ошибки
Заголовок раздела «Связанные ошибки»- E0277: trait bound not satisfied: оператор для левого типа есть, но не с таким правым типом (
i32 + f64,String == char). - E0308: mismatched types: то, что ты получишь для
String + Stringили при сравнении двух разных структур. - E0599: no method found: вызов
.sort()или.contains()может упасть так же, если твоему типу не хватаетOrdилиPartialEq. E0368: binary assignment operation += cannot be applied: версия этой ошибки для+=. РеализуйAddAssignизstd::ops.- Все ошибки компилятора: полный список страниц с ошибками.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Ты пишешь if order_a == order_b для структуры Order и получаешь E0369. Как это обычно исправить?
2. На struct Version { major: u32, minor: u32 } стоит #[derive(PartialOrd)]. Как сравниваются две версии?
3. first и second — обе &str. Какое выражение склеит их без ошибки?
Результат: 0 / 3
Что почитать
Заголовок раздела «Что почитать»- E0369 — список кодов ошибок Rust (на английском): официальное объяснение этой ошибки.
- Приложение C: выводимые трейты — книга «Язык программирования Rust» (перевод на русский): что даёт вывод
PartialEqиPartialOrd, включая порядок сравнения поле за полем. - Хранение текста в кодировке UTF-8 в строках — книга «Язык программирования Rust» (перевод на русский): почему
+со строками берётStringслева и&strсправа и когда лучше взятьformat!. - Перегрузка операторов — Rust на примерах (перевод на русский): короткий запускаемый пример реализации
Addдля своих типов. std::ops::Add— документация стандартной библиотеки (на английском): трейтAdd, его типOutputи сложение значений разных типов.