I. Les bases de la programmation en Rust▲
I-A. Présentation de Rust▲
Rust est un langage de programmation système, compilé et multiparadigme. C'est un croisement entre langage impératif (C), objet (C++), fonctionnel (Ocaml) et concurrent (Erlang). Il s'inspire des recherches en théories des langages de ces dernières années et des langages de programmation les plus populaires afin d'atteindre trois objectifs : rapidité, sécurité (en mémoire notamment) et concurrent (partage des données sécurisé entre tâches).
Le développement du langage, initié par Graydon Hoare, est opéré depuis 2009 par la fondation Mozilla, ainsi que par la communauté des développeurs Rust très présente sur Github. Pour suivre ce tutoriel, il est fortement recommandé d'avoir déjà développé dans au moins un autre langage (C, C++, Java, JavaScript, Python, etc.), car je ne passerai que très brièvement sur les bases. Ses points forts sont :
- la gestion de « propriété » des variables ;
- la gestion de la mémoire ;
- le typage statique ;
- l'inférence de type ;
- le filtrage par motif (pattern matching)Â ;
- la généricité.
Nous reverrons tout cela plus en détail. Quelques liens utiles :
- le site Internet : rust-lang.org ;
- la documentation (toujours utile d'avoir ça sous la main !) ;
- le dépôt Github (pour voir le code source) ;
- le rustbook (le « cours » officiel, en anglais) ;
- le reddit (pour poser une question)Â ;
- l'irc (pour obtenir de l'aide, en anglais). Le channel français est #rust-fr.
Il est maintenant temps de commencer.
I-B. Mise en place des outils▲
Pour pouvoir développer en Rust, il va déjà falloir les bons outils. Ici, je ne ferai qu'une présentation rapide de ceux que je connais. Pour écrire le code, vous pouvez utiliser soit :
- Soit http://www.tutorialspoint.com/compile_rust_online.php qui permet d'éditer, compiler et exécuter des projets complets (répartis sur plusieurs fichiers) tout en proposant des outils d'import et d'export.
- Soit un éditeur de texte. Pour le moment, il n'existe pas d'IDE dédié pour Rust, il va falloir attendre encore un peu avant d'en voir un émerger.
J'utilise personnellement sublime text. Si vous souhaitez l'utiliser et que vous voulez avoir la coloration syntaxique pour Rust, je vous invite à vous rendre sur cette page. Au final, ça donne ceci :
Après il vous suffit de suivre les instructions et vous aurez un éditeur de texte prêt à l'emploi ! Je tiens cependant à préciser que n'importe quel éditeur de texte fera l'affaire, sublime text n'est que ma préférence personnelle !
I-B-1. Le compilateur de Rust▲
Si vous ne souhaitez pas utiliser l'éditeur Rust en ligne, il va vous falloir télécharger le compilateur de Rust disponible ici, puis l'installer.
Nous pouvons maintenant commencer à nous intéresser au langage Rust à proprement parler !
I-C. Premier programme▲
Pour pouvoir tester tout ce que nous avons mis en œuvre dans le chapitre précédent, je vous propose d'écrire votre premier programme en Rust :
fn
main() {
println!
("Hello world!"
);
}
I-C-1. Si vous n'utilisez pas play.rust-lang▲
Maintenant que nous avons créé le fichier, compilons-le :
>
rustc votre_fichier.rs
Vous devriez maintenant avoir un exécutable votre_fichier. Lançons-le :
Sous Windows :
>
.\hello_world.exe
Sous Linux/MacOSÂ :
>
./hello_world
Et vous devriez obtenir :
Hello world!
Si jamais vous voulez changer le nom de l'exécutable généré, il vous faudra utiliser l'option -o. Exemple :
>
rustc votre_fichier.rs -o le_nom_de_l_executable
I-C-2. Si vous utilisez play.rust-lang▲
Appuyez tout simplement sur le bouton « Run ».
Vous savez maintenant comment compiler et exécuter vos programmes.
I-D. Variables▲
La première chose à savoir en Rust est que les variables sont toutes constantes par défaut. Exemple :
let
i = 0
;
i = 2
; // Erreur !
Pour déclarer une variable mutable, il faut utiliser le mot-clé mut :
let
mut
i = 0
;
i = 2
; // Ok !
Maintenant, voyons comment fonctionnent les types en Rust. Ici, rien de nouveau, on a toujours des entiers, des flottants, des strings, etc. La seule différence viendra de leur écriture. Par exemple, pour déclarer un entier de 32 bits, vous ferez :
let
i : i32 = 0
;
// ou :
let
i = 0
i32;
Sachez aussi que le compilateur de Rust utilise l'inférence de type. En gros, on n'est pas obligé de déclarer le type d'une variable, il peut généralement le déduire tout seul. Exemple :
let
i = 0
; // donc c'est un entier visiblement
let
max = 10
i32;
if
i < max { // max est un i32, donc le compilateur en déduit que i en est un aussi
println!
("i est inférieur à max !"
);
}
Donc pour résumer, voici une petite liste des différents types de base disponibles :
- i8 : un entier signé de 8 bits
- i16
- i32
- i64
- u8 : un entier non signé de 8 bits
- u16
- u32
- u64
- f32Â : un nombre flottant de 32 bits
- f64Â : un nombre flottant de 64 bits
- String
- Slice (on va y revenir plus loin dans ce chapitre)
Sachez cependant que les types isize et usize existent aussi et sont l'équivalent de intptr_t et de uintptr_t en C/C++. En gros, sur un système 32 bits, ils feront respectivement 32 bits tandis qu'ils feront 64 bits sur un système 64 bits.
Dernier petit point à aborder : il est courant de croiser ce genre de code en C/C++/Java/etc. :
i++;
++i;
Cette syntaxe est invalide en Rust, il vous faudra donc utiliser :
i += 1
;
Autre détail qui peut avoir son importance : si on fait commencer le nom d'une variable par un '_', nous n'aurons pas de warning du compilateur si elle est inutilisée. Ça a son utilité dans certains cas, bien que cela reste assez restreint. Exemple :
let
_i = 0
;
Il est temps de revenir sur les slices.
I-D-1. Les slices▲
Pour faire simple, une slice représente un morceau de tableau. Pour ceux qui auraient fait du C/C++, c'est tout simplement un pointeur et une taille. Exemple :
let
tab = [0
, 1
, 2
]; // tab est un tableau contenant 0, 1 et 2
let
s = &
tab; // s est une slice "pointant" sur tab
println!
("
{:?}
"
, s); // ça affichera "[0, 1, 2]"
let
s = &
tab[1
..]; // s est maintenant une slice commençant à partir du 2e élément de tab
println!
("
{:?}
"
, s); // ça affichera "[1, 2]"
De la même façon qu'il est possible d'obtenir une slice à partir d'un tableau, on peut en obtenir à partir des Vecs :
let
mut
v : Vec<u8> = Vec::new();
v.push(0
);
v.push(1
);
v.push(2
);
let
s = &
v;
println!
("
{:?}
"
, s); // ça affichera "[0, 1, 2]"
let
s = &
v[1
..];
println!
("
{:?}
"
, s); // ça affichera "[1, 2]"
Voilà qui conclut ce chapitre.
I-E. Conditions et pattern matching▲
Nous allons d'abord commencer par les conditions :
I-E-1. if / else if / else▲
Les if / else if / else fonctionnent de la même façon qu'en C/C++/Java :
let
age : i32 = 17
;
if
age >= 18
{
println!
("majeur !"
);
} else
{
println!
("mineur !"
);
}
Vous aurez noté que je n'ai pas mis de parenthèses ('(' et ')') autour des conditions : elles sont superflues en Rust. Cependant, elles sont toujours nécessaires si vous faites des « sous »-conditions :
if
age > 18
&&
(age == 20
|| age == 24
) {
println!
("ok"
);
}
Par contre, les accolades '{' et '}' sont obligatoires, même si le bloc de votre condition ne contient qu'une seule ligne de code !
En bref : pas de parenthèses autour de la condition, mais accolades obligatoires autour du bloc de la condition.
Je vais profiter de ce chapitre pour aborder le pattern matching.
I-E-2. Pattern matching▲
Définition Wikipédia :
« Le filtrage par motif, en anglais pattern matching, est la vérification de la présence de constituants d'un motif par un programme informatique, ou parfois par un matériel spécialisé. »
Pour dire les choses plus simplement, c'est une condition permettant de faire les choses de manière différente. Grâce à ça, on peut comparer ce que l'on appelle des expressions de manière plus intuitive. Ceux ayant déjà utilisé des langages fonctionnels ne devraient pas se sentir dépaysés. Comme un code vaut mieux que de longues explications :
let
my_string = "hello"
;
match
my_string {
"bonjour"
=> {
println!
("francais"
);
}
"ciao"
=> {
println!
("italien"
);
}
"hello"
=> {
println!
("anglais"
);
}
"hola"
=> {
println!
("espagnol"
);
}
_ => {
println!
("je ne connais pas cette langue..."
);
}
}
Ici, ça affichera donc « anglais ».
Comme vous vous en doutez, on peut s'en servir sur n'importe quel type de variable. Après tout, il sert à comparer des expressions, vous pouvez très bien matcher sur un i32 ou un f64 si vous en avez besoin.
Concernant le _, il signifie « toutes les autres expressions ». C'est en quelque sorte le else du pattern matching (il fonctionne de la même manière que le default d'un switch C/C++/Java). Cependant, il est obligatoire de le mettre si toutes les expressions possibles n'ont pas été testées ! Dans le cas présent, il est impossible de tester toutes les strings existantes, on met donc _ à la fin. Si on teste un booléen, on pourra faire :
let
b = true
;
match
b {
true
=> {
// faire quelque chose autre
}
false
=> {
// faire autre chose
}
}
Car il n'y a que deux valeurs possibles et qu'elles ont toutes les deux été testées ! Un autre exemple en utilisant un i32 :
let
age : i32 = 18
;
match
age {
17
=> {
println!
("mineur !"
);
}
18
=> {
println!
("majeur !"
);
}
_ => {
println!
("ni 17, ni 18 !"
);
}
}
C'est le moment où vous vous dites quelque chose du genre « mais c'est nul ! On ne va pas s'amuser à écrire toutes les valeurs en dessous de 18 pour voir s'il est majeur ! ». Sachez que vous avez tout à fait raison, dans le cas ci-dessus, le mieux serait d'écrire :
let
age : i32 = 17
;
match
age {
tmp if
tmp > 17
=> {
println!
("majeur !"
);
}
_ => {
println!
("mineur !"
);
}
}
Et là , vous vous demandez sans doute : « mais d'où il sort ce tmp ?! ». C'est une variable créée (temporairement) à l'intérieur du bloc du match contenant la valeur de l'expression évaluée (la variable age en l'occurrence, donc 17). Elle nous permet d'ajouter une condition à notre pattern matching afin d'affiner les résultats ! On peut donc ajouter des && ou des || selon les besoins.
Un petit détail avant de passer à la suite :
let
my_string = "hello"
;
let
s = match
my_string {
"bonjour"
=> "francais"
,
"ciao"
=> "italien"
,
"hello"
=> "anglais"
,
"hola"
=> "espagnol"
,
_ => "je ne connais pas cette langue..."
}; // on met un ';' ici, car ce match retourne un type
println!
("
{}
"
, s);
Tout comme pour les if/else if/else, il est possible de retourner une valeur d'un pattern matching et donc de la mettre directement dans une variable. Du coup, le ';' est nécessaire pour terminer ce bloc. Essayez le code suivant si vous avez encore un peu de mal à comprendre :
fn
main() {
if
1
== 2
{
"2"
} else
{
"1"
}
println!
("fini"
);
}
I-E-3. Toujours plus loin !▲
Il est aussi possible de matcher directement sur un ensemble de valeurs de cette façon :
let
i = 0
i32;
match
i {
10
...100
=> println!
("La variable est entre 10 et 100 (inclus)"
),
x => println!
("
{}
n'est pas entre 10 et 100 (inclus)"
, x)
};
À noter, dans le cas d'un for
i in
10
..100
{ println!
("
{}
"
, i); }, i prendra une valeur allant de 10 Ã 99 inclus.
Pratique ! Vous pouvez aussi « binder » (ou matcher sur un ensemble de valeurs) la variable avec le symbole « @ » :
let
i = 0
i32;
match
i {
x @ 10
...100
=> println!
("
{}
est entre 9 et 101"
, x),
x => println!
("
{}
n'est pas entre 9 et 101"
, x)
};
Il ne nous reste maintenant plus qu'un dernier point à aborder :
match
une_variable {
"jambon"
| "poisson"
| "oeuf"
=> println!
("Des protéines !"
),
"bonbon"
=> println!
("Des bonbons !"
),
"salade"
| "épinards"
| "fenouil"
=> println!
("Beurk ! Des légumes !"
),
_ => println!
("ça, je sais pas ce que c'est..."
)
}
Vous l'aurez sans doute deviné : ici, le '|' sert de condition « ou ». Dans le premier cas, si une_variable vaut « jambon », « poisson » ou « œuf », le match rentrera dans cette condition, et ainsi de suite.
Voilà qui clôt ce chapitre sur les conditions et le pattern matching. Encore une fois, n'hésitez pas à revenir sur des points que vous n'êtes pas sûr d'avoir parfaitement compris. Ce que nous voyons actuellement est vraiment la base de ce langage. Si quelque chose n'est pas parfaitement maîtrisé, vous risquez d'avoir du mal à comprendre la suite de ce cours.
I-F. if let / while let▲
C'est la suite directe du cours précédent. Ce chapitre est court mais très pratique.
I-F-1. Qu'est-ce que le if let ?▲
Le if
let
permet de simplifier certains traitements de pattern matching. Prenons un exemple :
fn
fais_quelque_chose(i: i32) -> Option
<String> {
if
i < 10
{
Some
("variable inférieure à 10"
.to_owned())
} else
{
None
}
}
Normalement, pour vérifier le retour de cette fonction, vous utiliseriez un match :
match
fais_quelque_chose(1
) {
Some
(s) => println!
("
{}
"
, &
s),
None
=> {} // rien à afficher donc on ne fait rien
}
Hé bien avec le if
let
vous pouvez faire :
if
let
Some
(s) = fais_quelque_chose(1
) {
println!
("
{}
"
, &
s)
}
Et c'est tout. Pour faire simple, si le type renvoyé par la fonction fais_quelque_chose correspond à celui donné au if
let
, le code du if sera exécuté. On peut bien évidemment le coupler avec un else
 :
if
let
Some
(s) = fais_quelque_chose(1
) {
println!
("
{}
"
, &
s)
} else
{
println!
("il ne s'est rien passé"
)
}
Essayez en passant un nombre supérieur à 10 comme argument, vous devriez rentrer dans le else
.
I-F-2. while let▲
Le while
let
fonctionne de la même façon : tant que le type renvoyé correspondra au type attendu, la boucle continuera. Donc le code suivant :
let
mut
v = vec!
(1
, 2
, 3
);
loop
{
match
v.pop() {
Some
(x) => println!
("
{}
"
, x),
None
=> break
,
}
}
Deviendra :
let
mut
v = vec!
(1
, 2
, 3
);
while
let
Some
(x) = v.pop() {
println!
("
{}
"
, x);
}
I-G. Les boucles▲
Les boucles sont l'une des bases de programmation, il est donc impératif de regarder comment ça fonctionne en Rust.
I-G-1. while▲
Comme dans les autres langages, elle continue tant que sa condition est respectée. Exemple :
let
mut
i : i32 = 0
;
while
i < 10
{
println!
("bonjour !"
);
i += 1
;
}
Ici, le programme affichera bonjour tant que i sera inférieur à 10.
Il faut cependant faire attention à plusieurs choses :
- Vous noterez encore une fois qu'il n'y a pas de parenthèses autour de la condition !
- Tout comme pour les conditions, les accolades sont encore une fois obligatoires !
Il existe aussi la possibilité d'écrire des boucles infinies avec le mot-clé loop (plutôt qu'un while true) :
I-G-2. loop▲
Je pense que vous vous demandez tous : « mais à quoi ça peut bien nous servir ? ». Prenons un exemple : un jeu vidéo. L'affichage doit continuer en permanence jusqu'à ce que l'on quitte. Plutôt que d'écrire :
while
true
{
//...
}
// ou
let
mut
end = false
;
while
end == false
{
//...
}
On écrira tout simplement :
loop
{
//...
}
Maintenant vous vous dites sans doute : « Ok, mais comment on l'arrête cette boucle ? ». Tout simplement avec le mot-clé break . Reprenons notre exemple du début :
let
mut
i : i32 = 0
;
loop
{
println!
("bonjour !"
);
i += 1
;
if
i > 10
{
break
;
}
}
Petit rappel concernant les mots-clés break et return : le mot-clé break permet seulement de quitter la boucle courante :
loop
{
println!
("Toujours là !"
);
let
mut
i = 0
i32;
loop
{
println!
("sous-boucle !"
);
i += 1
;
if
i > 2
{
break
; // et on revient dans la boucle précédente
}
}
}
Tandis que le mot-clé return fait quitter la fonction courante :
fn
main() {
loop
{
println!
("Toujours là !"
);
let
mut
i = 0
i32;
loop
{
println!
("sous-boucle !"
);
i += 1
;
if
i > 2
{
return
; // on quitte la fonction main et donc le programme se termine
}
}
}
}
I-G-3. for▲
La boucle for est un peu plus complexe que les deux précédentes. Elle ne fonctionne qu'avec des objets implémentant le trait IntoIterator. Vous ne savez pas ce qu'est un trait ? Pour le moment ce n'est pas important, j'y reviendrai plus tard. Prenons maintenant des exemples de comment fonctionne la boucle for :
for
i in
0
..10
{
println!
("i vaut :
{}
"
, i);
}
Ce qui va afficher :
0
1
2
3
4
5
6
7
8
9
Comme vous l'aurez compris, la variable i créée pour la boucle for prendra successivement toutes les valeurs allant de 0 à 9 puis la boucle s'arrêtera toute seule.
Certains d'entre vous doivent se demander si je n'ai pas menti en disant que la boucle for ne s'utilisait que sur des objets en voyant ce « 0..10 ». Hé bien sachez que non, ce « 0..10 » est considéré comme un objet de type Range qui implémente le trait IntoIterator. J'insiste sur le fait que c'est tout à fait normal si vous ne comprenez pas toutes mes explications pour l'instant.
Prenons un deuxième exemple :
let
v = vec!
(1
, 4
, 5
, 10
, 6
);
for
value in
v {
println!
("
{}
"
, value);
}
Ce qui va afficher :
1
4
5
10
6
I-G-4. Énumération▲
Si vous souhaitez savoir combien de fois vous avez itéré, vous pouvez utiliser la fonction enumerate :
for
(i, j) in
(5
..10
).enumerate() {
println!
("i =
{}
et j =
{}
"
, i, j);
}
Ce qui affichera :
i =
0
et j =
5
i =
1
et j =
6
i =
2
et j =
7
i =
3
et j =
8
i =
4
et j =
9
i vaut donc le nombre d'itérations effectuées à l'intérieur de la boucle tandis que j prend successivement les valeurs range. Autre exemple :
let
v = vec!
("a"
, "b"
, "c"
, "d"
);
for
(i, value) in
v.iter().enumerate() {
println!
("i =
{}
et value =
\"{}\"
"
, i, value);
}
I-G-5. Les boucles nommées▲
Encore une autre chose intéressante à connaître : les boucles nommées ! Mieux vaut commencer par un exemple :
'outer
: for
x in
0
..10
{
'inner
: for
y in
0
..10
{
if
x % 2
== 0
{ continue
'outer
; } // on continue la boucle sur x
if
y % 2
== 0
{ continue
'inner
; } // on continue la boucle sur y
println!
("x:
{}
, y:
{}
"
, x, y);
}
}
Je pense que vous l'aurez compris, on peut directement reprendre ou arrêter une boucle en utilisant son nom (pour peu que vous lui en ayez donné un bien évidemment). Autre exemple :
'global
: for
_ in
0
..10
{
'outer
: for
x in
0
..10
{
'inner
: for
y in
0
..10
{
if
x > 3
{ break
'global
; } // on arrête la boucle qui s'appelle global
if
x % 2
== 0
{ continue
'outer
; } // on continue la boucle sur x
if
y % 2
== 0
{ continue
'inner
; } // on continue la boucle sur y
println!
("x:
{}
, y:
{}
"
, x, y);
}
}
}
Encore une fois, je vous invite à tester pour bien comprendre comment tout ça fonctionne. Quand ce sera bon, il sera temps de passer aux fonctions !
I-H. Les fonctions▲
Jusqu'à présent, nous n'utilisions qu'une seule fonction : main. Pour le moment c'est amplement suffisant, mais quand vous voudrez faire des programmes beaucoup plus gros, ça deviendra vite ingérable. Je vais donc vous montrer comment créer des fonctions en Rust.
Commençons avec un exemple :
fn
addition(nb1: i32, nb2: i32) -> i32;
Ceci est donc une fonction appelée addition qui prend deux variables de types i32 en paramètre et retourne un i32. Rien de très différent de ce que vous connaissez déjà donc. Maintenant un exemple d'utilisation :
fn
main() {
println!
("1 + 2 =
{}
"
, addition(1
, 2
));
}
fn
addition(nb1: i32, nb2: i32) -> i32 {
nb1 + nb2
}
Ce qui affiche :
1 + 2 = 3
Ceux ayant bien compris le chapitre précédent se demanderont sans doute : « Tu nous avais parlé du mot-clé return, mais tu ne t'en sers pas ! Comment les valeurs ont-elles été retournées ? D'ailleurs, ne manque-t-il pas un point-virgule là  ? ».
Le fait de ne pas mettre de point-virgule signifie que l'on veut que « nb1 + nb2 » soit interprété comme une expression. Cependant, on pourrait tout aussi bien écrire :
fn
addition(nb1: i32, nb2: i32) -> i32 {
return
nb1 + nb2;
}
Ne vous inquiétez pas si vous ne comprenez pas tout parfaitement, nous verrons les expressions dans le chapitre suivant. Un autre exemple pour illustrer cette différence :
fn
get_bigger(nb1: i32, nb2: i32) -> i32 {
if
nb1 > n2 {
return
nb1;
}
nb2
}
Cette façon de faire n'est cependant pas recommandée en Rust, il aurait mieux valu écrire :
fn
get_bigger(nb1: i32, nb2: i32) -> i32 {
if
nb1 > nb2 {
nb1
} else
{
nb2
}
}
Une autre différence que certains d'entre vous auront peut-être notée (surtout ceux ayant déjà codé en C/C++) : je n'ai pas déclaré ma fonction addition et pourtant la fonction main l'a trouvée sans problème. Sachez juste que les déclarations de fonctions ne sont pas nécessaires en Rust si elles sont dans le même fichier (contrairement au C ou au C++ par exemple).
Voilà pour les fonctions, rien de bien nouveau par rapport aux autres langages que vous pourriez déjà connaître.
Il reste cependant un dernier point à éclaircir : println! et tous les appels ayant un '!' ne sont pas des fonctions, ce sont des macros.
Si vous pensez qu'elles ont quelque chose à voir avec celles que l'on peut trouver en C ou en C++, détrompez-vous ! Elles sont l'une des plus grandes forces de Rust, elles sont aussi très complètes et permettent d'étendre les possibilités du langage. Par contre, elles sont très complexes et seront le sujet d'un autre chapitre.
Pour le moment, sachez juste que :
fonction!
(); // c'est une macro
fonction(); // c'est une fonction
Enfin, une dernière chose : si vous souhaitez déclarer une fonction qui ne retourne rien (parce qu'elle ne fait qu'afficher du texte par exemple), vous pouvez la déclarer des façons suivantes :
fn
fait_quelque_chose() {
println!
("Je fais quelque chose !"
);
}
// ou bien :
fn
fait_quelque_chose() -> () {
println!
("Je fais quelque chose !"
);
}
« Le type () ? C'est une sorte de null ? »
Oui… Et non. En Rust, c'est un tuple vide. Son équivalent le plus proche en C/C++ est le type void.
Voilà , qui clôture ce chapitre. Il est maintenant temps de s'attaquer aux expressions !
I-I. Les expressions▲
Il faut bien comprendre que Rust est un langage basé sur les expressions. Avant de bien pouvoir vous les expliquer, il faut savoir qu'il y a les expressions et les déclarations. Leur différence fondamentale est que la première retourne une valeur alors que la seconde non. C'est pourquoi il est possible de faire ceci :
let
var = if
true
{
1
u32
} else
{
2
u32
};
Mais pas ça :
let
var = (let
var2 = 1
u32);
C'est tout simplement parce que le mot-clé let introduit une assignation et ne peut donc être considéré comme une expression. C'est donc une déclaration. Ainsi, il est possible de faire :
let
mut
var = 0
i32;
let
var2 = (var = 1
i32);
Car (var = 1i32) est considéré comme une expression.
Attention cependant, une assignation de valeur retourne le type () (qui est un tuple vide, son équivalent le plus proche en C/C++ est le type void comme je vous l'ai expliqué dans le chapitre précédent) et non la valeur assignée contrairement à un langage comme le C par exemple.
Un autre point important d'une expression est qu'elle ne peut pas se terminer par un point-virgule. Démonstration :
let
var : i32 = if
true
{
1
u32;
} else
{
2
u32;
};
Il vous dira à ce moment-là que le if else renvoie '()' et donc qu'il ne peut pas compiler, car il attendait un entier et j'ai explicitement demandé au compilateur de créer une variable var de type i32.
Je présume que vous vous dites : « Encore ce '()' ?! ». Hé oui. Je pense à présent que vous avez un petit aperçu de ce que sont les expressions. Il est très important que vous compreniez bien ce concept pour pouvoir aborder la suite de ce cours. Un dernier exemple d'une expression :
fn
test_expression(x: i32) -> i32 {
if
x < 0
{
println!
("
{}
< 0"
, x);
-1
} else
if
x == 0
{
println!
("
{}
== 0"
, x);
0
} else
{
println!
("
{}
> 0"
, x);
1
}
}
Il est temps de passer à la suite !
I-J. Gestion des erreurs▲
Il est courant dans d'autres langages de voir ce genre de code :
Objet *
obj =
creer_objet();
if
(obj ==
NULL
) {
// gestion de l'erreur
}
Vous ne verrez (normalement) pas ça en Rust.
I-J-1. Result▲
Créons un fichier par exemple :
use
std::fs::File;
let
mut
fichier = File::open("fichier.txt"
);
La documentation dit que File::open renvoie un Result. Il ne nous est donc pas possible d'utiliser directement la variable fichier. Cela nous « oblige » à vérifier le retour de File::open :
use
std::fs::File;
let
mut
fichier = match
File::open("fichier.txt"
) {
Ok
(f) => {
// Okay, l'ouverture du fichier s'est bien déroulée, on renvoie l'objet
f
},
Err
(e) => {
// Il y a eu un problème, affichons l'erreur pour voir ce qu'il se passe
println!
("
{}
"
, e);
// on ne peut pas renvoyer le fichier ici, donc on quitte la fonction
return
;
}
};
Il est cependant possible de passer outre cette vérification, mais c'est à vos risques et périls !
use
std::fs::File;
let
mut
fichier = File::open("fichier.txt"
).unwrap();
Si jamais il y a une erreur lors de l'ouverture du fichier, votre programme plantera et vous ne pourrez rien y faire. Il est toutefois possible d'utiliser cette méthode de manière « sûre » avec les fonctions is_ok et is_err :
use
std::fs::File;
let
mut
fichier = File::open("fichier.txt"
);
if
fichier.is_ok() {
// on peut faire unwrap !
} else
{
// il y a eu une erreur, unwrap impossible !
}
Utiliser le pattern matching est cependant préférable.
I-J-2. Option▲
Vous savez maintenant qu'il n'est normalement pas possible d'avoir des objets invalides. Exemple :
let
mut
v = vec!
(1
, 2
);
v.pop(); // retourne Some(2)
v.pop(); // retourne Some(1)
v.pop(); // retourne None
Cependant, il est tout à fait possible que vous ayez besoin d'avoir un objet qui serait initialisé plus tard pendant le programme ou qui vous permettrait de vérifier un état. Dans ce cas, comment faire ? Option est là pour ça !
Imaginons que vous ayez un vaisseau customisable sur lequel il est possible d'avoir des bonus (disons un salon intérieur). Il ne sera pas là au départ, mais peut être ajouté par la suite :
struct
Vaisseau {
// pleins de trucs
salon: Option
<Salon>
}
impl
Vaisseau {
pub
fn
new() -> Vaisseau {
Vaisseau {
// on initialise le reste
salon: None
// on n'a pas de salon
}
}
}
let
mut
vaisseau = Vaisseau::new();
Donc pour le moment, on n'a pas de salon. Maintenant nous en rajoutons un :
vaisseau.salon = Some
(Salon::new());
Je présume que vous vous demandez comment accéder au salon maintenant. Tout simplement comme ceci :
match
vaisseau.salon {
Some
(s) => {
println!
("ce vaisseau a un salon"
);
},
None
=> {
println!
("ce vaisseau n'a pas de salon"
);
}
}
Au début, vous risquez de trouver ça agaçant, mais la sécurité que cela apporte est un atout non négligeable ! Cependant, tout comme avec Result, vous pouvez utiliser la fonction unwrap.
vaisseau.salon = Some
(Salon::new());
let
salon = vaisseau.salon.unwrap(); // je ne le recommande pas !
Tout comme avec Result, il est possible de se passer du mécanisme de pattern matching avec les méthodes is_some et is_none :
if
vaisseau.salon.is_some() {
// on peut unwrap !
} else
{
// ce vaisseau ne contient pas de salon !
}
Encore une fois, utiliser le pattern matching est préférable.
I-J-3. panic!▲
Cette macro est très utile puisqu'elle permet de quitter le programme. Elle n'est à appeler que lorsque le programme a une erreur irrécupérable. Elle est très simple d'utilisation :
panic!
();
panic!
(4
); // panic avec une valeur de 4 pour la récupérer ailleurs (hors du programme par exemple)
panic!
("Une erreur critique vient d'arriver !"
);
panic!
("Une erreur critique vient d'arriver :
{}
"
, "le moteur droit est mort"
);
Et c'est tout.
I-J-4. try!▲
Et maintenant voici la macro try ! Elle permet de se « passer » du pattern matching en retournant directement le résultat en cas de réussite ou bien en quittant la fonction en cas d'erreur. Exemple :
let
mut
fichier = try!
(File::create("fichier.txt"
));
try!
(f.write_all("Test"
));
La fonction qui utilise cette macro doit obligatoirent retourner Err.
Voilà pour ce chapitre, vous devriez maintenant être capables de créer des codes un minimum sécurisés.
I-K. Cargo▲
Rust possède un gestionnaire de paquets : Cargo. Il permet de grandement faciliter la gestion de la compilation (en permettant de faire des builds personnalisées notamment) ainsi que des dépendances externes. La majeure partie des informations que je vais vous donner dans ce chapitre peuvent être retrouvées ici (en anglais). N'hésitez pas à y faire un tour !
Pour pouvoir se servir de Cargo, il va vous falloir créer un fichier Cargo.toml à la racine de votre projet :
[package]
name = "le_nom_du_projet"
version = "0.0.1"
authors = ["Votre nom <vous@exemple.com>"
]
Tous les fichiers sources (.rs normalement) doivent être placés dans un sous-dossier appelé src. C'est-à -dire qu'on va avoir un fichier main.rs dans le dossier src :
fn
main() {
println!
("Début du projet"
);
}
Maintenant pour compiler le projet, il vous suffit de faire :
cargo build
Et voilà  ! L'exécutable sera généré dans le dossier target/debug/. Pour le lancer :
>
./target/debug/le_nom_du_projet
Début du projet
Si vous voulez compiler et lancer l'exécutable tout de suite après, vous pouvez utiliser la commande run :
>
cargo run
Fresh le_nom_du_projet v0.0
.1
(
file:///path/to/project/le_nom_du_projet)
Running `target/debug/le_nom_du_projet`
Début du projet
Et voilà  !
Par défaut, cargo compile en mode debug. Si vous souhaitez compiler en mode release, il vous faudra passer l'option « --release » :
>
cargo build --release
Bien évidemment, l'exécutable généré se trouvera dans le dossier target/release.
I-K-1. Gérer les dépendances▲
Si vous voulez utiliser une bibliothèque externe, cargo peut le gérer pour vous. Il y a plusieurs façons de faire :
- Soit la bibliothèque est disponible sur crates.io, et dans ce cas il vous suffira de préciser la version que vous désirez.
- Soit elle ne l'est pas : dans ce cas, vous pourrez indiquer son chemin d'accès si elle est présente sur votre ordinateur, soit vous pourrez donner son adresse github.
Par exemple, vous voulez utiliser la bibliothèque GTK+, elle est disponible sur crates.io (ici) donc pas de souci :
[package]
name = "le_nom_du_projet"
version = "0.0.1"
authors = ["Votre nom <vous@exemple.com>"
]
[dependencies]
gtk = "*"
Nous avons donc ajouté GTK+ comme dépendance à notre projet. Détail important : à chaque fois que vous ajoutez/modifiez/supprimez une dépendance, il vous faudra relancer cargo build pour que ce soit pris en compte ! D'ailleurs, si vous souhaitez mettre à jour les bibliothèques que vous utilisez, il vous faudra utiliser la commande :
>
cargo update
Je ne rentrerai pas plus dans les détails concernant l'utilisation d'une bibliothèque externe ici, car le chapitre suivant traite de ce sujet.
Si vous voulez utiliser une version précise de GTK , vous pouvez la préciser comme ceci :
[dependencies]
gdk = "0.0.2"
Il est cependant possible de faire des choses un peu plus intéressantes avec la gestion des versions. Par exemple, vous pouvez autoriser certaines versions de la bibliothèque :
Le « ^ » permet notamment :
^1
.2
.3
:= >=1
.2
.3
<2
.0
.0
^0
.2
.3
:= >=0
.2
.3
<0
.3
.0
^0
.0
.3
:= >=0
.0
.3
<0
.0
.4
^0
.0
:= >=0
.0
.0
<0
.1
.0
^0
:= >=0
.0
.0
<1
.0
.0
Le « ~ » permet :
~1
.2
.3
:= >=1
.2
.3
<1
.3
.0
~1
.2
:= >=1
.2
.0
<1
.3
.0
~1
:= >=1
.0
.0
<2
.0
.0
Le « * » permet :
* := >=0
.0
.0
1
.* := >=1
.0
.0
<2
.0
.0
1
.2
.* := >=1
.2
.0
<1
.3
.0
Et enfin les symboles d'(in)égalité permettent :
>= 1
.2
.0
> 1
< 2
= 1
.2
.3
Il est possible de mettre plusieurs exigences en les séparant avec une virgule : >= 1
.2
, < 1
.5
..
Maintenant, regardons comment ajouter une dépendance à une bibliothèque qui n'est pas sur crates.io (ou qui y est, mais pour une raison ou pour une autre, vous ne voulez pas passer par elle) :
[package]
name = "le_nom_du_projet"
version = "0.0.1"
authors = ["Votre nom <vous@exemple.com>"
]
[dependencies.gtk]
git = "https://github.com/rust-gnome/gtk"
Ici nous avons indiqué que la bibliothèque gtk se trouvait à cette adresse de github. Il est aussi possible que vous l'ayez téléchargée, dans ce cas il va vous falloir indiquer où elle se trouve :
[dependencies.gtk]
path = "chemin/vers/gtk"
Voici en gros à quoi ressemblerait un gros fichier cargo :
[package]
name = "le_nom_du_projet"
version = "0.0.1"
authors = ["Votre nom <vous@exemple.com>"
]
[dependencies.gtk]
git = "https://github.com/rust-gnome/gtk"
[dependencies.gsl]
version = "0.0.1"
# optionnel
path = "path/vers/gsl"
[dependencies]
sdl = "*"
cactus = "0.2.3"
I-K-2. Publier une bibliothèque sur crates.io▲
Vous avez fait une bibliothèque et vous avez envie de la mettre à disposition des autres développeurs ? Pas de souci ! Tout d'abord, il va vous falloir un compte sur crates.io (pour le moment, il semblerait qu'il faille obligatoirement un compte sur github pour pouvoir se connecter sur crates.io). Une fois que c'est fait, allez sur la page de votre compte. Vous devriez voir ça écrit dessus :
cargo login abcdefghijklmnopqrstuvwxyz012345
Exécutez cette commande sur votre ordinateur pour que cargo puisse vous identifier. IMPORTANT : CETTE CLEF NE DOIT PAS ÊTRE TRANSMISE !!! Si jamais elle venait à être divulguée à quelqu'un d'autre que vous-même, régénérez-en une nouvelle aussitôt !
Regardons maintenant les metadata que nous pouvons indiquer pour permettre « d'identifier » notre bibliothèque :
- description : brève description de la bibliothèque.
- documentation : URL vers la page où se trouve la documentation de votre bibliothèque.
- homepage : URL vers la page de présentation de votre bibliothèque.
- repository : URL vers le dépôt où se trouve le code source de votre bibliothèque.
- readme : chemin de l'emplacement du fichier README (relatif au fichier Cargo.toml).
- keywords : mots-clés permettant pour catégoriser votre bibliothèque.
- license : licence(s) de votre bibliothèque. On peut en mettre plusieurs en les séparant avec un '/'. La liste des licences disponibles se trouve ici.
- license-file : si la licence que vous cherchez n'est pas dans la liste de celles disponibles, vous pouvez donner le chemin du fichier contenant la vôtre (relatif au fichier Cargo.toml).
Je vais vous donner ici le contenu du fichier Cargo.toml de la bibliothèque GTK pour que vous ayez un exemple :
[package]
name = "gtk"
version = "0.0.2"
authors = ["The Rust-GNOME Project Developers"
]
description = "Rust bindings for the GTK+ library"
repository = "https://github.com/rust-gnome/gtk"
license = "MIT"
homepage = "https://github.com/rust-gnome/gtk"
documentation = "https://github.com/rust-gnome/gtk"
readme = "README.md"
keywords = ["gtk"
, "gnome"
, "GUI"
]
[lib]
name = "gtk"
[features]
default = ["gtk_3_6"
]
gtk_3_4 = ["gtk-sys/gtk_3_4"
, "gdk/gdk_3_4"
]
gtk_3_6 = ["gtk-sys/gtk_3_6"
, "gdk/gdk_3_6"
, "gtk_3_4"
]
gtk_3_8 = ["gtk-sys/gtk_3_8"
, "gdk/gdk_3_8"
, "gtk_3_6"
]
gtk_3_10 = ["gtk-sys/gtk_3_10"
, "gdk/gdk_3_10"
, "cairo-rs/cairo_1_12"
, "gtk_3_8"
]
gtk_3_12 = ["gtk-sys/gtk_3_12"
, "gdk/gdk_3_12"
, "gtk_3_10"
]
gtk_3_14 = ["gtk-sys/gtk_3_14"
, "gdk/gdk_3_14"
, "gtk_3_12"
]
[dependencies]
libc = "0.1"
gtk-sys = "^0"
glib = "^0"
glib-sys = "^0"
gdk-sys = "^0"
gdk = "^0"
pango-sys = "^0"
pango = "^0"
cairo-sys-rs = "^0"
cairo-rs = "^0"
Voilà  ! Comme vous pouvez le voir, il y a aussi une option [features]. Elle permet dans le cas de GTK de faire une compilation conditionnelle dépendant de la version que vous possédez sur votre ordinateur. Vous ne pouvez par exemple pas utiliser du code de la version 3.12 si vous avez une version 3.4.
Nous voilà enfin à la dernière étape : publier la bibliothèque. ATTENTION : une bibliothèque publiée ne peut pas être supprimée ! Il n'y a pas de limite non plus sur le nombre de versions qui peuvent être publiées.
Le nom sous lequel votre bibliothèque sera publiée est celui donné par la metadata name :
[package]
name = "super"
Si une bibliothèque portant le nom « super » est déjà publiée sur crates.io, vous ne pourrez rien y faire, il faudra trouver un autre nom. Une fois que tout est prêt, utilisez la commande :
>
cargo publish
Et voilà , votre bibliothèque est maintenant visible sur crates.io et peut être utilisée par tout le monde !
I-L. Utiliser des bibliothèques externes▲
Nous avons vu comment gérer les dépendances vers des bibliothèques externes dans le précédent chapitre, il est temps de voir comment s'en servir.
Commençons par le fichier Cargo.toml, ajoutez ces deux lignes :
[dependencies]
time = "*"
Nous avons donc ajouté une dépendance vers la bibliothèque time. Maintenant dans votre fichier principal (celui que vous avez indiqué à Cargo), ajoutez :
extern
crate
time;
Pour appeler une fonction depuis la bibliothèque, il suffit de faire :
println!
("
{:?}
"
, time::now());
Et c'est tout ! Les imports fonctionnent de la même façon :
use
time::Tm;
Voilà qui conclut ce chapitre !
I-M. Jeu du plus ou moins▲
Le but de ce chapitre est de mettre en pratique ce que vous avez appris dans les chapitres précédents au travers de l'écriture d'un jeu du plus ou moins. Voici le déroulement :
- L'ordinateur choisit un nombre (on va dire entre 1 et 100).
- Vous devez deviner le nombre.
- Vous gagnez si vous le trouvez en moins de 10 essais.
Relativement simple. Je pense que vous commencez déjà à voir comment tout ça va s'articuler. Exemple d'une partie :
Génération du nombre...
C'est parti !
Entrez un nombre : 50
-> C'est plus grand
Entrez un nombre : 75
-> C'est plus petit
Entrez un nombre : 70
Vous avez gagné !
(Je sais, je suis vraiment trop fort à ce jeu)
« Mais comment fait-on pour générer un nombre aléatoire ? »
Bonne question ! On va utiliser la bibliothèque externe rand. Ajoutez-la comme dépendance dans votre fichier Cargo.toml et ensuite importez-la dans votre fichier principal. Maintenant, pour générer un nombre il vous suffira de faire :
use
rand::Rng;
let
nombre_aleatoire = rand::thread_rng().gen_range(1
, 101
);
Il va aussi falloir récupérer ce que l'utilisateur écrit sur le clavier. Pour cela, utilisez la méthode read_line de l'objet Stdin (qu'on peut récupérer avec la fonction stdin). Il ne vous restera plus qu'à convertir cette String en entier en utilisant la méthode from_str. Je pense vous avoir donné assez d'indications pour que vous puissiez vous débrouiller seuls. Bon courage !
Maintenant vous savez ce que vous avez à faire. Je propose une solution juste en dessous pour ceux qui n'y arriveraient pas ou qui souhaiteraient tout simplement comparer leur code avec le mien.
I-M-1. La solution▲
Je vais écrire cette solution en essayant de rester aussi clair que possible sur ce que je fais. Commençons par la fonction qui se chargera de nous retourner le nombre entré par l'utilisateur :
use
std::io;
use
std::str::FromStr;
fn
recuperer_entree_utilisateur() -> Option
<isize> { // elle ne prend rien en entrée et retourne un Option<isize> (dans le cas où ça ne fonctionnerait pas)
let
mut
entree = String::new();
match
io::stdin().read_line(&
mut
entree) { // on récupère ce qu'a entré l'utilisateur dans la variable entree
Ok
(_) => { // tout s'est bien passé, on peut convertir la String en entier
match
isize::from_str(&
entree.trim()) { // la méthode trim enlève tous les caractères "blancs" en début et fin de chaîne
Ok
(nombre) => Some
(nombre), // tout s'est bien déroulé, on retourne donc le nombre
Err
(_) => { // si jamais la conversion échoue (si l'utilisateur n'a pas rentré un nombre valide), on retourne None
println!
("Veuillez entrer un nombre valide !"
);
None
}
}
},
_ => { // une erreur s'est produite, on doit avertir l'utilisateur !
println!
("Erreur lors de la récupération de la saisie..."
);
None
}
}
}
Voilà une bonne chose de faite ! Il va nous falloir à présent implémenter le cœur du jeu :
use
std::io::Write; // utilisé pour "flusher" la sortie console
fn
jeu() -> bool {
let
mut
tentative = 10
; // on va mettre 10 tentatives avant de lui dire qu'il a perdu
println!
("Génération du nombre..."
);
let
nombre_aleatoire = rand::thread_rng().gen_range(1
, 101
);
println!
("C'est parti !"
);
while
tentative > 0
{
print!
("Entrez un nombre : "
); // on ne veut pas de retour à la ligne !
io::stdout().flush(); // si on n'utilise pas cette méthode, on ne verra pas l'affichage de print! tout de suite
match
recuperer_entree_utilisateur() {
Some
(nombre) => {
if
nombre < nombre_aleatoire {
println!
("C'est plus grand !"
);
} else
if
nombre > nombre_aleatoire {
println!
("C'est plus petit !"
);
} else
{
return
true
;
}
}
None
=> {}
}
tentative -= 1
;
}
false
}
Il ne nous reste désormais plus qu'à appeler cette fonction dans notre main et le tour est joué !
fn
main() {
println!
("=== Jeu du plus ou moins ==="
);
println!
(""
);
if
jeu() == true
{
println!
("Vous avez gagné !"
);
} else
{
println!
("Vous avez perdu..."
);
}
}
Voici maintenant le code complet (non commenté) de ma solution :
extern
crate
rand;
use
rand::Rng;
use
std::io::Write;
use
std::io;
use
std::str::FromStr;
fn
recuperer_entree_utilisateur() -> Option
<isize> {
let
mut
entree = String::new();
match
io::stdin().read_line(&
mut
entree) {
Ok
(_) => {
match
isize::from_str(&
entree.trim()) {
Ok
(nombre) => Some
(nombre),
Err
(_) => {
println!
("Veuillez entrer un nombre valide !"
);
None
}
}
},
_ => {
println!
("Erreur lors de la récupération de la saisie..."
);
None
}
}
}
fn
jeu() -> bool {
let
mut
tentative = 10
;
println!
("Génération du nombre..."
);
let
nombre_aleatoire = rand::thread_rng().gen_range(1
, 101
);
println!
("C'est parti !"
);
while
tentative > 0
{
print!
("Entrez un nombre : "
);
io::stdout().flush();
match
recuperer_entree_utilisateur() {
Some
(nombre) => {
if
nombre < nombre_aleatoire {
println!
("C'est plus grand !"
);
} else
if
nombre > nombre_aleatoire {
println!
("C'est plus petit !"
);
} else
{
return
true
;
}
}
None
=> {}
}
tentative -= 1
;
}
false
}
fn
main() {
println!
("=== Jeu du plus ou moins ==="
);
println!
(""
);
if
jeu() == true
{
println!
("Vous avez gagné !"
);
} else
{
println!
("Vous avez perdu..."
);
}
}
Si vous avez un problème, des commentaires ou autres à propos de cette solution, n'hésitez pas à venir en parler sur #rust-fr ou directement sur github en ouvrant une issue.
I-M-2. Améliorations▲
Il est possible d'ajouter quelques améliorations à cette version comme :
- un mode 2 joueurs ;
- proposer la possibilité de recommencer quand on a fini une partie ;
- afficher le nombre de coups qu'il a fallu pour gagner (et pourquoi pas sauvegarder les meilleurs scores ?) ;
- proposer plusieurs modes de difficulté ;
- …
Les choix sont vastes, à vous de faire ce qui vous amuse !