Les méthodes
Les méthodes sont similaires aux fonctions : nous les déclarons avec le mot-clé fn et un nom, elles peuvent avoir des paramètres et une valeur de retour, et elles contiennent du code qui est exécuté lorsque la méthode est appelée depuis un autre endroit. Contrairement aux fonctions, les méthodes sont définies dans le contexte d’une struct (ou d’un enum ou d’un objet trait, que nous abordons respectivement au [chapitre 6][enums] et au [chapitre 18][trait-objects]), et leur premier paramètre est toujours self, qui représente l’instance de la struct sur laquelle la méthode est appelée.
Syntaxe des méthodes
Modifions la fonction area qui prend une instance de Rectangle comme paramètre et faisons-en plutôt une méthode area définie sur la struct Rectangle, comme le montre le listing 5-13.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
area method on the Rectangle structPour définir la fonction dans le contexte de Rectangle, nous commençons un bloc impl (implémentation) pour Rectangle. Tout ce qui se trouve dans ce bloc impl sera associé au type Rectangle. Ensuite, nous déplaçons la fonction area à l’intérieur des accolades du bloc impl et changeons le premier (et dans ce cas, le seul) paramètre pour qu’il soit self dans la signature et partout dans le corps. Dans main, là où nous appelions la fonction area en passant rect1 comme argument, nous pouvons à la place utiliser la syntaxe de méthode pour appeler la méthode area sur notre instance de Rectangle. La syntaxe de méthode s’écrit après une instance : nous ajoutons un point suivi du nom de la méthode, des parenthèses et des éventuels arguments.
Dans la signature de area, nous utilisons &self au lieu de rectangle: &Rectangle. Le &self est en fait l’abréviation de self: &Self. Dans un bloc impl, le type Self est un alias pour le type auquel le bloc impl est destiné. Les méthodes doivent avoir un paramètre nommé self de type Self comme premier paramètre, donc Rust vous permet d’abréger cela avec uniquement le nom self en première position de paramètre. Notez que nous devons toujours utiliser le & devant le raccourci self pour indiquer que cette méthode emprunté l’instance de Self, tout comme nous l’avons fait avec rectangle: &Rectangle. Les méthodes peuvent prendre possession de self, emprunter self de manière immutable, comme nous l’avons fait ici, ou emprunter self de manière mutable, tout comme elles le peuvent pour n’importe quel autre paramètre.
Nous avons choisi &self ici pour la même raison que nous avons utilisé &Rectangle dans la version avec fonction : nous ne voulons pas prendre possession, et nous voulons seulement lire les données de la struct, pas les modifier. Si nous voulions modifier l’instance sur laquelle nous avons appelé la méthode dans le cadre de ce que fait la méthode, nous utiliserions &mut self comme premier paramètre. Avoir une méthode qui prend possession de l’instance en utilisant simplement self comme premier paramètre est rare ; cette technique est généralement utilisée quand la méthode transformé self en quelque chose d’autre et que vous voulez empêcher l’appelant d’utiliser l’instance originale après la transformation.
La raison principale d’utiliser des méthodes plutôt que des fonctions, en plus de fournir la syntaxe de méthode et de ne pas avoir à répéter le type de self dans la signature de chaque méthode, est l’organisation. Nous avons regroupé tout ce que nous pouvons faire avec une instance d’un type dans un seul bloc impl plutôt que d’obliger les futurs utilisateurs de notre code à chercher les fonctionnalités de Rectangle à différents endroits dans la bibliothèque que nous fournissons.
Notez que nous pouvons choisir de donner à une méthode le même nom qu’un des champs de la struct. Par exemple, nous pouvons définir une méthode sur Rectangle qui s’appelle également width :
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
Ici, nous choisissons de faire en sorte que la méthode width retourné true si la valeur du champ width de l’instance est supérieure à 0 et false si la valeur est 0 : nous pouvons utiliser un champ dans une méthode du même nom pour n’importe quel usage. Dans main, quand nous faisons suivre rect1.width de parenthèses, Rust sait que nous parlons de la méthode width. Quand nous n’utilisons pas de parenthèses, Rust sait que nous parlons du champ width.
Souvent, mais pas toujours, quand nous donnons à une méthode le même nom qu’un champ, nous voulons qu’elle retourné uniquement la valeur du champ sans rien faire d’autre. Les méthodes comme celles-ci sont appelées des accesseurs (getters), et Rust ne les implémente pas automatiquement pour les champs des structs comme le font certains autres langages. Les accesseurs sont utiles car vous pouvez rendre le champ privé mais la méthode publique et ainsi permettre un accès en lecture seule à ce champ dans le cadre de l’API publique du type. Nous verrons ce que sont le public et le privé et comment désigner un champ ou une méthode comme public ou privé au [chapitre 7][public].
Où est l’opérateur -> ?
En C et C++, deux opérateurs différents sont utilisés pour appeler des méthodes : vous utilisez . si vous appelez une méthode directement sur l’objet et -> si vous appelez la méthode sur un pointeur vers l’objet et que vous devez d’abord déréférencer le pointeur. Autrement dit, si object est un pointeur, object->something() est similaire à (*object).something().
Rust n’a pas d’équivalent à l’opérateur -> ; à la place, Rust à une fonctionnalité appelée référencement et déréférencement automatiques. L’appel de méthodes est l’un des rares endroits en Rust où ce comportement s’applique.
Voici comment cela fonctionne : quand vous appelez une méthode avec object.something(), Rust ajouté automatiquement &, &mut ou * pour que object corresponde à la signature de la méthode. Autrement dit, les deux lignes suivantes sont équivalentes :
#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self, other: &Point) -> f64 {
let x_squared = f64::powi(other.x - self.x, 2);
let y_squared = f64::powi(other.y - self.y, 2);
f64::sqrt(x_squared + y_squared)
}
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}
La première forme est bien plus propre. Ce comportement de référencement automatique fonctionne car les méthodes ont un receveur clair — le type de self. Étant donné le receveur et le nom d’une méthode, Rust peut déterminer avec certitude si la méthode est en lecture (&self), en mutation (&mut self) ou en consommation (self). Le fait que Rust rende implicite l’emprunt pour les receveurs de méthodes est une grande partie de ce qui rend la possession ergonomique en pratique.
Méthodes avec plus de paramètres
Pratiquons l’utilisation des méthodes en implémentant une deuxième méthode sur la struct Rectangle. Cette fois, nous voulons qu’une instance de Rectangle prenne une autre instance de Rectangle et retourné true si le second Rectangle peut tenir entièrement dans self (le premier Rectangle) ; sinon, elle devrait retourner false. C’est-à-dire qu’une fois que nous aurons défini la méthode can_hold, nous voulons pouvoir écrire le programme montré dans le listing 5-14.
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold methodLa sortie attendue ressemblerait à ce qui suit car les deux dimensions de rect2 sont plus petites que les dimensions de rect1, mais rect3 est plus large que rect1 :
Can rect1 hold rect2? true
Can rect1 hold rect3? false
Nous savons que nous voulons définir une méthode, donc elle sera dans le bloc impl Rectangle. Le nom de la méthode sera can_hold, et elle prendra un emprunt immutable d’un autre Rectangle comme paramètre. Nous pouvons deviner le type du paramètre en regardant le code qui appelle la méthode : rect1.can_hold(&rect2) passe &rect2, qui est un emprunt immutable de rect2, une instance de Rectangle. Cela a du sens car nous n’avons besoin que de lire rect2 (plutôt que d’écrire, ce qui signifierait que nous aurions besoin d’un emprunt mutable), et nous voulons que main conserve la possession de rect2 pour pouvoir l’utiliser à nouveau après l’appel à la méthode can_hold. La valeur de retour de can_hold sera un booléen, et l’implémentation vérifiera si la largeur et la hauteur de self sont respectivement supérieures à la largeur et à la hauteur de l’autre Rectangle. Ajoutons la nouvelle méthode can_hold au bloc impl du listing 5-13, comme le montre le listing 5-15.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold method on Rectangle that takes another Rectangle instance as a parameterQuand nous exécutons ce code avec la fonction main du listing 5-14, nous obtiendrons la sortie souhaitée. Les méthodes peuvent prendre plusieurs paramètres que nous ajoutons à la signature après le paramètre self, et ces paramètres fonctionnent exactement comme les paramètres dans les fonctions.
Fonctions associées
Toutes les fonctions définies dans un bloc impl sont appelées fonctions associées car elles sont associées au type nommé après le impl. Nous pouvons définir des fonctions associées qui n’ont pas self comme premier paramètre (et ne sont donc pas des méthodes) car elles n’ont pas besoin d’une instance du type pour fonctionner. Nous avons déjà utilisé une telle fonction : la fonction String::from qui est définie sur le type String.
Les fonctions associées qui ne sont pas des méthodes sont souvent utilisées comme constructeurs qui retourneront une nouvelle instance de la struct. Elles sont souvent appelées new, mais new n’est pas un nom spécial et n’est pas intégré au langage. Par exemple, nous pourrions choisir de fournir une fonction associée nommée square qui aurait un seul paramètre de dimension et l’utiliserait à la fois comme largeur et comme hauteur, rendant ainsi plus facile la création d’un Rectangle carré plutôt que de devoir spécifier la même valeur deux fois :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
Les mots-clés Self dans le type de retour et dans le corps de la fonction sont des alias pour le type qui apparaît après le mot-clé impl, qui dans ce cas est Rectangle.
Pour appeler cette fonction associée, nous utilisons la syntaxe :: avec le nom de la struct ; let sq = Rectangle::square(3); en est un exemple. Cette fonction est dans l’espace de noms de la struct : la syntaxe :: est utilisée à la fois pour les fonctions associées et les espaces de noms créés par les modules. Nous aborderons les modules au [chapitre 7][modules].
Blocs impl multiples
Chaque struct est autorisée à avoir plusieurs blocs impl. Par exemple, le listing 5-15 est équivalent au code montré dans le listing 5-16, qui à chaque méthode dans son propre bloc impl.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
impl blocksIl n’y à aucune raison de séparer ces méthodes en plusieurs blocs impl ici, mais c’est une syntaxe valide. Nous verrons un cas où les blocs impl multiples sont utiles au chapitre 10, où nous aborderons les types génériques et les traits.
Résumé
Les structs vous permettent de créer des types personnalisés qui ont du sens pour votre domaine. En utilisant les structs, vous pouvez garder des éléments de données associés connectés les uns aux autres et nommer chaque élément pour rendre votre code clair. Dans les blocs impl, vous pouvez définir des fonctions qui sont associées à votre type, et les méthodes sont un type de fonction associée qui vous permet de spécifier le comportement que les instances de vos structs possèdent.
Mais les structs ne sont pas le seul moyen de créer des types personnalisés : tournons-nous vers la fonctionnalité enum de Rust pour ajouter un autre outil à votre boîte à outils.