Aller au contenu

Erreur Rust E0369 : binary operation cannot be applied to type

E0369 veut dire que vous avez utilisé un opérateur comme ==, < ou + sur un type qui ne sait pas encore s’en servir. En Rust, les opérateurs ne sont pas intégrés d’office à vos propres structs et enums. Chacun vient d’un trait (une capacité qu’un type déclare avoir), et votre type ne l’obtient que si vous dérivez ou implémentez ce trait. La correction tient le plus souvent en une ligne #[derive(...)], et le compilateur vous dit même laquelle. Pour les autres erreurs de cette série, consultez les erreurs du compilateur Rust.

error[E0369]: binary operation `==` cannot be applied to type `Point`
--> src/main.rs:9:10
|
9 | if a == b { // ❌ Point ne sait pas se comparer
| - ^^ - 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 {
|

Le compilateur dit : « l’opération binaire == ne peut pas être appliquée au type Point ». Il ajoute qu’il manque peut-être une implémentation de PartialEq, et propose d’ajouter #[derive(PartialEq)].

Voici le programme qui la provoque :

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 ne sait pas se comparer
println!("Same point!");
}
}

La première ligne change selon l’opérateur, mais c’est la même erreur :

  • binary operation `<` cannot be applied to type `Version` : il manque PartialOrd (pareil pour >, <=, >=).
  • cannot add `Point` to `Point` : il manque Add. Vous verrez aussi « cannot subtract », « cannot multiply », et ainsi de suite.
  • cannot add `&str` to `&str` : vous avez essayé de coller deux tranches de chaîne (&str) avec +.

Les étiquettes sous l’opérateur indiquent le type de chaque côté, ici Point et Point. Ensuite, la note: montre votre struct et dit qu’elle doit implémenter PartialEq (« must implement PartialEq »). Autrement dit : « vous me demandez de comparer deux Point, mais personne ne m’a dit ce que veut dire égal pour un Point ».

Imaginez un arbitre à qui l’on tend deux objets en demandant « lequel est le plus grand ? ». Pour des nombres, la réponse est évidente. Pour deux sacs à dos, l’arbitre a d’abord besoin d’une règle : au poids ? au volume ? Rust est cet arbitre prudent.

Chaque opérateur est un appel de méthode déguisé. a == b appelle PartialEq::eq, a < b appelle PartialOrd::partial_cmp, et a + b appelle Add::add. Les nombres, String et beaucoup de types standard implémentent déjà ces traits. Votre nouvelle struct, elle, n’en a aucun au départ.

Rust ne devine pas, parce qu’il se tromperait parfois. Deux User sont-ils égaux quand leurs identifiants sont les mêmes, ou seulement quand tous les champs sont identiques ? Point + Point doit-il additionner les coordonnées, et est-ce que ça a seulement un sens d’additionner des points ? C’est vous qui décidez, en dérivant le trait (le comportement par défaut, champ par champ) ou en écrivant votre propre impl. Les traits sont expliqués dans Les traits courants.

Choisissez cette solution pour == et != quand « égal » veut dire « tous les champs sont égaux ». C’est la bonne correction la plupart du temps, et c’est exactement ce que propose rustc. Ajouter aussi Debug vous permet d’afficher la struct :

#[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!("Même point !");
}
println!("{:?} != {:?} : {}", a, c, a != c);
}

Sortie :

Même point !
Point { x: 1, y: 2 } != Point { x: 5, y: 0 } : true
▶ Essayez par vous-même

Chaque champ doit lui aussi pouvoir être comparé. Si le type d’un champ n’a pas PartialEq, dérivez-le aussi sur ce type. Les enums fonctionnent de la même façon : #[derive(PartialEq)] sur enum Status permet à status == Status::Banned de compiler.

Choisissez cette solution quand votre type a un ordre naturel. PartialOrd a besoin de PartialEq, donc rustc propose #[derive(PartialEq, PartialOrd)]. La version dérivée compare les champs de haut en bas : d’abord major, et seulement s’ils sont égaux, minor. L’ordre des champs dans votre struct compte donc :

#[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!("Veuillez mettre à jour depuis {:?}", installed);
}
let next = Version { major: 2, minor: 0 };
println!("2.0 est plus récente que 1.7 : {}", next > required);
}

Sortie :

Veuillez mettre à jour depuis Version { major: 1, minor: 4 }
2.0 est plus récente que 1.7 : true
▶ Essayez par vous-même

Si vous voulez trier un Vec de votre type avec .sort(), il vous faudra aussi Eq et Ord. Voir Le tri.

Choisissez cette solution quand additionner deux valeurs de votre type a un sens clair, comme additionner des coordonnées ou de l’argent. Il n’existe pas de derive pour les opérateurs mathématiques, donc vous écrivez un petit impl. Output est le type que renvoie le + :

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 });
}

Sortie :

Point { x: 4, y: 6 }
true
▶ Essayez par vous-même

Sub, Mul, Div et Neg fonctionnent de la même façon, et se trouvent tous dans std::ops. Dériver Copy ici permet de continuer à utiliser start et step après le +. Plus de détails dans Les traits courants et Les traits avancés.

Solution 4 : coller des chaînes avec + ou format!

Section intitulée « Solution 4 : coller des chaînes avec + ou format! »

Choisissez cette solution quand l’erreur dit cannot add `&str` to `&str`. + sur des chaînes demande une String possédée à gauche et un &str à droite. L’aide de rustc le dit à sa manière : « create an owned String from a string reference » (créez une String possédée à partir d’une référence de chaîne), donc first.to_owned() + second. Avec deux String, empruntez celle de droite : s1 + &s2. Et format! marche toujours, sans rien déplacer :

fn main() {
let first = String::from("Bonjour, ");
let second = String::from("Ferris");
let shout = format!("{first}{second} !"); // emprunte les deux
let joined = first + &second; // first est déplacée dans joined
let short = "Salut, ".to_owned() + "crabe"; // &str à gauche : on le rend possédé d'abord
println!("{shout}");
println!("{joined}");
println!("{short}");
println!("{second} est toujours là");
}

Sortie :

Bonjour, Ferris !
Bonjour, Ferris
Salut, crabe
Ferris est toujours là
▶ Essayez par vous-même

+ prend l’ownership (la possession) de la String de gauche et lui ajoute la suite, c’est pourquoi first n’est plus utilisable après cette ligne. Si ça vous embrouille, utilisez simplement format!. Voir Les chaînes de caractères.

Comparer dans du code générique. La fonction classique largest échoue avec binary operation `>` cannot be applied to type `&T`, parce que T pourrait être n’importe quel type, même un type sans ordre :

fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // ❌ T n'est peut-être pas comparable
largest = item;
}
}
largest
}

Correction : ajoutez une contrainte de trait, exactement comme le propose rustc : fn largest<T: PartialOrd>(list: &[T]) -> &T. Voir Les génériques.

Tester la valeur d’un enum. if status == Status::Banned échoue tant que l’enum n’a pas #[derive(PartialEq)]. Si vous ne pouvez pas modifier l’enum (il vient d’un autre crate), utilisez matches!(status, Status::Banned) ou un match à la place.

Coller deux String. first + second avec deux String, ce n’est pas E0369 : c’est E0308 (« expected &str, found String ») avec help: consider borrowing here, c’est-à-dire first + &second. Même famille, même correction que la solution 4.

Comparer deux types différents. count == limit avec un i32 et un f64 donne E0308, et name == 'F' avec une String et un char donne E0277 (« can’t compare String with char »). Un repère simple : vous obtenez E0369 quand le type de gauche n’a aucune version de l’opérateur, et E0308 ou E0277 quand il en a une, mais pas pour ce type de droite. Correction : convertissez un des deux côtés (count as f64 == limit, name == "F").

Petit quiz

1. Vous écrivez if order_a == order_b pour une struct Order et obtenez E0369. Quelle est la correction habituelle ?

2. Avec #[derive(PartialOrd)] sur struct Version { major: u32, minor: u32 }, comment deux versions sont-elles comparées ?

3. first et second sont tous les deux des &str. Quelle expression les colle sans erreur ?

Score : 0 / 3