Перейти к содержимому

Ошибка 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 — как раз такой аккуратный судья.

Каждый оператор — это замаскированный вызов метода. a == b вызывает PartialEq::eq, a < b вызывает PartialOrd::partial_cmp, а a + b вызывает Add::add. Числа, String и многие стандартные типы уже реализуют эти трейты. А у твоей новой структуры поначалу нет ни одного.

Rust не угадывает, потому что догадка иногда оказалась бы неверной. Должны ли два User быть равны, когда совпадают их ID, или только когда совпадают все поля? Должно ли Point + Point складывать координаты — и есть ли вообще смысл складывать точки? Решаешь ты: выводишь трейт (стандартное сравнение поле за полем) или пишешь свой impl. Трейты разобраны на странице Распространённые трейты.

Выбирай это для == и !=, когда «равны» значит «все поля равны». Чаще всего это правильное исправление, и именно его предлагает 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 компилируется.

Выбирай это, когда у твоего типа есть естественный порядок. 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. Подробнее в уроке Сортировка.

Выбирай это, когда сложение двух значений твоего типа имеет понятный смысл, например сложение координат или денег. Для математических операторов 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 }
true
▶ Попробуй сам

Sub, 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").

Проверь себя

1. Ты пишешь if order_a == order_b для структуры Order и получаешь E0369. Как это обычно исправить?

2. На struct Version { major: u32, minor: u32 } стоит #[derive(PartialOrd)]. Как сравниваются две версии?

3. first и second — обе &str. Какое выражение склеит их без ошибки?

Результат: 0 / 3