Prise en main de la syntaxe des méthodes en Rust

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Method Syntax. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, les méthodes sont déclarées avec le mot clé fn et un nom, peuvent avoir des paramètres et une valeur de retour, et sont définies dans le contexte d'un struct, le premier paramètre étant toujours self pour représenter l'instance du struct sur laquelle la méthode est appelée.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100397{{"Prise en main de la syntaxe des méthodes en Rust"}} rust/integer_types -.-> lab-100397{{"Prise en main de la syntaxe des méthodes en Rust"}} rust/boolean_type -.-> lab-100397{{"Prise en main de la syntaxe des méthodes en Rust"}} rust/function_syntax -.-> lab-100397{{"Prise en main de la syntaxe des méthodes en Rust"}} rust/expressions_statements -.-> lab-100397{{"Prise en main de la syntaxe des méthodes en Rust"}} rust/method_syntax -.-> lab-100397{{"Prise en main de la syntaxe des méthodes en Rust"}} rust/traits -.-> lab-100397{{"Prise en main de la syntaxe des méthodes en Rust"}} end

Method Syntax

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é lorsqu'on appelle la méthode depuis un autre endroit. Contrairement aux fonctions, les méthodes sont définies dans le contexte d'un struct (ou d'un enum ou d'un trait object, que nous aborderons respectivement dans le Chapitre 6 et le Chapitre 17), et leur premier paramètre est toujours self, qui représente l'instance du struct sur laquelle la méthode est appelée.

Définition de méthodes

Modifions la fonction area qui prend une instance de Rectangle en paramètre et définissons plutôt une méthode area sur la structure Rectangle, comme indiqué dans la Liste 5-13.

Nom de fichier : src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

1 impl Rectangle {
  2 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.",
      3 rect1.area()
    );
}

Liste 5-13 : Définition d'une méthode area sur la structure Rectangle

Pour définir la fonction dans le contexte de Rectangle, nous commençons un bloc impl (implémentation) pour Rectangle [1]. Tout ce qui se trouve à l'intérieur de ce bloc impl sera associé au type Rectangle. Ensuite, nous déplaçons la fonction area à l'intérieur des accolades impl [2] et changeons le premier (et dans ce cas, unique) paramètre pour qu'il soit self dans la signature et partout dans le corps. Dans main, où nous appelions la fonction area et passions rect1 en argument, nous pouvons maintenant utiliser la syntaxe des méthodes pour appeler la méthode area sur notre instance de Rectangle [3]. La syntaxe des méthodes suit une instance : nous ajoutons un point suivi du nom de la méthode, des parenthèses et de tout argument.

Dans la signature de area, nous utilisons &self au lieu de rectangle: &Rectangle. Le &self est en fait une abréviation de self: &Self. Dans un bloc impl, le type Self est un alias pour le type sur lequel porte le bloc impl. Les méthodes doivent avoir un paramètre nommé self du type Self pour leur premier paramètre, donc Rust vous permet d'abréger cela en utilisant seulement le nom self dans le premier emplacement de paramètre. Notez que nous devons toujours utiliser le & devant l'abréviation self pour indiquer que cette méthode emprunte l'instance Self, tout comme nous l'avons fait avec rectangle: &Rectangle. Les méthodes peuvent prendre la propriété de self, emprunter self de manière immuable, comme nous l'avons fait ici, ou emprunter self de manière mutable, tout comme elles peuvent tout autre paramètre.

Nous avons choisi &self ici pour la même raison que nous avons utilisé &Rectangle dans la version fonction : nous ne voulons pas prendre la propriété, et nous ne voulons que lire les données dans la structure, pas les modifier. Si nous avions voulu modifier l'instance sur laquelle nous avons appelé la méthode en tant que partie de ce que fait la méthode, nous aurions utilisé &mut self comme premier paramètre. Il est rare d'avoir une méthode qui prend la propriété de l'instance en utilisant seulement self comme premier paramètre ; cette technique est généralement utilisée lorsque la méthode transforme self en quelque chose d'autre et que vous voulez empêcher l'appelant d'utiliser l'instance d'origine après la transformation.

La principale raison d'utiliser des méthodes plutôt que des fonctions, en plus de fournir la syntaxe des méthodes et de ne pas avoir à répéter le type de self dans la signature de chaque méthode, est pour 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 de forcer les futurs utilisateurs de notre code à chercher les capacité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 structure. Par exemple, nous pouvons définir une méthode sur Rectangle qui s'appelle également width :

Nom de fichier : src/main.rs

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 renvoie true si la valeur dans le 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 but. Dans main, lorsque nous suivons rect1.width de parenthèses, Rust sait que nous voulons dire la méthode width. Lorsque nous n'utilisons pas de parenthèses, Rust sait que nous voulons dire le champ width.

Souvent, mais pas toujours, lorsque nous donnons à des méthodes le même nom qu'un champ, nous voulons qu'elles ne fassent que renvoyer la valeur dans le champ et ne fassent rien d'autre. Les méthodes comme celles-ci sont appelées getters, et Rust ne les implémente pas automatiquement pour les champs de structure comme le font certaines autres langues. Les getters sont utiles car vous pouvez rendre le champ privé mais la méthode publique, et ainsi autoriser un accès en lecture seulement à ce champ en tant que partie de l'API publique du type. Nous discuterons de ce qu'est public et privé et de la manière de désigner un champ ou une méthode comme public ou privé au Chapitre 7.

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. En d'autres termes, si object est un pointeur, object->quelque chose() est similaire à (*object).quelque chose().

Rust n'a pas d'équivalent à l'opérateur -> ; au lieu de cela, Rust a une fonctionnalité appelée référence et déréférence automatiques. Appeler des méthodes est l'un des rares endroits dans Rust où ce comportement existe.

Voici comment cela fonctionne : lorsque vous appelez une méthode avec object.quelque chose(), Rust ajoute automatiquement &, &mut ou * de sorte que object corresponde à la signature de la méthode. En d'autres termes, les suivantes sont équivalentes :

p1.distance(&p2);
(&p1).distance(&p2);

La première semble beaucoup plus propre. Ce comportement de référence automatique fonctionne car les méthodes ont un récepteur clair - le type de self. Étant donné le récepteur et le nom d'une méthode, Rust peut déterminer de manière définitive si la méthode lit (&self), modifie (&mut self) ou consomme (self). Le fait que Rust rende l'emprunt implicite pour les récepteurs de méthodes est une grande partie de ce qui rend la propriété 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 structure Rectangle. Cette fois, nous voulons qu'une instance de Rectangle prenne une autre instance de Rectangle et renvoie true si la deuxième Rectangle peut s'insérer entièrement dans self (la première Rectangle); sinon, elle devrait renvoyer false. C'est-à-dire que une fois que nous avons défini la méthode can_hold, nous voulons être capable d'écrire le programme montré dans la Liste 5-14.

Nom de fichier : src/main.rs

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

Liste 5-14 : Utilisation de la méthode can_hold non encore écrite

La sortie attendue serait la suivante 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 à l'intérieur du bloc impl Rectangle. Le nom de la méthode sera can_hold, et elle prendra un emprunt immuable d'une autre Rectangle en tant que paramètre. Nous pouvons déterminer le type du paramètre en regardant le code qui appelle la méthode : rect1.can_hold(&rect2) passe &rect2, qui est un emprunt immuable de rect2, une instance de Rectangle. Cela a du sens car nous avons seulement besoin 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 propriété de rect2 pour pouvoir la réutiliser après avoir 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 plus grandes que la largeur et la hauteur de l'autre Rectangle. Ajoutons la nouvelle méthode can_hold au bloc impl de la Liste 5-13, comme montré dans la Liste 5-15.

Nom de fichier : src/main.rs

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

Liste 5-15 : Implémentation de la méthode can_hold sur Rectangle qui prend une autre instance de Rectangle en tant que paramètre

Lorsque nous exécutons ce code avec la fonction main de la Liste 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 à l'intérieur d'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 donc ne sont pas des méthodes) car elles n'ont pas besoin d'une instance du type pour fonctionner. Nous avons déjà utilisé une fonction de ce type : 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 pour les constructeurs qui renvoient une nouvelle instance de la structure. Ces fonctions sont souvent appelées new, mais new n'est pas un nom spécial et n'est pas intégré à la langue. Par exemple, nous pourrions choisir de fournir une fonction associée nommée square qui aurait un paramètre de dimension et utiliserait cela comme largeur et hauteur, ce qui faciliterait la création d'un carré Rectangle plutôt que d'avoir à spécifier la même valeur deux fois :

Nom de fichier : src/main.rs

impl Rectangle {
    fn square(size: u32) -> 1 Self  {
      2 Self  {
            width: size,
            height: size,
        }
    }
}

Les mots clés Self dans le type de retour [1] et dans le corps de la fonction [2] 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 structure ; let sq = Rectangle::square(3); est un exemple. Cette fonction est organisée dans l'espace de nom de la structure : la syntaxe :: est utilisée pour les fonctions associées et les espaces de nom créés par les modules. Nous discuterons des modules au Chapitre 7.

Plusieurs blocs impl

Chaque structure est autorisée à avoir plusieurs blocs impl. Par exemple, la Liste 5-15 est équivalente au code montré dans la Liste 5-16, qui a chaque méthode dans son propre bloc impl.

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

Liste 5-16 : Reprise de la Liste 5-15 en utilisant plusieurs blocs impl

Il n'y a pas de raison de séparer ces méthodes en plusieurs blocs impl ici, mais c'est une syntaxe valide. Nous verrons un cas où plusieurs blocs impl sont utiles au Chapitre 10, où nous discuterons des types génériques et des traits.

Sommaire

Félicitations ! Vous avez terminé le laboratoire sur la syntaxe des méthodes. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.