Les fonctions

Les fonctions sont très utilisées dans le code Rust. Vous avez déjà vu l'une des fonctions les plus importantes du langage : la fonction main, qui est le point d'entrée de beaucoup de programmes. Vous avez aussi vu le mot-clé fn, qui vous permet de déclarer des nouvelles fonctions.

Le code Rust utilise le snake case comme convention de style de nom des fonctions et des variables, toutes les lettres sont en minuscule et on utilise des tirets bas pour séparer les mots. Voici un programme qui est un exemple de définition de fonction :

Fichier : src/main.rs

fn main() {
    println!("Hello, world!");

    une_autre_fonction();
}

fn une_autre_fonction() {
    println!("Une autre fonction.");
}

Nous définissons une fonction avec Rust en saisissant fn suivi par un nom de fonction ainsi qu'une paire de parenthèses. Les accolades indiquent au compilateur où le corps de la fonction commence et où il se termine.

Nous pouvons appeler n'importe quelle fonction que nous avons définie en utilisant son nom, suivi d'une paire de parenthèses. Comme une_autre_fonction est définie dans le programme, elle peut être appelée à l'intérieur de la fonction main. Remarquez que nous avons défini une_autre_fonction après la fonction main dans le code source ; nous aurions aussi pu la définir avant. Rust ne se soucie pas de l'endroit où vous définissez vos fonctions, du moment qu'elles sont bien définies quelque part.

Créons un nouveau projet de binaire qui s'appellera functions afin d'en apprendre plus sur les fonctions. Ajoutez l'exemple une_autre_fonction dans le src/main.rs et exécutez-le. Vous devriez avoir ceci :

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Une autre fonction.

Les lignes s'exécutent dans l'ordre dans lequel elles apparaissent dans la fonction main. D'abord, le message Hello, world! est écrit, et ensuite une_autre_fonction est appelée et son message est affiché.

Les paramètres

Nous pouvons définir des fonctions avec des paramètres, qui sont des variables spéciales qui font partie de la signature de la fonction. Quand une fonction a des paramètres, vous pouvez lui fournir des valeurs concrètes avec ces paramètres. Techniquement, ces valeurs concrètes sont appelées des arguments, mais dans une conversation courante, on a tendance à confondre les termes paramètres et arguments pour désigner soit les variables dans la définition d'une fonction, soit les valeurs concrètes passées quand on appelle une fonction.

Dans cette version de une_autre_fonction, nous ajoutons un paramètre :

Fichier : src/main.rs

fn main() {
    une_autre_fonction(5);
}

fn une_autre_fonction(x: i32) {
    println!("La valeur de x est : {}", x);
}

En exécutant ce programme, vous devriez obtenir ceci :

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
La valeur de x est : 5

La déclaration de une_autre_fonction a un paramètre nommé x. Le type de x a été déclaré comme i32. Quand nous passons 5 à une_autre_fonction, la macro println! place 5 là où la paire d'accolades {} a été placée dans la chaîne de formatage.

Dans la signature d'une fonction, vous devez déclarer le type de chaque paramètre. C'est un choix délibéré de conception de Rust : exiger l'annotation de type dans la définition d'une fonction fait en sorte que le compilateur n'a presque plus besoin que vous les utilisiez autre part pour qu'il comprenne avec quel type vous souhaitez travailler.

Lorsque vous définissez plusieurs paramètres, séparez les paramètres avec des virgules, comme ceci :

Fichier : src/main.rs

fn main() {
    afficher_mesure_avec_unite(5, 'h');
}

fn afficher_mesure_avec_unite(valeur: i32, unite: char) {
    println!("La mesure est : {}{}", valeur, unite);
}

Cet exemple crée la fonction afficher_mesure_avec_unite qui a deux paramètres. Le premier paramètre s'appelle valeur et est un i32. Le second, nom_unite, est de type char. La fonction affiche ensuite le texte qui contient les valeurs de valeur et de nom_unite.

Essayons d'exécuter ce code. Remplacez le programme présent actuellement dans votre fichier src/main.rs de votre projet functions par l'exemple précédent et lancez-le en utilisant cargo run :

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
La mesure est : 5h

Comme nous avons appelé la fonction avec la valeur 5 pour valeur et 'h' pour nom_unite, la sortie de ce programme contient ces valeurs.

Instructions et expressions

Les corps de fonctions sont constitués d'une série d'instructions qui se termine éventuellement par une expression. Jusqu'à présent, les fonctions que nous avons vu n'avaient pas d'expression à la fin, mais vous avez déjà vu une expression faire partie d'une instruction. Comme Rust est un langage basé sur des expressions, il est important de faire la distinction. D'autres langages ne font pas de telles distinctions, donc penchons-nous sur ce que sont les instructions et les expressions et comment leurs différences influent sur le corps des fonctions.

Les instructions effectuent des actions et ne retournent aucune valeur. Les expressions sont évaluées pour retourner une valeur comme résultat. Voyons quelques exemples.

Nous avons déjà utilisé des instructions et des expressions. La création d'une variable en lui assignant une valeur avec le mot-clé let est une instruction. Dans l'encart 3-1, let y = 6; est une instruction.

Fichier : src/main.rs

fn main() {
    let y = 6;
}

Encart 3-1 : une fonction main qui contient une instruction

La définition d'une fonction est aussi une instruction ; l'intégralité de l'exemple précédent est une instruction à elle toute seule.

Une instruction ne retourne pas de valeur. Ainsi, vous ne pouvez pas assigner le résultat d'une instruction let à une autre variable, comme le code suivant essaye de le faire, car vous obtiendrez une erreur :

Fichier : src/main.rs

fn main() {
    let x = (let y = 6);
}

Quand vous exécutez ce programme, l'erreur que vous obtenez devrait ressembler à ceci :

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are experimental
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
  = help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  | 

For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 2 previous errors; 1 warning emitted

L'instruction let y = 6 ne retourne pas de valeur, donc cela ne peut pas devenir une valeur de x. Ceci est différent d'autres langages, comme le C ou Ruby, où l'assignation retourne la valeur de l'assignation. Dans ces langages, vous pouvez écrire x = y = 6 et avoir ainsi x et y qui ont chacun la valeur 6 ; cela n'est pas possible avec Rust.

Les expressions sont calculées en tant que valeur et seront ce que vous écrirez le plus en Rust (hormis les instructions). Prenez une opération mathématique, comme 5 + 6, qui est une expression qui s'évalue à la valeur 11. Les expressions peuvent faire partie d'une instruction : dans l'encart 3-1, le 6 dans l'instruction let y = 6; est une expression qui s'évalue à la valeur 6. L'appel de fonction est aussi une expression. L'appel de macro est une expression. Un nouveau bloc de portée que nous créons avec des accolades est une expression, par exemple :

Fichier : src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("La valeur de y est : {}", y);
}

L'expression suivante…

{
    let x = 3;
    x + 1
}

… est un bloc qui, dans ce cas, s'évalue à 4. Cette valeur est assignée à y dans le cadre de l'instruction let. Remarquez la ligne x + 1 ne se termine pas par un point-virgule, ce qui est différent de la plupart des lignes que vous avez vues jusque là. Les expressions n'ont pas de point-virgule de fin de ligne. Si vous ajoutez un point-virgule à la fin de l'expression, vous la transformez en instruction, et elle ne va donc pas retourner de valeur. Gardez ceci à l'esprit quand nous aborderons prochainement les valeurs de retour des fonctions ainsi que les expressions.

Les fonctions qui retournent des valeurs

Les fonctions peuvent retourner des valeurs au code qui les appelle. Nous ne nommons pas les valeurs de retour, mais nous devons déclarer leur type après une flèche (->). En Rust, la valeur de retour de la fonction est la même que la valeur de l'expression finale dans le corps de la fonction. Vous pouvez sortir prématurément d'une fonction en utilisant le mot-clé return et en précisant la valeur de retour, mais la plupart des fonctions vont retourner implicitement la dernière expression. Voici un exemple d'une fonction qui retourne une valeur :

Fichier : src/main.rs

fn cinq() -> i32 {
    5
}

fn main() {
    let x = cinq();

    println!("La valeur de x est : {}", x);
}

Il n'y a pas d'appel de fonction, de macro, ni même d'instruction let dans la fonction cinq — uniquement le nombre 5 tout seul. C'est une fonction parfaitement valide avec Rust. Remarquez que le type de retour de la fonction a été précisé aussi, avec -> i32. Essayez d'exécuter ce code ; le résultat devrait ressembler à ceci :

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
La valeur de x est : 5

Le 5 dans cinq est la valeur de retour de la fonction, ce qui explique le type de retour de i32. Regardons cela plus en détail. Il y a deux éléments importants : premièrement, la ligne let x = cinq(); dit que nous utilisons la valeur de retour de la fonction pour initialiser la variable. Comme la fonction cinq retourne un 5, cette ligne revient à faire ceci :


#![allow(unused)]
fn main() {
let x = 5;
}

Deuxièmement, la fonction cinq n'a pas de paramètre et déclare le type de valeur de retour, mais le corps de la fonction est un simple 5 sans point-virgule car c'est une expression dont nous voulons retourner la valeur.

Regardons un autre exemple :

Fichier : src/main.rs

fn main() {
    let x = plus_un(5);

    println!("La valeur de x est : {}", x);
}

fn plus_un(x: i32) -> i32 {
    x + 1
}

Exécuter ce code va afficher La valeur de x est : 6. Mais si nous ajoutons un point-virgule à la fin de la ligne qui contient x + 1, ce qui la transforme d'une expression à une instruction, nous obtenons une erreur.

Fichier : src/main.rs

fn main() {
    let x = plus_un(5);

    println!("La valeur de x est : {}", x);
}

fn plus_un(x: i32) -> i32 {
    x + 1;
}

Compiler ce code va produire une erreur, comme ci-dessous :

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_un(x: i32) -> i32 {
  |    -------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: consider removing this semicolon

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error

Le message d'erreur principal, “mismatched types” (types inadéquats) donne le cœur du problème de ce code. La définition de la fonction plus_un dit qu'elle va retourner un i32, mais les instructions ne retournent pas de valeur, ceci est donc représenté par (), le type unité. Par conséquent, rien n'est retourné, ce qui contredit la définition de la fonction et provoque une erreur. Rust affiche un message qui peut aider à corriger ce problème : il suggère d'enlever le point-virgule, ce qui va résoudre notre problème.