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