Aller au contenu

Erreur Rust E0277 : the trait bound is not satisfied (trait manquant)

L’erreur E0277 veut dire : vous avez utilisé un type à un endroit qui demande une capacité que ce type n’a pas. L’afficher, le comparer, l’additionner, le convertir, l’envoyer à un autre thread : chacune de ces actions demande un trait (une capacité qu’un type déclare avoir), et il manque ce trait à votre type. C’est l’une des erreurs Rust les plus courantes, le message indique en général la correction exacte, et la plupart du temps elle tient en une ligne. (Cette page fait partie de la série Les erreurs du compilateur Rust expliquées.)

La version que la plupart des gens rencontrent en premier, c’est l’affichage de leur propre struct avec {} :

error[E0277]: `Player` doesn't implement `std::fmt::Display`
--> src/main.rs:8:20
|
8 | println!("{}", p);
| -- ^ `Player` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
help: the trait `std::fmt::Display` is not implemented for `Player`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Le compilateur dit que Player n’implémente pas Display et ne peut donc pas être affiché avec le format par défaut. Il suggère d’essayer {:?} à la place.

Le programme qui la provoque :

struct Player {
name: String,
score: u32,
}
fn main() {
let p = Player { name: String::from("Ana"), score: 42 };
println!("{}", p);
}

Un trait est une promesse : le type sait faire quelque chose. Display, c’est la promesse « je sais m’afficher joliment ». PartialEq, c’est « on peut me comparer avec == ». Send, c’est « je peux être envoyé sans danger à un autre thread ».

Du code comme println!, .sort(), + ou thread::spawn réclame l’une de ces promesses. Le compilateur montre la valeur que vous avez passée (p à la ligne 8) et dit : ce type n’a jamais fait cette promesse, donc je ne peux pas l’utiliser ici.

Pensez à une prise électrique. La prise n’accepte qu’une seule forme de fiche. Votre appareil peut très bien fonctionner, mais si sa fiche n’a pas la bonne forme, elle ne rentre pas. E0277, c’est le compilateur qui vous dit « mauvaise fiche ». Pour corriger, vous donnez au type la bonne fiche : le plus souvent avec #[derive(...)], parfois avec un petit bloc impl.

Rust ne devine jamais. Pour votre propre type Player, il n’existe pas une seule « bonne » façon de l’afficher, de le comparer ou de le trier. Deux joueurs sont-ils égaux quand leurs noms sont identiques, ou seulement quand tous les champs le sont ? Rust vous demande de le dire, plutôt que de faire en douce quelque chose que vous ne vouliez pas.

La vérification a lieu à la compilation. Dans beaucoup de langages, afficher un objet à qui on n’a jamais appris à s’afficher donne un résultat inutile comme <Player object at 0x7f...>, et envoyer à un thread une valeur qui n’est pas thread-safe provoque un plantage rare, en production. En Rust, ce sont deux erreurs de compilation que vous corrigez une fois pour toutes.

Vous en saurez plus sur le fonctionnement des traits dans Les traits, et sur ceux du quotidien (Debug, Display, PartialEq, Ord, Hash) dans Les traits courants.

E0277 a plusieurs visages. Cherchez ci-dessous le message qui ressemble au vôtre.

{:?} demande Debug. Vous ne l’écrivez presque jamais à la main : ajoutez #[derive(Debug)]. Servez-vous-en pour le débogage et les logs. La version {:?} de l’erreur ressemble à ceci :

error[E0277]: `Player` doesn't implement `Debug`
= help: the trait `Debug` is not implemented for `Player`
= note: add `#[derive(Debug)]` to `Player` or manually `impl Debug for Player`

La note propose justement d’ajouter #[derive(Debug)], ou d’implémenter Debug à la main.

{} demande Display. Rust ne peut pas le dériver, parce que vous seul savez à quoi doit ressembler la version « jolie ». Écrivez un petit impl avec write!. Servez-vous-en pour le texte que vos utilisateurs verront.

use std::fmt;
#[derive(Debug)] // vous donne {:?} gratuitement
struct Player {
name: String,
score: u32,
}
// Display, c'est vous qui l'écrivez : il décide de ce qu'affiche {}.
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({} points)", self.name, self.score)
}
}
fn main() {
let p = Player { name: String::from("Ana"), score: 42 };
println!("{:?}", p);
println!("{}", p);
}

Sortie :

Player { name: "Ana", score: 42 }
Ana (42 points)
▶ Essayez par vous-même

Solution 2 : comparer, trier, ou utiliser comme clé de HashMap

Section intitulée « Solution 2 : comparer, trier, ou utiliser comme clé de HashMap »

.contains() et == demandent PartialEq. .sort() demande Ord. Les clés de HashSet et de HashMap demandent Eq et Hash. Les messages ressemblent à ceci :

error[E0277]: can't compare `Point` with `Point`
= help: the trait `PartialEq` is not implemented for `Point`
error[E0277]: the trait bound `Priority: Ord` is not satisfied
= help: the trait `Ord` is not implemented for `Priority`
error[E0277]: the trait bound `Point: Hash` is not satisfied
= help: the trait `Hash` is not implemented for `Point`

Autrement dit : « impossible de comparer Point avec Point », « Priority n’implémente pas Ord », « Point n’implémente pas Hash ». La correction consiste à les dériver. Ils s’appuient les uns sur les autres, donc l’ensemble complet habituel est PartialEq, Eq, Hash, PartialOrd, Ord :

use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Priority {
Low,
Medium,
High,
}
fn main() {
let mut tasks = vec![Priority::High, Priority::Low, Priority::Medium];
tasks.sort(); // demande Ord
println!("{:?}", tasks);
println!("Contient High : {}", tasks.contains(&Priority::High)); // demande PartialEq
let mut counts: HashMap<Priority, u32> = HashMap::new(); // la clé demande Eq + Hash
for task in &tasks {
*counts.entry(*task).or_insert(0) += 1;
}
println!("Tâches Low : {}", counts[&Priority::Low]);
}

Sortie :

[Low, Medium, High]
Contient High : true
Tâches Low : 1
▶ Essayez par vous-même

Un Ord dérivé trie les variantes d’un enum dans l’ordre où vous les avez écrites, et les structs champ par champ, de haut en bas. Un piège, pour être honnête : un type qui a un champ f64 ne peut pas dériver Eq, Hash ni Ord, parce que les flottants ont NaN, qui n’est pas égal à lui-même. Pour trier des flottants, voir Le tri.

Solution 3 : mélanger des types de nombres (cannot add f64 to i32)

Section intitulée « Solution 3 : mélanger des types de nombres (cannot add f64 to i32) »
error[E0277]: cannot add `f64` to `i32`
--> src/main.rs:4:23
|
4 | let total = items + price;
| ^ no implementation for `i32 + f64`
|
= help: the trait `Add<f64>` is not implemented for `i32`

Le message dit « impossible d’ajouter un f64 à un i32 » : il n’existe pas d’implémentation de i32 + f64. Rust ne convertit jamais les nombres à votre place, même pas de i32 vers f64. Les deux côtés de +, -, * et / doivent avoir le même type. Convertissez vous-même un des deux côtés avec as :

fn main() {
let items: i32 = 3;
let price: f64 = 2.5;
let total = items as f64 * price; // on convertit d'abord, puis on multiplie
println!("Total : {total}");
}

Sortie :

Total : 7.5
▶ Essayez par vous-même

f64::from(items) marche aussi et ne peut jamais perdre de données. Dans l’autre sens (de f64 vers i32), les décimales sont coupées, alors pensez d’abord à arrondir. Les pages Conversion de types et Les conversions donnent tous les détails.

error[E0277]: `?` couldn't convert the error to `ConfigError`
|
8 | fn parse_port(text: &str) -> Result<u16, ConfigError> {
| ------------------------ expected `ConfigError` because of this
9 | let port = text.parse::<u16>()?;
| --------------^ the trait `From<ParseIntError>` is not implemented for `ConfigError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait

« ? n’a pas pu convertir l’erreur en ConfigError ». Quand ? tombe sur une erreur, il la renvoie depuis votre fonction. Si le type de cette erreur n’est pas celui que votre fonction promet, ? essaie de la convertir avec From (c’est ce que dit la note). À vous d’écrire ce From :

use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
BadPort(ParseIntError),
}
// On apprend à `?` comment transformer une ParseIntError en ConfigError.
impl From<ParseIntError> for ConfigError {
fn from(err: ParseIntError) -> Self {
ConfigError::BadPort(err)
}
}
fn parse_port(text: &str) -> Result<u16, ConfigError> {
let port = text.parse::<u16>()?;
Ok(port)
}
fn main() {
println!("{:?}", parse_port("8080"));
println!("{:?}", parse_port("quatre-vingts"));
}

Sortie :

Ok(8080)
Err(BadPort(ParseIntError { kind: InvalidDigit }))
▶ Essayez par vous-même

Autres bonnes options : convertir un seul appel avec .map_err(ConfigError::BadPort)?, utiliser #[from] du crate thiserror, ou renvoyer Result<T, Box<dyn std::error::Error>> dans les petits programmes. Voir Les erreurs personnalisées et anyhow et thiserror.

Solution 5 : Rc cannot be sent between threads safely

Section intitulée « Solution 5 : Rc cannot be sent between threads safely »
error[E0277]: `Rc<String>` cannot be sent between threads safely
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(move || {
| ------------- required by a bound introduced by this call
...
= help: within `{closure@src/main.rs:6:32: 6:39}`, the trait `Send` is not implemented for `Rc<String>`

« Rc<String> ne peut pas être envoyé entre threads sans danger. » Rc compte ses propriétaires avec un simple nombre, que deux threads pourraient modifier en même temps et corrompre. Rc n’est donc pas Send. Utilisez plutôt Arc (la version « atomique ») : il s’utilise de la même façon et ne pose aucun problème entre threads :

use std::sync::Arc;
use std::thread;
fn main() {
let config = Arc::new(String::from("debug"));
let for_thread = Arc::clone(&config); // un deuxième propriétaire pour le thread
let handle = thread::spawn(move || {
println!("Le thread voit : {for_thread}");
});
handle.join().unwrap();
println!("main a toujours : {config}");
}

Sortie :

Le thread voit : debug
main a toujours : debug
▶ Essayez par vous-même

C’est pareil pour RefCell : entre threads, utilisez Arc<Mutex<T>> au lieu de Rc<RefCell<T>>. Voir L’état partagé et La concurrence.

  • Les fonctions génériques. fn announce<T: Display>(item: T) appelée avec votre struct donne `Song` doesn't implement `std::fmt::Display` avec « required by a bound in announce » (exigé par une contrainte de announce). Correction : implémentez le trait que demande la contrainte.
  • Les tests. assert_eq!(a, b) demande à la fois PartialEq et Debug (pour afficher les valeurs quand le test échoue). Correction : #[derive(Debug, PartialEq)].
  • .collect() vers le mauvais type. let nums: Vec<i32> = words.iter().collect(); donne « a value of type Vec<i32> cannot be built from an iterator over elements of type &String » (on ne peut pas construire un Vec<i32> à partir d’éléments &String). Correction : convertissez d’abord chaque élément, par exemple avec .map(|w| w.parse().unwrap()). Voir Les itérateurs.

Petit quiz

1. Vous obtenez "Order doesn't implement Debug" en utilisant {:?}. Quelle est la correction la plus rapide ?

2. Pourquoi let total = 3_i32 + 2.5_f64; échoue-t-il ?

3. Votre code avec un thread échoue avec "Rc<String> cannot be sent between threads safely". Que devez-vous utiliser ?

Score : 0 / 3