Tutoriel pour apprendre les bases de la programmation en Rust

Image non disponible


précédentsommairesuivant

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 :

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 :

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 :

Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
> rustc votre_fichier.rs

Vous devriez maintenant avoir un exécutable votre_fichier. Lançons-le :

Sous Windows :

 
Sélectionnez
> .\hello_world.exe

Sous Linux/MacOS :

 
Sélectionnez
> ./hello_world

Et vous devriez obtenir :

 
Sélectionnez
Hello world!

Si jamais vous voulez changer le nom de l'exécutable généré, il vous faudra utiliser l'option -o. Exemple :

 
Sélectionnez
> 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 :

 
Sélectionnez
let i = 0;

i = 2; // Erreur !

Pour déclarer une variable mutable, il faut utiliser le mot-clé mut :

 
Sélectionnez
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 :

 
Sélectionnez
let i : i32 = 0;
// ou :
let i = 0i32;

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 :

 
Sélectionnez
let i = 0; // donc c'est un entier visiblement
let max = 10i32;

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. :

 
Sélectionnez
i++;
++i;

Cette syntaxe est invalide en Rust, il vous faudra donc utiliser :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
let i = 0i32;

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 « @ » :

 
Sélectionnez
let i = 0i32;

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
let mut v = vec!(1, 2, 3);

loop {
    match v.pop() {
        Some(x) => println!("{}", x),
        None => break,
    }
}

Deviendra :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
while true {
    //...
}

// ou

let mut end = false;

while end == false {
    //...
}

On écrira tout simplement :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
loop {
    println!("Toujours  !");
    let mut i = 0i32;

    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 :

 
Sélectionnez
fn main() {
    loop {
        println!("Toujours  !");
        let mut i = 0i32;

        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 :

 
Sélectionnez
for i in 0..10 {
    println!("i vaut : {}", i);
}

Ce qui va afficher :

 
Sélectionnez
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 :

 
Sélectionnez
let v = vec!(1, 4, 5, 10, 6);

for value in v {
    println!("{}", value);
}

Ce qui va afficher :

 
Sélectionnez
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 :

 
Sélectionnez
for (i, j) in (5..10).enumerate() {
    println!("i = {} et j = {}", i, j);
}

Ce qui affichera :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
'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 :

 
Sélectionnez
'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 :

 
Sélectionnez
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 :

 
Sélectionnez
fn main() {
    println!("1 + 2 = {}", addition(1, 2));
}

fn addition(nb1: i32, nb2: i32) -> i32 {
    nb1 + nb2
}

Ce qui affiche :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
let var = if true {
    1u32
} else {
    2u32
};

Mais pas ça :

 
Sélectionnez
let var = (let var2 = 1u32);

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 :

 
Sélectionnez
let mut var = 0i32;
let var2 = (var = 1i32);

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 :

 
Sélectionnez
let var : i32 = if true {
    1u32;
} else {
    2u32;
};

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 !

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
vaisseau.salon = Some(Salon::new());

Je présume que vous vous demandez comment accéder au salon maintenant. Tout simplement comme ceci :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
[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 :

 
Sélectionnez
fn main() {
    println!("Début du projet");
}

Maintenant pour compiler le projet, il vous suffit de faire :

 
Sélectionnez
cargo build

Et voilà ! L'exécutable sera généré dans le dossier target/debug/. Pour le lancer :

 
Sélectionnez
> ./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 :

 
Sélectionnez
> 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 » :

 
Sélectionnez
> 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 :

 
Sélectionnez
[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 :

 
Sélectionnez
> 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 :

 
Sélectionnez
[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 :

 
Sélectionnez
^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 :

 
Sélectionnez
~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 :

 
Sélectionnez
* := >=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 :

 
Sélectionnez
>= 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) :

 
Sélectionnez
[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 :

 
Sélectionnez
[dependencies.gtk]
path = "chemin/vers/gtk"

Voici en gros à quoi ressemblerait un gros fichier cargo :

 
Sélectionnez
[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 :

 
Sélectionnez
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 :

 
Sélectionnez
[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 :

 
Sélectionnez
[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 :

 
Sélectionnez
> 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 :

 
Sélectionnez
[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 :

 
Sélectionnez
extern crate time;

Pour appeler une fonction depuis la bibliothèque, il suffit de faire :

 
Sélectionnez
println!("{:?}", time::now());

Et c'est tout ! Les imports fonctionnent de la même façon :

 
Sélectionnez
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 :

  1. L'ordinateur choisit un nombre (on va dire entre 1 et 100).
  2. Vous devez deviner le nombre.
  3. 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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  ç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 :

 
Sélectionnez
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é !

 
Sélectionnez
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 :

 
Sélectionnez
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 !


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Guillaume Gomez. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.