Rust par l'exemple

Rust Core Team

Traducteur

Anthony Defranceschi

Rust est un langage système moderne mettant l'accent sur la sécurité, la vélocité et la concurrence. Il y parvient en gérant la mémoire sans utiliser de ramasse-miettes(garbage collector).

RBE est une collection d'exemples parfaitement exécutables qui illustre les différents concepts du langage Rust et ses bibliothèques standards.

Ce document est une traduction du livre Rust by Example.

Un espace de discussion vous est proposé sur le forum. N'hésitez pas à apporter vos avis sur ce tutoriel. Commentez

- Titre : Rust par l'exemple

- Auteur : Rust Core Team

- Traducteur : Anthony Defranceschi

- Parution : 23 avril 2018

- Licence : Le contenu de cet article est rédigé par Rust Core Team et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- 1. Hello World

- 1-1. Les commentaires

- 1-2. Affichage formaté

- 1-2-1. Debug

- 1-2-2. Display

- 1-2-2-1. Exemple d'utilisation : La structure List

- 1-2-3. Formatage

- 2. Les primitifs

- 2-1. Les littéraux et les opérateurs

- 2-2. Les tuples

- 2-3. Les tableaux et les "slices"

- 3. Les types personnalisés

- 3-1. Les structures

- 3-2. Les énumérations

- 3-2-1. Le mot-clé use

- 3-2-2. Énumérations "C-like"

- 3-2-3. Exemple d'utilisation : « linked-list »

- 3-3. Les constantes

- 4. Les assignations

- 4-1. Mutabilité

- 4-2. Scope et shadowing

- 4-3. Déclaration seule

- 5. Le casting

- 5-1. Les littéraux

- 5-2. L'inférence des types

- 5-3. Les alias

- 6. Les expressions

- 7. Contrôle du flux

- 7-1. If/else

- 7-2. Le mot-clé loop

- 7-2-1. L'imbrication et les labels

- 7-3. La boucle while

- 7-4. La boucle for et les invervalles

- 7-5. Le pattern matching

- 7-5-1. La déstructuration

- 7-5-1-1. Les tuples

- 7-5-1-2. Les énumérations

- 7-5-1-3. Les pointeurs et références

- 7-5-1-4. Les structures

- 7-5-2. Les gardes

- 7-5-3. Assignation

- 7-6. if let

- 7-7. while let

- 8. Les fonctions

- 8-1. Les méthodes

- 8-2. Les closures

- 8-2-1. Capture

- 8-2-2. Les closures passées en paramètres

- 8-2-3. Les types anonymes

- 8-2-4. Fonctions passées en paramètres

- 8-2-5. Renvoyer une closure

- 8-2-6. Exemples de la bibliothèque standard

- 8-2-6-1. Iterator::any

- 8-2-6-2. Iterator::find

- 8-3. Les fonctions d'ordre supérieur

- 9. Les modules

- 9-1. La visibilité

- 9-2. La visibilité des structures

- 9-3. La déclaration use

- 9-4. Les mot-clés super et self

- 9-5. La hiérarchie des fichiers

- 10. Les crates

- 10-1. Créer une bibliothèque

- 10-2. La déclaration extern crate

- 11. Les attributs

- 11-1. L'avertissement dead_code

- 11-2. Méta-données relatives aux crates

- 11-3. L'attribut cfg

- 11-3-1. Condition personnalisée

- 12. La généricité

- 12-1. Les fonctions

- 12-2. Implémentation générique

- 12-3. Les traits

- 12-4. Les restrictions

- 12-4-1. Exemple d'utilisation : Traits sans services

- 12-5. Restrictions multiples

- 12-6. La clause where

- 12-7. Les éléments associés

- 12-7-1. Le problème

- 12-7-2. Les types associés

- 12-8. Les paramètres fantômes

- 12-8-1. Exemple d'utilisation  Petite précision

- 13. Les contextes

- 13-1. Le RAII

- 13-2. Ownership et transferts

- 13-2-1. Mutabilité

- 13-3. Système d'emprunts

- 13-3-1. Mutabilité

- 13-3-2. Verrouillage des ressources

- 13-3-3. Limitations des accès lecture/écriture

- 13-3-4. Le pattern ref

- 13-4. Système de durée de vie

- 13-4-1. Les labels

- 13-4-2. Les fonctions

- 13-4-3. Les méthodes

- 13-4-4. Les structures

- 13-4-5. Les restrictions

- 13-4-6. La coercition

- 13-4-7. La lifetime 'static

- 13-4-8. Annotation implicite

- 14. Les traits

- 14-1. L'attribut Derive

- 14-2. La surcharge des opérateurs

- 14-3. Le trait Drop

- 14-4. Les itérateurs

- 14-5. Le trait Clone

- 15. macro_rules!

- 15-1. Les indicateurs

- 15-2. Surcharge

- 15-3. Répétition

- 15-4. DRY (Don't Repeat Yourself)

- 16. La gestion des erreurs

- 16-1. La macro panic

- 16-2. L'enum Option et la méthode unwrap

- 16-2-1. Les combinateurs: map

- 16-2-2. Les combinateurs: and_then

- 16-3. L'enum Result

- 16-3-1. La méthode map pour Result

- 16-3-2. Les alias de Result

- 16-4. Multiples types d'erreur

- 16-4-1. Retour prématuré

- 16-4-2. Introduction à try!

- 16-5. Définition d'un type d'erreur

- 16-6. D'autres cas d'utilisation de try!

- 16-7. Boxing des erreurs

- 17. Les types de la bibliothèque standard

- 17-1. Les Box, la pile et le tas

- 17-2. Les vecteurs

- 17-3. Les chaînes de caractères

- 17-4. L'énumération Option

- 17-5. L'énumération Result

- 17-5-1. La macro try!

- 17-6. La macro panic!

- 17-7. La structure HashMap

- 17-7-1. Personnaliser les types de clé

- 17-7-2. La structure HashSet

- 18. Outils standards

- 18-1. Les fils d'exécution

- 18-2. Les canaux

- 18-3. La structure Path

- 18-4. La structure File

- 18-4-1. La méthode open

- 18-4-2. La méthode create

- 18-5. Les sous-processus

- 18-5-1. Les pipes

- 18-5-2. La méthode wait

- 18-6. Opérations sur le système de fichiers

- 18-7. Les arguments du programme

- 18-7-1. Récupération des arguments

- 18-8. FFI

- 19. Divers

- 19-1. Documentation

- 19-2. Tests

- 20. Opérations à risque

- 21. Pour aller plus loin

- 23. Remerciements

1 - Hello World

Voici le code source d'un traditionnel « Hello World ».

1. // Ceci est un commentaire, et sera ignoré par le compilateur.

3. // Ceci est la fonction principale

4. fn main() {

5. // Toutes les déclarations se trouvant dans le corps de la fonction

6. // seront exécutées lorsque le binaire est exécuté.

7. // Afficher du texte dans la console.

8. println!("Hello World!");

9. }

println! est une macro qui affiche du texte sur la console.

Un binaire peut être généré en utilisant le compilateur Rust : rustc.

$ rustc hello.rs

rustc va produire un binaire nommé « hello » qui pourra être exécuté :

$ ./hello
Hello World!

Activité

Cliquez sur le bouton « Run » en début de section pour visualiser le résultat présenté. Ensuite, ajoutez une nouvelle ligne qui permettra de visualiser le résultat ci-dessous :

Hello World!
I'm a Rustacean!

1-1 - Les commentaires

N'importe quel programme a besoin de commentaires, c'est pour cela que Rust supporte différentes syntaxes :

Les commentaires basiques ignorés par le compilateur :

- // Les commentaires mono-lignes. ;

- /* Les blocs de commentaires régis par leurs délimiteurs. */.

Les commentaires dédiés à la documentation qui seront convertis au format HTML :

- /// Génère de la documentation pour ce qui suit ce commentaire. ;

- //! Génère la documentation pour un conteneur (e.g. un module) ;

- /*! Permet de rédiger un bloc entier de documentation.*/.

1-2 - Affichage formaté

L'affichage est pris en charge par une série de macros déclarées dans le module std::fmt qui inclut :

- format! : Construit la chaîne de caractères du texte à afficher ;

- print! : Fait exactement la même chose que format!, mais le texte est affiché dans la console ;

- println! : Fait exactement la même chose que print!, mais un retour à la ligne est ajouté.

Toutes formatent le texte de la même manière.

Note : la validité du formatage (i.e. Si la chaîne de caractères que vous soumettez peut être formatée comme vous le désirez) est vérifiée au moment de la compilation.

1. fn main(){

2. // En général, le marqueur '{}' sera automatiquement remplacé par

3. // n'importe quel argument. Il sera transformé en chaîne de caractères.

4. println!("{} jours", 31);

6. // Sans suffixe, 31 est de type i32. Vous pouvez changer le type de 31 avec

7. // un suffixe. (e.g. 31i64)

9. // Différents modèles peuvent être utilisés.

10. // Les marqueurs de position peuvent être utilisés.

11. println!("{0}, voici {1}. {1}, voici {0}", "Alice", "Bob");

13. // Les marqueurs peuvent également être

14. // nommés

15. println!("{sujet} {verbe} {objet}",

16. objet="le chien paresseux",

17. sujet="Rapide, le renard",

18. verbe="saute par-dessus");

20. // Un formatage spécial peut être spécifié après un ':'.

21. println!("{} personne sur {:b} sait lire le binaire, l'autre moité non.", 1, 2);

23. // Vous pouvez aligner vers la droite votre texte en spécifiant

24. // la largeur (en espace) entre le côté gauche de la console

25. // et votre chaîne. Cet exemple affichera: "     1", un "1" après 5 espaces.

27. println!("{number:>width$}", number=1, width=6);

29. // Vous pouvez également remplacer les white spaces par des '0'.

30. // Affiche: "000001"

32. println!("{number:>0width$}", number=1, width=6);

34. // Le nombre d'arguments utilisé est vérifié par le compilateur.

35. // println!("Mon nom est {0}, {1} {0}", "Bond");

36. // FIXME ^ Ajoutez l'argument manquant: "James".

38. // On créé une structure nommé 'Structure' contenant un entier de type 'i32'.

39. #[allow(dead_code)]

40. struct Structure(i32);

42. // Cependant, les types complexes tels que les structures demandent

43. // une gestion de l'affichage plus complexe. Cela ne fonctionnera pas.

44. // println!("Cette structure '{}' ne sera pas affichée...", Structure(3));

45. // FIXME ^ Commentez/Décommentez cette ligne pour voir le message d'erreur.

46. }

std::fmt contient plusieurs traits qui structurent l'affichage du texte. Les deux plus « importants » sont listés ci-dessous :

1. fmt::Debug : Utilise le marqueur {:?}. Applique un formatage dédié au débogage.

2. fmt::Display : Utilise le marqueur {}. Formate le texte de manière plus élégante, plus « user friendly ».

Dans cet exemple, fmt::Display était utilisé parce que la bibliothèque standard fournit les implémentations pour ces types. Pour afficher du texte à partir de types complexes/personnalisés, d'autres étapes sont requises.

Activité

Réglez les deux problèmes dans le code ci-dessus (cf. FIXME) pour qu'il s'exécute sans erreurs.

Ajoutez une macro println! qui affiche : « Pi est, à peu près, égal à 3,142 » en contrôlant le nombre affiché de chiffres après la virgule. Dans le cadre de l'exercice, vous utiliserez let pi = 3.141592 comme estimation de Pi (Note : vous pourriez avoir besoin de consulter la documentation du module std::fmt pour configurer le nombre de décimaux à afficher).

Voir aussi

std::fmt, les macros, les structures, les traits.

1-2-1 - Debug

Tous les types qui utilisent le formatage des traits du module std::fmt doivent en posséder une implémentation pour être affichés.

Les implémentations ne sont fournies automatiquement que pour les types supportés par la bibliothèque standard. Les autres devront l'implémenter « manuellement ».

Pour le trait fmt::Debug, rien de plus simple. Tous les types peuvent hériter de son implémentation (i.e. la créer automatiquement, sans intervention de votre part). Ce n'est, en revanche, pas le cas pour le second trait : fmt::Display.

1. // Cette structure ne peut être affichée par `fmt::Debug`,

2. // ni par `fmt::Display`.

3. struct UnPrintable(i32);

5. // L'attribut `derive` créé automatiquement l'implémentation requise

6. // pour permettre à cette structure d'être affichée avec `fmt::Debug`.

7. #[derive(Debug)]

8. struct DebugPrintable(i32);

Également, tous les types de la bibliothèque standard peuvent être automatiquement affichés avec le marqueur {:?} :

1. // On fait hériter l'implémentation de `fmt::Debug` pour `Structure`.

2. // `Structure` est une structure qui contient un simple entier de type `i32`.

3. #[derive(Debug)]

4. struct Structure(i32);

6. // On créé une structure nommée `Deep`, que l'on rend également affichable,

7. // contenant un champ de type `Structure`,

8. #[derive(Debug)]

9. struct Deep(Structure);

11. fn main() {

12. // L'affichage avec le marqueur `{:?}` est similaire à `{}`,

13. // pour des types standards comme les entiers et les chaînes de caractères.

14. println!("{:?} mois dans une année.", 12);

15. println!("{1:?} {0:?} est le nom de {actor:?}.",

16. "Slater",

17. "Christian",

18. actor="l'acteur");

20. // `Structure` peut être affichée !

21. println!("{:?} peut désormais être affichée!", Structure(3));

23. // Le problème avec `derive` est que vous n'avez aucun contrôle quant au résultat

24. // affiché. Comment faire si je souhaite seulement afficher `7` ?

25. println!("{:?} peut désormais être affichée!", Deep(Structure(7)));

26. }

Finalement, fmt::Debug permet de rendre un type personnalisé affichable en sacrifiant quelque peu « l'élégance » du résultat. Pour soigner cela, il faudra implémenter soit-même les services du traits fmt::Display.

Voir aussi

Les attributs, derive, std::fmt, les structures.

1-2-2 - Display

fmt::Debug propose un formatage rudimentaire, et il peut être de bon ton de soigner ce que nous affichons. Pour ce faire, il faudra implémenter fmt::Display (qui utilise le marqueur {}).

Voici un exemple d'implémentation du trait :

1. // On importe (via `use`) le module `fmt` pour le rendre accessible.

2. use std::fmt;

4. // Nous définissons une structure dans laquelle le trait `fmt::Display`

5. // sera implémenté. Ce n'est qu'un simple tuple, nommée `Structure`, contenant un entier de type i32.

6. struct Structure(i32);

8. // Pour pouvoir utiliser le marqueur `{}`, le trait `fmt::Display` doit être implémenté

9. // manuellement pour le type.

10. impl fmt::Display for Structure {

11. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

12. // écrit le premier élément de la structure dans le flux en sortie

13. // soumis: `f`. Renvoie une instance de `fmt::Result` qui témoigne du succès

14. // ou de l'échec de l'opération. Notez que `write!` possède une syntaxe très

15. // similaire à `println!`.

16. write!(f, "{}", self.0)

17. }

18. }

fmt::Display pourrait être plus lisible que fmt::Debug mais il présente un problème pour la bibliothèque standard. Comment les types ambiguës devraient être affichés ? Par exemple, si la bibliothèque standard devait implémenter un seul formatage pour toutes les « variantes » de Vec<T>, quel style devrait être choisi ? N'importe lequel ?

1. Vec<Path> : /:/etc:/home/username:/bin (séparé par des « : ») ;

2. Vec<i32> : 1,2,3 (séparé par des « , »).

Bien sûr que non, puisqu'il n'y a pas de mise en forme idéale pour tous les types et la bibliothèque standard n'en impose pas.

fmt::Display n'est pas implémenté pour la structure Vec<T> ni pour aucun autre conteneur générique. fmt::Debug  doit alors être utilisé pour ces ressources.

Ce n'est en revanche pas un problème pour les conteneurs(e.g. structures) qui ne sont pas génériques, fmt::Display peut être implémenté et utilisé.

1. use std::fmt; // On importe le module `fmt`

3. // Une structure qui contient deux nombres. `Debug` va être hérité pour que les résultats

4. // puissent être comparés avec `Display`.

5. #[derive(Debug)]

6. struct MinMax(i64, i64);

8. // Implémentation du trait `Display` pour la structure `MinMax`.

9. impl fmt::Display for MinMax {

10. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

11. // On utilise `self.nombre` pour faire référence à la donnée se trouvant

12. // à cette position.

13. write!(f, "({}, {})", self.0, self.1)

14. }

15. }

17. // Définissons une structure où les champs sont nommés pour comparer.

18. #[derive(Debug)]

19. struct Point2D {

20. x: f64,

21. y: f64,

22. }

24. // On implémente également le trait fmt::Display pour la structure Point2D

25. impl fmt::Display for Point2D {

26. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

27. // On désigne les champs de notre choix. (en l'occurrence `x` et `y`)

28. write!(f, "x: {}, y: {}", self.x, self.y)

29. }

30. }

32. fn main() {

33. let minmax = MinMax(0, 14);

35. println!("Comparaison des structures:");

36. println!("Display: {}", minmax);

37. println!("Debug: {:?}", minmax);

39. let big_range =   MinMax(-300, 300);

40. let small_range = MinMax(-3, 3);

42. println!("Le grand intervalle est {big} et le petit est {small}",

43. small = small_range,

44. big = big_range);

46. let point = Point2D { x: 3.3, y: 7.2 };

48. println!("Comparaison des points:");

49. println!("Display: {}", point);

50. println!("Debug: {:?}", point);

52. // Erreur. Les traits `Debug` and `Display` étaient implémentés mais

53. // le marqueur `{:b}` requiert l'implémentation du trait `fmt::Binary`.

54. // Cela ne fonctionnera pas.

55. // println!("A quoi ressemble Point2D formaté en binaire: {:b} ?", point);

56. }

Donc fmt::Display a été implémenté mais ce n'est pas le cas de fmt::Binary, il ne peut alors pas être utilisé.

std::fmt possède de nombreux traits et chacun doit posséder sa propre implémentation. Pour plus d'informations, nous vous invitons à consulter la documentation du module.

Activité

Après avoir constaté le résultat de l'exemple ci-dessus, aidez-vous de la structure Point2D pour ajouter à l'exemple une nouvelle structure nommée Complex. Voici le résultat attendu lorsqu'une instance de la structure Complex sera affichée :

Display: 3.3 + 7.2i
Debug: Complex { real: 3.3, imag: 7.2 }

Voir aussi

L'attribut derive, std::fmt, les macros, les structures, les traits, le mot-clé use.

1-2-2-1 - Exemple d'utilisation : La structure List

Implémenter le traitfmt::Display pour une structure où les éléments doivent être gérés séquentiellement est assez délicat. Le problème réside dans le fait que chaque appel de la macro write! génère une instance de fmt::Result. Une bonne gestion de ces appels nécessite de tester chaque résultat. Rust vous permet de gérer les erreurs de deux manières :

1. En utilisant la macro try! ;

2. En utilisant l'opérateur ? (qui est l'équivalent de try! mais intégré directement au langage).

La macro try! vient envelopper la fonction (ou la macro) cible comme ceci :

// On 'test' write! Pour voir si une erreur survient. S'il y a une erreur,
// elle sera renvoyée. Sinon, l'exécution continue.
try!(write!(f, "{}", value));

L'opérateur ?, bien qu'équivalent à la macro try!, vient se positionner devant l'appel de la fonction (ou macro).

write!(f, "{}", value)?;

Avec l'opérateur ?, l'implémentation du trait fmt::Display pour un Vec est simple et lisible.

1. use std::fmt; // On importe le module `fmt`.

4. // On déclare une structure nommée 'List' qui contient un 'Vec'.

5. struct List(Vec<i32>);

7. impl fmt::Display for List {

8. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

9. // On extrait le premier champ de la structure.

10. // Nous créons une référence de 'vec'.

11. let vec = &self.0;

13. write!(f, "[")?;

15. // On parcourt 'vec' en stockant chacun de ses éléments et

16. // le nombre d'itérations.

17. for (count, v) in vec.iter().enumerate() {

18. // Pour tout élément, excepté le premier, on ajoute une virgule.

19. // On utilise l'opérateur ?, ou la macro try!, pour renvoyer les erreurs.

20. if count != 0 { write!(f, ", ")?; }

21. write!(f, "{}", v)?;

22. }

24. // On ferme le crochet ouvert précédemment et on renvoie une instance

25. // de la structure `fmt::Result`.

26. write!(f, "]")

27. }

28. }

30. fn main() {

31. let v = List(vec![1, 2, 3]);

32. println!("{}", v);

33. }

Activité

Essayez de modifier le programme pour que l'index de chaque élément du vector soit également affiché durant l'exécution. Le résultat devrait ressembler à ceci :

[0: 1, 1: 2, 2: 3]

Voir aussi

La boucle for, le pattern ref, Result, les structures, try!, vec!.

1-2-3 - Formatage

Nous avons vu que le formatage désiré était spécifié par des « chaînes de formatage » :

- format!("{}", foo) -> "3735928559" ;

- format!("0x{:X}", foo) -> "0xDEADBEEF" ;

- format!("0o{:o}", foo) -> "0o33653337357".

La même variable (foo) peut être formatée de différentes manières suivant le type d'argument utilisé dans le marqueur (e.g. X, o, rien).

Cette fonctionnalité est implémentée à l'aide de traits, et il y en a un pour chaque type d'argument. Le plus commun est, bien entendu, Display. Il est chargé de gérer les cas où le type d'argument n'est pas spécifié (i.e. {}).

1. use std::fmt::{self, Formatter, Display};

3. struct City {

4. name: &'static str,

5. // Latitude

6. lat: f32,

7. // Longitude

8. lon: f32,

9. }

11. impl Display for City {

12. // `f` est un tampon, cette méthode écrit la chaîne de caractères

13. // formattée à l'intérieur de ce dernier.

14. fn fmt(&self, f: &mut Formatter) -> fmt::Result {

15. let lat_c = if self.lat >= 0.0 { 'N' } else { 'S' };

16. let lon_c = if self.lon >= 0.0 { 'E' } else { 'W' };

18. // `write!` est équivalente à `format!`, à l'exception qu'elle écrira

19. // la chaîne de caractères formatée dans un tampon (le premier argument).

20. write!(f, "{}: {:.3}°{} {:.3}°{}",

21. self.name, self.lat.abs(), lat_c, self.lon.abs(), lon_c)

22. }

23. }

25. #[derive(Debug)]

26. struct Color {

27. red: u8,

28. green: u8,

29. blue: u8,

30. }

32. fn main() {

33. for city in [

34. City { name: "Dublin", lat: 53.347778, lon: -6.259722 },

35. City { name: "Oslo", lat: 59.95, lon: 10.75 },

36. City { name: "Vancouver", lat: 49.25, lon: -123.1 },

37. ].iter() {

38. println!("{}", *city);

39. }

40. for color in [

41. Color { red: 128, green: 255, blue: 90 },

42. Color { red: 0, green: 3, blue: 254 },

43. Color { red: 0, green: 0, blue: 0 },

44. ].iter() {

45. // Utilisez le marqueur `{}` une fois que vous aurez implémenté

46. // le trait fmt::Display.

47. println!("{:?}", *color)

48. }

49. }

N'hésitez pas à consulter la liste complète des traits dédiés au formatage ainsi que leurs types d'argument dans la documentation du module std::fmt.

Activité

Implémentez le trait fmt::Display pour la structure Color dans l'exemple ci-dessus de manière à obtenir un résultat identique à celui-ci :

RGB (128, 255, 90) 0x80FF5A
RGB (0, 3, 254) 0x0003FE
RGB (0, 0, 0) 0x000000

Indices :

- Vous pourriez avoir besoin d'itérer plusieurs fois sur vos couleurs ;

- Vous pouvez créer une « compensation » (remplissant votre chaîne de zéros) d'une largeur n avec :0n.

Voir aussi

std::fmt

2 - Les primitifs

Le langage Rust offre une grande variété de primitifs. Liste non-exhaustive :

- Les entiers signés : i8, i16, i32, i64 et isize (dépend de l'architecture de la machine) ;

- Les entiers non-signés : u8, u16, u32, u64, usize (dépend de l'architecture de la machine) ;

- Les réels : f32, f64 ;

- Les caractères (Unicode) : 'a', 'α', '∞'. Codés sur 4 octets ;

- Les booléens : true ou false ;

- L'absence de type (), qui n'engendre qu'une seule valeur : () ;

- Les tableaux : [1, 2, 3] ;

- Les tuples : (1, true).

Le type des variables peut toujours être spécifié. Les nombres peuvent également être typés grâce à un suffixe, ou par défaut (laissant le compilateur les typer). Les entiers, par défaut, sont typés i32 tandis les réels sont typés f64.

1. fn main() {

2. // Le type des variables peut être spécifié, annoté.

3. let logical: bool = true;

5. let a_float: f64 = 1.0;  // typage classique

6. let an_integer   = 5i32; // Typage par suffixe

8. // Le type par défaut peut également être conservé.

9. // typage implicite

10. let default_float   = 3.0; // `f64`

11. let default_integer = 7;   // `i32`

13. let mut mutable = 12; // Entier signé codé sur 4 octets (i32).

15. // Erreur! Le type d'une variable ne peut être modifié en cours de route.

16. // mutable = true;

17. }

Voir aussi

La bibliothèque standard.

2-1 - Les littéraux et les opérateurs

Les entiers (1), les réels (1,2), les caractères ('a'), les chaînes de caractères ("abc"), les booléens (true) et l'absence de type () (un tuple vide) peuvent être représentés en utilisant les littéraux.

Les entiers peuvent également être exprimés sous différentes bases : hexadécimal, octal ou binaire en utilisant, respectivement, les préfixes : 0x, 0o ou 0b.

Des underscores peuvent être insérés à l'intérieur des littéraux numériques pour soigner la lisibilité (e.g. 1_000 est équivalent à 1000 et 0.000_001 est équivalent à 0.000001).

Nous devons renseigner le compilateur quant au type de littéral que nous utilisons. Pour le moment, nous allons utiliser le suffixe u32 pour indiquer que le littéral est un entier non-signé codé sur 32 bits et le suffixe i32 pour indiquer que c'est un entier signé codé sur 32 bits.

Les opérateurs et leur priorité dans le langage Rust peuvent être retrouvés dans les langages « C-like ».

1. fn main() {

2. // Addition d'entiers

3. println!("1 + 2 = {}", 1u32 + 2);

5. // Soustraction d'entiers

6. println!("1 - 2 = {}", 1i32 - 2);

7. // TODO ^ Essayez de changer `1i32` par `1u32` pour prendre conscience de l'importance du type

9. // Logique booléenne

10. println!("true AND false is {}", true && false);

11. println!("true OR false is {}", true || false);

12. println!("NOT true is {}", !true);

14. // Opérations bit-à-bit

15. println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);

16. println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);

17. println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);

18. println!("1 << 5 is {}", 1u32 << 5);

19. println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);

21. // Utilisez des underscores pour améliorer la lisibilité !

22. println!("One million is written as {}", 1_000_000u32);

23. }

2-2 - Les tuples

Un tuple est une collection de valeurs de différents types (ou pas). Les tuples peuvent être construits en utilisant les parenthèses () et chaque tuple est lui-même un type possédant sa propre signature (T1, T2, …), où T1, T2 sont les types de ses membres. Les fonctions peuvent se servir des tuples pour renvoyer plusieurs valeurs, puisque ces derniers peuvent être extensibles à volonté.

1. // Les tuples peuvent être utilisés comme arguments passés à une fonction

2. // et comme valeurs de renvoi.

3. fn reverse(pair: (i32, bool)) -> (bool, i32) {

4. // `let` peut être utilisé pour assigner, lier les membres d'un tuple à des

5. // variables.

6. let (integer, boolean) = pair;

8. (boolean, integer)

9. }

11. // La structure suivante est dédiée à l'activité.

12. #[derive(Debug)]

13. struct Matrix(f32, f32, f32, f32);

15. fn main() {

16. // Un tuple composé de différents types

17. let long_tuple = (1u8, 2u16, 3u32, 4u64,

18. -1i8, -2i16, -3i32, -4i64,

19. 0.1f32, 0.2f64,

20. 'a', true);

22. // Les valeurs peuvent être extraites depuis le tuple en utilisant son

23. // indexation.

24. println!("long tuple first value: {}", long_tuple.0);

25. println!("long tuple second value: {}", long_tuple.1);

27. // Les tuples peuvent être eux-même des membres d'un tuple.

28. let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16);

30. // Les tuples peuvent être affichés avec Debug.

31. println!("tuple of tuples: {:?}", tuple_of_tuples);

33. let pair = (1, true);

34. println!("pair is {:?}", pair);

36. println!("the reversed pair is {:?}", reverse(pair));

38. // Pour créer un élément de tuple, une virgule est requise pour différencier

39. // un élément de tuple d'un simple litéral entouré de parenthèses.

40. println!("one element tuple: {:?}", (5u32,));

41. println!("just an integer: {:?}", (5u32));

43. // Les tuples peuvent être "déstructurés" (i.e. décomposés pour créer de

44. // nouvelles assignations).

45. let tuple = (1, "hello", 4.5, true);

47. let (a, b, c, d) = tuple;

48. println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);

50. let matrix = Matrix(1.1, 1.2, 2.1, 2.2);

51. println!("{:?}", matrix)

53. }

Activité

1. Récapitulatif : Implémentez les services du trait fmt::Display pour la structure Matrix dans l'exemple ci-dessus. Donc si vous passez de l'affichage de débogage {:?} à l'affichage plus « user friendly » {}, vous devriez voir le résultat suivant :

( 1.1 1.2 )
( 2.1 2.2 )

Vous pouvez vous référer à l'exemple précédemment donné pour l'implémentation du trait Display.

1. Ajoutez une fonction transpose(), en vous appuyant sur l'implémentation de la fonction reverse(), qui accepte une matrice en paramètre et renvoie une matrice dans laquelle deux éléments ont été inversés. Exemple :

println!("Matrix:\n{}", matrix);
println!("Transpose:\n{}", transpose(matrix));

Affiche :

1. Matrix:

2. ( 1.1 1.2 )

3. ( 2.1 2.2 )

4. Transpose:

5. ( 1.1 2.1 )

6. ( 1.2 2.2 )

2-3 - Les tableaux et les "slices"

Un tableau est une collection d'objets appartenant au même type T, contenu dans un bloc mémoire défragmenté. Vous pouvez créer un tableau en utilisant les crochets [] et leur taille, connue à la compilation, fait partie intégrante de la signature du type [T; taille].

Note : Le terme « slice », en français, pourrait être traduit par « morceau », « tranche », g. Pour la suite du chapitre, nous utiliserons le terme « slice ».

Les slices sont similaires aux tableaux, à l'exception de leur taille qui n'est pas connue à la compilation. Une slice est un objet composé de deux « mots », le premier étant un pointeur vers la ressource initiale et le second étant la taille de la slice. La taille en mémoire de la slice est déterminée par l'architecture du processeur (e.g. 64 bits pour une architecture x86-64). Les slices peuvent être utilisées pour isoler une partie d'un tableau et héritent de la signature de ce dernier &[T].

1. use std::mem;

4. // Cette fonction emprunte une slice.

5. fn analyze_slice(slice: &[i32]) {

6. println!("first element of the slice: {}", slice[0]);

7. println!("the slice has {} elements", slice.len());

8. }

10. fn main() {

11. // Tableau dont la taille est connue à la compilation (le type peut être omis).

12. let xs: [i32; 5] = [1, 2, 3, 4, 5];

14. // Tous les éléments peuvent être initialisés à la même valeur.

15. let ys: [i32; 500] = [0; 500];

17. // L'indexation débute à 0.

18. println!("first element of the array: {}", xs[0]);

19. println!("second element of the array: {}", xs[1]);

21. // `len` renvoie la taille du tableau.

22. println!("array size: {}", xs.len());

24. // Les tableaux sont alloués dans la pile.

25. println!("array occupies {} bytes", mem::size_of_val(&xs));

27. // Les tableaux peuvent être automatiquement empruntés en tant que

28. // slice.

29. println!("borrow the whole array as a slice");

30. analyze_slice(&xs);

32. // Les slices peuvent pointer sur une partie bien précise d'un tableau.

33. println!("borrow a section of the array as a slice");

34. analyze_slice(&ys[1 .. 4]);

36. // Erreur! Le dépassement de tampon fait planter le programme.

37. // println!("{}", xs[5]);

38. }

3 - Les types personnalisés

En Rust, les types de données personnalisés sont principalement créés à partir de ces deux mots-clés :

1. struct : Définit une structure ;

2. enum : Définit une énumération.

Les constantes peuvent également être créées via les mots-clés const et static.

3-1 - Les structures

Il y a trois types de structures pouvant être créé en utilisant le mot-clé struct :

1. Les « tuple structs », aussi appelées simplement tuples ;

2. Les structures classiques issues du langage C ;

3. Les structures unitaires. Ne possèdant aucun champ, elles sont utiles pour la généricité.

1. // Une structure unitaire.

2. struct Nil;

4. // Un tuple.

5. struct Pair(i32, f32);

7. // Une structure avec deux champs.

8. struct Point {

9. x: f32,

10. y: f32,

11. }

13. // Les structures peuvent faire partie des champs d'une autre structure.

14. #[allow(dead_code)]

15. struct Rectangle {

16. p1: Point,

17. p2: Point,

18. }

20. fn main() {

21. // On instancie la structure `Point`.

22. let point: Point = Point { x: 0.3, y: 0.4 };

24. // On accède aux champs du point.

25. println!("point coordinates: ({}, {})", point.x, point.y);

27. // On décompose les champs de la structure pour les assigner

28. // à de nouvelles variables (i.e. my_x et my_y)

29. let Point { x: my_x, y: my_y } = point;

31. let _rectangle = Rectangle {

32. // L'instanciation de la structure est également une expression.

33. p1: Point { x: my_y, y: my_x },

34. p2: point,

35. };

37. // On instancie la structure unitaire, vide.

38. let _nil = Nil;

40. // On instancie un tuple.

41. let pair = Pair(1, 0.1);

43. // Accède aux champs du tuple.

44. println!("pair contains {:?} and {:?}", pair.0, pair.1);

46. // On décompose un tuple.

47. let Pair(integer, decimal) = pair;

49. println!("pair contains {:?} and {:?}", integer, decimal);

50. }

Activité

1. Ajoutez une fonction rect_area qui calcule l'air d'un rectangle (essayez d'utiliser la déstructuration) ;

2. Ajoutez une fonction square qui prend en paramètre une instance de la structure Point et un réel de type f32 puis renvoie une instance de la structure Rectangle contenant le point du coin inférieur gauche du rectangle ainsi qu'une largeur et une hauteur correspondant au réel passé en paramètre à la fonction square.

Voir aussi

Les attributs et la déstructuration.

3-2 - Les énumérations

Le mot-clé enum permet la création d'un type qui peut disposer d'une ou plusieurs variantes de lui-même. Toutes les variantes des structures sont valides dans une énumération.

1. // Masque les avertissements du compilateur lorsqu'il y a

2. // du code mort présent dans votre code.

3. #![allow(dead_code)]

5. // On créé une énumération pour définir des "classes" de personnes.

6. // Notez que chaque variante de l'énumération est indépendante de l'autre.

7. // Aucune n'est égale à l'autre: `Engineer != Scientist` et

8. // `Height(i32) != Weight(i32)`.

9. enum Person {

10. // Une variante peut être une structure unitaire,

11. Engineer,

12. Scientist,

13. // un tuple

14. Height(i32),

15. Weight(i32),

16. // ou simplement une structure classique.

17. Info { name: String, height: i32 }

18. }

20. // Prend une variante de l'énumération `Person` en argument et

21. // ne renvoie rien.

22. fn inspect(p: Person) {

23. // En utilisant une énumération, vous devez analyser tous les cas

24. // possibles (obligatoire).

25. // Le pattern matching permet de les couvrir efficacement.

26. match p {

27. Person::Engineer  => println!("Is an engineer!"),

28. Person::Scientist => println!("Is a scientist!"),

29. // On récupère l'attribut de l'instance `Height`.

30. Person::Height(i) => println!("Has a height of {}.", i),

31. Person::Weight(i) => println!("Has a weight of {}.", i),

32. // Destructure `Info` into `name` and `height`.

33. // On récupère les attributs

34. Person::Info { name, height } => {

35. println!("{} is {} tall!", name, height);

36. },

37. }

38. }

40. fn main() {

41. let person   = Person::Height(18);

42. let amira    = Person::Weight(10);

43. // La fonction `to_owned()` créé une instance de la structure `String`

44. // possédée par l'assignation `name` à partir d'une slice (i.e. &str).

45. let dave     = Person::Info { name: "Dave".to_owned(), height: 72 };

46. let rebecca  = Person::Scientist;

47. let rohan    = Person::Engineer;

49. inspect(person);

50. inspect(amira);

51. inspect(dave);

52. inspect(rebecca);

53. inspect(rohan);

54. }

Voir aussi

Les attributs, le mot-clé match, le mot-clé fn, les chaînes de caractères.

3-2-1 - Le mot-clé use

Grâce au mot-clé use, il n'est pas toujours obligatoire de spécifier le contexte d'une ressource à chaque utilisation.

1. // Masque les avertissements du compilateur concernant le code mort.

2. #![allow(dead_code)]

4. enum Status {

5. Rich,

6. Poor,

7. }

9. enum Work {

10. Civilian,

11. Soldier,

12. }

14. fn main() {

15. // Nous précisons que ces variantes de l'énumération sont utilisées, donc

16. // il n'est plus nécessaire de préciser leur conteneur.

17. use Status::{Poor, Rich};

18. // On utilise automatiquement toutes les variantes de l'enum `Work`.

19. use Work::*;

21. // Equivalent à `Status::Poor`.

22. let status = Poor;

23. // Equivalent à `Work::Civilian`.

24. let work = Civilian;

26. match status {

27. // Notez la disparition du conteneur lors de la recherche de pattern.

28. Rich => println!("The rich have lots of money!"),

29. Poor => println!("The poor have no money..."),

30. }

32. match work {

33. // Une fois encore, le conteneur a disparu.

34. Civilian => println!("Civilians work!"),

35. Soldier  => println!("Soldiers fight!"),

36. }

37. }

Voir aussi

Le mot-clé match et la déclaration use.

3-2-2 - Énumérations "C-like"

Les énumérations du langage Rust peuvent également adopter la même syntaxe que celles du langage C (possédant un identifiant explicite).

1. // Un attribut qui masque les avertissements du compilateur

2. // concernant le code mort.

3. #![allow(dead_code)]

5. // Énumération avec un identifiant implicite (partant de 0).

6. enum Number {

7. Zero, // 0

8. One, // 1

9. Two, // 2

10. }

12. // Énumération avec un identifiant explicite.

13. enum Color {

14. Red = 0xff0000,

15. Green = 0x00ff00,

16. Blue = 0x0000ff,

17. }

19. fn main() {

20. // Les variantes d'une énumération peuvent être converties en entiers.

21. println!("zero is {}", Number::Zero as i32);

22. println!("one is {}", Number::One as i32);

24. println!("roses are #{:06x}", Color::Red as i32);

25. println!("violets are #{:06x}", Color::Blue as i32);

26. }

Voir aussi

Le casting.

3-2-3 - Exemple d'utilisation : « linked-list »

Voici un exemple dans lequel une énumération peut être utilisée pour créer une liste de nœuds :

1. use List::*;

3. enum List {

4. // Cons: Un tuple contenant un élément(i.e. u32) et un pointeur vers le noeud suivant (i.e. Box<List>).

5. Cons(u32, Box<List>),

6. // Nil: Un noeud témoignant de la fin de la liste.

7. Nil,

8. }

10. // Il est possible de lier, d'implémenter des méthodes

11. // pour une énumération.

12. impl List {

13. // Créé une liste vide.

14. fn new() -> List {

15. // `Nil` est une variante de `List`.

16. Nil

17. }

19. // Consomme, s'approprie la liste et renvoie une copie de cette même liste

20. // avec un nouvel élément ajouté à la suite.

21. fn prepend(self, elem: u32) -> List {

22. // `Cons` est également une variante de `List`.

23. Cons(elem, Box::new(self))

24. }

26. // Renvoie la longueur de la liste.

27. fn len(&self) -> u32 {

28. // `self` doit être analysé car le comportement de cette méthode

29. // dépend du type de variante auquel appartient `self`.

30. // `self` est de type `&List` et `*self` est de type `List`, rendant

31. // possible l'analyse directe de la ressource plutôt que par le biais d'un alias (i.e. une référence).

32. // Pour faire simple: on déréférence `self` avant de l'analyser.

33. // Note: Lorsque vous travaillez sur des références, préférez le déréférencement

34. // avant analyse.

35. match *self {

36. // On ne peut pas prendre "l'ownership" de la queue (liste)

37. // puisque l'on emprunte seulement `self` (nous ne le possédons pas);

38. // Nous créerons simplement une référence de la queue.

39. Cons(_, ref tail) => 1 + tail.len(),

40. // De base, une liste vide possède 0 élément.

41. Nil => 0

42. }

43. }

45. // Renvoie une représentation de la liste sous une chaîne de caractères

46. // (wrapper)

47. fn stringify(&self) -> String {

48. match *self {

49. Cons(head, ref tail) => {

50. // `format!` est équivalente à `println!` mais elle renvoie

51. // une chaîne de caractères allouée dans le tas (wrapper)

52. // plutôt que de l'afficher dans la console.

53. format!("{}, {}", head, tail.stringify())

54. },

55. Nil => {

56. format!("Nil")

57. },

58. }

59. }

60. }

62. fn main() {

63. // Créé une liste vide.

64. let mut list = List::new();

66. // On ajoute quelques éléments.

67. list = list.prepend(1);

68. list = list.prepend(2);

69. list = list.prepend(3);

71. // Affiche l'état définitif de la liste.

72. println!("La linked list possède une longueur de: {}", list.len());

73. println!("{}", list.stringify());

74. }

Voir aussi

Box, les méthodes.

3-3 - Les constantes

Rust possède deux types de constantes qui peuvent être déclarées dans n'importe quel contexte global.

Chacun dispose d'un mot-clé :

- const : Une valeur immuable (état par défaut de toute variable) ;

- static : Une variable pouvant être accédée en lecture et (accessoirement) en écriture possédant la « lifetime » 'static.

Exception pour les "chaînes de caractères" littérales qui peuvent être directement assignées à une variable statique sans modification de votre part, car leur type &'static str dispose déjà de la lifetime 'static. Tous les autres types de référence doivent être explicitement annotés pour étendre leur durée de vie.

1. // Les variables globales sont déclarées en dehors de tous contextes.

2. static LANGUAGE: &'static str = "Rust";

3. const  THRESHOLD: i32 = 10;

5. fn is_big(n: i32) -> bool {

6. // Accès à la constante dans une fonction.

7. n > THRESHOLD

8. }

10. fn main() {

11. let n = 16;

13. // Accès à la constante dans le fil d'exécution principal.

14. println!("This is {}", LANGUAGE);

15. println!("The threshold is {}", THRESHOLD);

16. println!("{} is {}", n, if is_big(n) { "big" } else { "small" });

18. // Erreur! Vous ne pouvez pas modifier une constante.

19. // THRESHOLD = 5;

20. // FIXME ^ Commentez cette ligne pour voir disparaître

21. // le message d'erreur.

22. }

Voir aussi

La RFC des mot-clés const et static, la lifetime 'static.

4 - Les assignations

Rust assure l'immuabilité du type d'une variable grâce au typage statique. Lorsqu'une variable est déclarée elle peut être typée. Cependant, dans la plupart des cas, le compilateur sera capable d'inférer le type de la variable en se basant sur le contexte, atténuant sérieusement la lourdeur du typage.

Les valeurs (tels que les littéraux) peuvent être assignées à des variables en utilisant le mot-clé let.

1. fn main() {

2. let an_integer = 1u32;

3. let a_boolean = true;

4. let unit = ();

6. // Copie `an_integer` dans `copied_integer`.

7. let copied_integer = an_integer;

9. println!("An integer: {:?}", copied_integer); // un entier

10. println!("A boolean: {:?}", a_boolean); // un booléen

11. println!("Meet the unit value: {:?}", unit); // rien

13. // Le compilateur vous alertera lorsqu'il détecte une variable inutilisée;

14. // Vous pouvez faire taire ces avertissements en préfixant l'identificateur

15. // de la variable avec un underscore (i.e. _).

16. let _unused_variable = 3u32;

18. let noisy_unused_variable = 2u32;

19. // FIXME ^ Préfixez cet identificateur avec un underscore pour supprimer

20. // l'avertissement.

21. }

4-1 - Mutabilité

L'assignation à une variable est immuable par défaut mais ceci peut être changé en utilisant le modificateur mut.

1. fn main() {

2. let _immutable_binding = 1;

3. let mut mutable_binding = 1;

5. println!("Avant modification: {}", mutable_binding);

7. // Ok

8. mutable_binding += 1;

10. println!("Après modification: {}", mutable_binding);

12. // Erreur!

13. //_immutable_binding += 1;

14. // FIXME ^ Décommentez cette ligne pour voir le message d'erreur

15. }

4-2 - Scope et shadowing

Les assignations possèdent un contexte dans lequel elles persisteront et qui sera représenté par un « bloc ». Un bloc est une suite d'instructions et de déclarations englobées par des accolades {}. Le shadowing est également permis.

1. fn main() {

2. // Cette assignation vit dans la fonction `main`.

3. let long_lived_binding = 1;

5. // Ceci est un bloc, il possède un contexte plus petit que celui de la fonction

6. // `main`.

7. {

8. // Cette assignation existe seulement dans ce bloc.

9. let short_lived_binding = 2;

11. println!("inner short: {}", short_lived_binding);

13. // Cette assignation *masque* l'assignation du contexte supérieur (la fonction

14. // `main`).

15. let long_lived_binding = 5_f32;

17. println!("inner long: {}", long_lived_binding);

18. }

19. // Fin du bloc.

21. // Erreur! `short_lived_binding` n'existe pas dans ce contexte.

22. // println!("outer short: {}", short_lived_binding);

23. // FIXME ^ Décommentez cette ligne pour voir l'erreur.

25. println!("outer long: {}", long_lived_binding);

27. // Cette assignation *masque* également l'assignation précédente.

28. let long_lived_binding = 'a';

30. println!("outer long: {}", long_lived_binding);

31. }

4-3 - Déclaration seule

Il est possible de déclarer une variable dans un premier temps, pour l'initialiser dans un second temps. Cependant, cette forme est rarement utilisée puisqu'elle peut conduire à l'utilisation de variables qui ne sont pas initialisées (et donc à faire des erreurs).

1. fn main() {

2. // On déclare une variable.

3. let a_binding;

5. {

6. let x = 2;

8. // On initialise la variable.

9. a_binding = x * x;

10. }

12. println!("a binding: {}", a_binding);

14. let another_binding;

16. // Erreur! Utilisation d'une variable non-initialisée.

17. // println!("another binding: {}", another_binding);

18. // FIXME ^ Décommentez cette ligne pour voir l'erreur.

19. another_binding = 1;

21. println!("another binding: {}", another_binding);

22. }

Comme l'utilisation d'une variable, qui n'a pas été initialisée au préalable, peut mener à des comportements imprévisibles à l'exécution, le compilateur vous interdit de les utiliser.

5 - Le casting

Rust ne permet pas la conversion implicite des types primitifs (coercition). En revanche, une conversion explicite (casting) peut être entreprise à l'aide du mot-clé as.

Les règles régissant la conversion entre les types littéraux s'inspirent, principalement, des conventions du langage C à l'exception des cas où le C réserve des comportements imprévisibles.

1. // Supprime tous les avertissements relatifs aux dépassements

2. // de capacité (e.g. une variable de type u8 ne peut pas

3. // contenir plus qu'une variable de type u16).

4. #![allow(overflowing_literals)]

6. fn main() {

7. let decimal = 65.4321_f32;

9. // Erreur! La conversion implicite n'est pas supportée.

10. // let integer: u8 = decimal;

11. // FIXME ^ Décommentez/Commentez cette ligne pour voir

12. // le message d'erreur apparaître/disparaître.

14. // Conversion explicite.

15. let integer = decimal as u8;

16. let character = integer as char;

18. println!("Casting: {} -> {} -> {}", decimal, integer, character);

20. // Lorsque vous convertissez une valeur vers un type

21. // non-signé T, std::T::MAX + 1 est incrémenté ou soustrait jusqu'à

22. // ce que la valeur respecte la capacité du nouveau type.

24. // 1000 ne dépasse pas la capacité d'un entier non-signé codé sur 16 bits.

25. println!("1000 as a u16 is: {}", 1000 as u16);

27. // 1000 - 256 - 256 - 256 = 232

28. // En réalité, les 8 premiers bits les plus faibles (LSB) sont conservés et les

29. // bits les plus forts (MSB) restants sont tronqués.

30. println!("1000 as a u8 is : {}", 1000 as u8);

31. // -1 + 256 = 255

32. println!("  -1 as a u8 is : {}", (-1i8) as u8);

34. // Pour les nombres positifs, cette soustraction est équivalente à une

35. // division par 256.

36. println!("1000 mod 256 is : {}", 1000 % 256);

38. // Quand vous convertissez un type d'entiers signés, le résultat (bit à bit)

39. // est équivalent à celui de la conversion vers un type d'entiers non-signés.

40. // Si le bit de poids fort vaut 1, la valeur sera négative.

42. // Sauf si il n'y a pas de dépassements, évidemment.

43. println!(" 128 as a i16 is: {}", 128 as i16);

44. // 128 as u8 -> 128, complément à deux de 128 codé sur 8 bits:

45. println!(" 128 as a i8 is : {}", 128 as i8);

47. // On répète l'exemple ci-dessus.

48. // 1000 as u8 -> 232

49. println!("1000 as a i8 is : {}", 1000 as i8);

50. // et le complément à deux de 232 est -24.

51. println!(" 232 as a i8 is : {}", 232 as i8);

54. }

5-1 - Les littéraux

Les littéraux numériques peuvent être typés en suffixant le littéral avec son type. Par exemple, pour préciser que le littéral 42 devrait posséder le type i32, nous écrirons 42i32.

Le type des littéraux numériques qui ne sont pas suffixés va dépendre du contexte dans lequel ils sont utilisés. S'il n'y a aucune contrainte (i.e. si rien ne force la valeur à être codée sur un nombre de bits bien précis), le compilateur utilisera le type i32 pour les entiers et f64 pour les nombres réels.

1. fn main() {

2. // Ces littéraux sont suffixés, leurs types sont connus à l'initialisation.

3. let x = 1u8;

4. let y = 2u32;

5. let z = 3f32;

7. // Ces littéraux ne sont pas suffixés, leurs types dépendent du contexte.

8. let i = 1;

9. let f = 1.0;

11. // La fonction `size_of_val` renvoie la taille d'une variable en octets.

12. println!("La taille de `x` en octets: {}", std::mem::size_of_val(&x));

13. println!("La taille de `y` en octets: {}", std::mem::size_of_val(&y));

14. println!("La taille de `z` en octets: {}", std::mem::size_of_val(&z));

15. println!("La taille de `i` en octets: {}", std::mem::size_of_val(&i));

16. println!("La taille de `f` en octets: {}", std::mem::size_of_val(&f));

17. }

Certains concepts présentés dans l'exemple ci-dessus n'ont pas encore été abordés. Pour les plus impatients, voici une courte explication :

- fun(&foo) : Cette syntaxe représente le passage d'un paramètre par référence plutôt que par valeur (i.e. fun(foo)). Pour plus d'informations, voir le chapitre du système d'emprunts.

- std::mem::size_of_val est une fonction mais appelée avec son chemin absolu. Le code peut être divisé et organisé en plusieurs briques logiques nommées modules. Pour le cas de la fonction size_of_val, elle se trouve dans le module mem, lui-même se trouvant dans le paquet std. Pour plus d'informations voir les modules et/ou les « crates ».

5-2 - L'inférence des types

Le moteur dédié à l'inférence des types est très intelligent. Il fait bien plus que d'inférer le type d'une r-value à l'initialisation. Il se charge également d'analyser l'utilisation de la variable dans la suite du programme pour inférer son type définitif. Voici un exemple plus avancé dédié à l'inférence :

1. fn main() {

2. // Dû à l'annotation(suffixe), le compilateur sait que `elem` possède le type

3. // u8.

4. let elem = 5u8;

6. // Crée un vecteur vide (un tableau dont la taille n'est pas définie).

7. let mut vec = Vec::new();

8. // À ce niveau, le compilateur ne connaît pas encore le type exact de `vec`,

9. // il sait simplement que c'est un vecteur de quelque chose (`Vec<_>`).

11. // On ajoute `elem` dans le vecteur.

12. vec.push(elem);

13. // Tada! Maintenant le compilateur sait que `vec` est un vecteur

14. // d'entiers non-signés typés `u8` (`Vec<u8>`).

15. // TODO ^ Essayez de commenter la ligne où se trouve `vec.push(elem)`.

17. println!("{:?}", vec);

18. }

Aucun typage explicite n'était nécessaire, le compilateur est heureux et le programmeur aussi !

5-3 - Les alias

Le mot-clé type peut être utilisé pour donner un nouveau nom à un type existant. Les types doivent respecter la convention de nommage CamelCase ou le compilateur vous renverra un avertissement. L'exception à cette règle sont les types primitifs : usize, f32, etc.

1. // `NanoSecond` est le nouveau nom de `u64`.

2. type NanoSecond = u64;

3. type Inch = u64;

5. // Utilisons un attribut pour faire taire les

6. // avertissements.

7. #[allow(non_camel_case_types)]

8. type u64_t = u64;

9. // TODO ^ Essayez de supprimer l'attribut.

11. fn main() {

12. // `NanoSecond` = `Inch` = `u64_t` = `u64`.

13. let nanoseconds: NanoSecond = 5 as u64_t;

14. let inches: Inch = 2 as u64_t;

16. // Notez que les alias de types ne fournissent aucune sécurité supplémentaire,

17. // car ce ne sont pas de nouveaux types (i.e. vous pouvez très bien changer

18. // Inch par NanoSecond, vous n'aurez aucune erreur).

19. println!("{} nanoseconds + {} inches = {} unit?",

20. nanoseconds,

21. inches,

22. nanoseconds + inches);

23. }

Voir aussi

Les attributs.

6 - Les expressions

Un programme écrit en Rust est (principalement) composé d'une série de déclarations :

1. fn main() {

2. // déclaration

3. // déclaration

4. // déclaration

5. }

Il y a plusieurs sortes de déclarations en Rust. Les deux plus communes sont les assignations et les expressions suivies par un point-virgule « ; » :

1. fn main() {

2. // assignation

3. let x = 5;

5. // expression;

6. x;

7. x + 1;

8. 15;

9. }

Les blocs sont également des expressions, donc ils peuvent être utilisés comme r-value dans les assignations. La dernière expression dans le bloc sera assignée à la l-value. Notez toutefois que si la dernière expression du bloc se termine par un point-virgule « ; », la valeur de renvoi sera ().

1. fn main() {

2. let x = 5u32;

4. let y = {

5. let x_squared = x * x;

6. let x_cube = x_squared * x;

8. // Cette expression sera assignée à `y`.

9. x_cube + x_squared + x

10. };

12. let z = {

13. // Le point-virgule supprime cette expression et `()` est assigné

14. // à `z`.

15. 2 * x;

16. };

18. println!("x is {:?}", x);

19. println!("y is {:?}", y);

20. println!("z is {:?}", z);

21. }

7 - Contrôle du flux

La caractéristique commune à tous langages est la capacité à contrôler le flux : if/else, for, etc., et Rust ne fait pas exception. Allons voir ça !

7-1 - If/else

Les branchements conditionnels tels que if ou else sont similaires à d'autres langages. Contrairement à beaucoup d'entre-eux, la condition booléenne peut toutefois ne pas être enveloppée de parenthèses et chaque condition est suivie d'un bloc. Les conditions if/else sont des expressions et toutes les branches doivent renvoyer le même type.

1. fn main() {

2. let n = 5;

4. if n < 0 {

5. print!("{} est négatif.", n);

6. } else if n > 0 {

7. print!("{} est positif.", n);

8. } else {

9. print!("{} est nul.", n);

10. }

12. let big_n =

13. if n < 10 && n > -10 {

14. println!(" et est un petit nombre, multiplions-le par dix");

16. // Cette expression renvoie un entier de type `i32`.

17. 10 * n

18. } else {

19. println!(" est un grand nombre, divisons-le par deux");

21. // Cette expression doit également renvoyer un entier de type `i32`.

22. n / 2

23. // TODO ^ Essayez de supprimer cette expression en ajoutant un point-virgule.

24. };

25. //   ^ Ne pas oubliez de mettre un point-virgule ici! Toutes les

26. // assignations (`let`) doivent se terminer par un point-virgule.

28. println!("{} -> {}", n, big_n);

29. }

7-2 - Le mot-clé loop

Rust fournit le mot-clé loop pour créer une boucle infinie.

Le mot-clé break peut être utilisé pour sortir de la boucle n'importe où tandis que le mot-clé continue peut être utilisé pour ignorer le reste de l'itération en cours et en débuter une nouvelle.

1. fn main() {

2. let mut count = 0u32;

4. println!("Comptons jusqu'à l'infini!");

6. // Boucle infinie.

7. loop {

8. count += 1;

10. if count == 3 {

11. println!("trois");

13. // Ignore le reste de l'itération.

14. continue;

15. }

17. println!("{}", count);

19. if count == 5 {

20. println!("Ok, ça suffit!");

22. // Sort de la boucle.

23. break;

24. }

25. }

26. }

7-2-1 - L'imbrication et les labels

Il est possible de sortir (i.e. break) ou de relancer (i.e. continue) l'itération d'une boucle à partir d'une autre boucle interne à cette dernière. Pour ce faire, les boucles concernées doivent être annotées avec un 'label et il devra être passé aux instructions break et/ou continue.

1. #![allow(unreachable_code)] // permet de faire taire les avertissements

2. // relatifs au code mort.

4. fn main() {

5. 'externe: loop {

6. println!("Entré dans la boucle annotée 'externe.");

8. 'interne: loop {

9. println!("Entré dans la boucle annotée 'interne.");

11. // Cette instruction nous ferait simplement

12. // sortir de la boucle 'interne.

13. // break;

15. // On sort de la boucle 'externe

16. // à partir de la boucle 'interne.

17. break 'externe;

18. }

20. println!("Cette ligne ne sera jamais exécutée.");

21. }

23. println!("Sorti de la boucle annotée 'externe.");

24. }

7-3 - La boucle while

Le mot-clé while peut être utilisé pour itérer jusqu'à ce qu'une condition soit remplie.

Écrivons les règles de l'infâme FizzBuzz en utilisant une boucle while :

1. fn main() {

2. // Un compteur.

3. let mut n = 1;

5. // Itère sur `n` tant que sa valeur est strictement inférieure

6. // à 101.

7. while n < 101 {

8. if n % 15 == 0 {

9. println!("fizzbuzz");

10. } else if n % 3 == 0 {

11. println!("fizz");

12. } else if n % 5 == 0 {

13. println!("buzz");

14. } else {

15. println!("{}", n);

16. }

18. // Incrémente le compteur.

19. n += 1;

20. }

21. }

7-4 - La boucle for et les invervalles

L'ensemble for in peut être utilisé pour itérer à l'aide d'une instance Iterator. L'une des manières les plus simples pour créer un itérateur est d'utiliser la notation d'intervalle (« range notation ») a..b. Soit un intervalle [a;b[ (comprend toutes les valeurs entre a (inclut) et b (exclut)).

Écrivons les règles de FizzBuzz en utilisant la boucle for au lieu de while.

1. fn main() {

2. // `n` prendra pour valeur: 1, 2, ..., 100  au fil des itérations.

3. for n in 1..101 {

4. if n % 15 == 0 {

5. println!("fizzbuzz");

6. } else if n % 3 == 0 {

7. println!("fizz");

8. } else if n % 5 == 0 {

9. println!("buzz");

10. } else {

11. println!("{}", n);

12. }

13. }

14. }

Voir aussi

Les itérateurs.

7-5 - Le pattern matching

Rust fournit le pattern matching via le mot-clé match, lequel peut être utilisé comme le mot-clé switch avec le langage C.

1. fn main() {

2. let number = 13;

3. // TODO ^ Assignez différentes valeurs à `number`.

5. println!("Nature de number {}", number);

6. match number {

7. // Teste une seule valeur.

8. 1 => println!("Un!"),

9. // Teste plusieurs valeurs.

10. 2 | 3 | 5 | 7 | 11 => println!("C'est un nombre premier."),

11. // Teste l'intervalle [13;19].

12. 13...19 => println!("A teen"),

13. // Couvre tous les autres cas.

14. _ => println!("Ain't special"),

15. }

17. let boolean = true;

18. // L'analyse est également une expression.

19. let binary = match boolean {

20. // Les branches du match doivent couvrir tous les cas possibles.

21. false => 0,

22. true => 1,

23. // TODO ^ Essayez de commenter l'une de ses branches.

24. };

26. println!("{} -> {}", boolean, binary);

27. }

7-5-1 - La déstructuration

Un bloc match peut décomposer des objets de différentes manières.

7-5-1-1 - Les tuples

Les tuples peuvent être déstructurés (entendez « décortiqués », « décomposés ») dans un bloc match comme suit :

1. fn main() {

2. let pair = (0, -2);

3. // TODO ^ Essayez de modifier les valeurs contenues par

4. // le tuple.

6. println!("Dites m'en plus à propos de {:?}", pair);

7. // match peut être utilisé pour déstructurer un tuple.

8. match pair {

9. // Déstructure `y`.

10. (0, y) => println!("Le premier élément est égal à `0`

11. et `y` égal à `{:?}`", y),

12. (x, 0) => println!("`x` est égal à `{:?}` et le dernier est égal à `0`", x),

13. _      => println!("Peu importe ce qu'ils sont."),

14. // L'underscore `_` siginifie que vous ne souhaitez pas

15. // assigner de valeurs à une variable, que vous souhaitez couvrir tous les

16. // autres cas.

17. }

18. }

Voir aussi

Les tuples.

7-5-1-2 - Les énumérations

Une énumération est déstructurée de la même manière :

1. // On fait taire les avertissements (puisqu'on utilise

2. // qu'une seule variante).

3. #[allow(dead_code)]

4. enum Color {

5. // Identification implicite.

6. Red,

7. Blue,

8. Green,

9. // Ces variantes assignent plusieurs tuples sous différents noms: les modèles

10. // de couleur.

11. RGB(u32, u32, u32),

12. HSV(u32, u32, u32),

13. HSL(u32, u32, u32),

14. CMY(u32, u32, u32),

15. CMYK(u32, u32, u32, u32),

16. }

18. fn main() {

19. let color = Color::RGB(122, 17, 40);

20. // TODO ^ Essayez de modifier les valeurs du tuple.

21. println!("De quelle couleur s'agit-il?");

22. // Une énumération peut être déstructurée en utilisant le pattern matching.

23. match color {

24. Color::Red   => println!("La couleur rouge!"),

25. Color::Blue  => println!("La couleur bleu!"),

26. Color::Green => println!("La couleur vert!"),

27. Color::RGB(r, g, b) =>

28. println!("Rouge: {}, Vert: {}, et Bleu: {}!", r, g, b),

29. Color::HSV(h, s, v) =>

30. println!("Teinte: {}, Saturation: {}, Valeur: {}!", h, s, v),

31. Color::HSL(h, s, l) =>

32. println!("Teinte: {}, Saturation: {}, Lumière: {}!", h, s, l),

33. Color::CMY(c, m, y) =>

34. println!("Cyan: {}, Magenta: {}, Jaune: {}!", c, m, y),

35. Color::CMYK(c, m, y, k) =>

36. println!("Cyan: {}, Magenta: {}, Jaune: {}, Noir: {}!",

37. c, m, y, k),

38. // Inutile d'ajouter une branche "par défaut" car tous les

39. // cas ont été couverts.

40. }

41. }

Voir aussi

L'attribut allow(...), les modèles de couleur FR ou EN et les énumérations.

7-5-1-3 - Les pointeurs et références

À propos des pointeurs, la distinction doit être faite entre la déstructuration et le déréférencement puisque ce sont deux concepts différents utilisés différemment par rapport au langage C.

- Le déréférencement utilise * ;

- La déstructuration utilise &, ref, et ref mut.

1. fn main() {

2. // Assigne une référence de type `i32`. Le `&` signifie qu'une

3. // référence est assignée.

4. // L'équivalent non-raccourci de cette assignation pourrait ressembler à ceci:

5. // ```rust

6. // let _reference: i32 = 4;

7. // let reference: &i32 = &_reference;

8. // ```

9. let reference: &i32 = &4;

11. match reference {

12. // Lorsque `reference` est comparé à `&val`, la comparaison ressemble à ceci:

13. // `&i32`

14. // `&val` <- `val` est plus ou moins une représentation de `reference`.

15. // ^ Nous remarquons que si le `&` est omis, la valeur devrait être

16. // assignée à `val`.

17. &val => println!("On récupère une valeur via déstructuration: {:?}", val),

18. }

20. // Pour éviter d'utiliser la référence, vous pouvez déréférencer `reference`

21. // avant analyse (vous permettant d'opérer sur la valeur, si elle est mutable).

22. match *reference {

23. val => println!("On récupère la valeur déréférencée: {:?}", val),

24. }

26. // Que se passe-t-il si vous ne créez pas une référence ? `reference`

27. // était une référence parce que la r-value était une référence. Cette

28. // variable n'en est pas une parce que la r-value n'en est pas une.

29. let _not_a_reference = 3;

31. // Rust fournit le mot-clé `ref` dans ce but. Il modifie l'assignation

32. // de manière à créer une référence pour l'élément; cette référence est assignée.

33. let ref _is_a_reference = 3;

35. // Bien entendu, en assignant deux valeurs sans références, ces dernières

36. // peuvent être récupérées à l'aide du mot-clé `ref` et `ref mut`.

37. let value = 5;

38. let mut mut_value = 6;

40. // On utilise le mot-clé `ref` pour créer une référence.

41. match value {

42. ref r => println!("On récupère une référence de la valeur: {:?}", r),

43. }

45. // `ref mut` s'utilise de la même manière.

46. match mut_value {

47. ref mut m => {

48. // On obtient une référence. Nous allons déréférencer `m` avant

49. // de pouvoir opérer.

50. *m += 10;

51. println!("Nous incrémentons de 10. `mut_value`: {:?}", m);

52. },

53. }

54. }

7-5-1-4 - Les structures

Une structure peut également être déstructurée comme suit :

1. fn main() {

2. struct Foo { x: (u32, u32), y: u32 }

4. // Déstructure les membres de la structure.

5. let foo = Foo { x: (1, 2), y: 3 };

6. let Foo { x: (a, b), y } = foo;

8. println!("a = {}, b = {},  y = {} ", a, b, y);

10. // Vous pouvez déstructurer les structures et renommer

11. // leurs variables. L'ordre n'est pas important.

13. let Foo { y: i, x: j } = foo;

14. println!("i = {:?}, j = {:?}", i, j);

16. // et vous pouvez aussi ignorer certaines variables:

17. let Foo { y, .. } = foo;

18. println!("y = {}", y);

20. // Ceci donne une erreur: le pattern ne mentionne pas le champ `x`.

21. // let Foo { y } = foo;

22. }

Voir aussi

Les structures, ref.

7-5-2 - Les gardes

Lorsque vous usez du pattern matching, un « garde » peut être ajouté dans chaque branche du match.

1. fn main() {

2. let pair = (2, -2);

3. // TODO ^ Essayez de modifier les valeurs de `pair`.

5. println!("Dites m'en plus à propos de: {:?}", pair);

6. match pair {

7. (x, y) if x == y => println!("Ils sont jumeaux!"),

8. // La ^ condition if est un garde.

9. (x, y) if x + y == 0 => println!("De l'antimatière, boom!"),

10. (x, _) if x % 2 == 1 => println!("Le premier est étrange..."),

11. _ => println!("Rien de spécial..."),

12. }

13. }

Voir aussi

Les tuples.

7-5-3 - Assignation

Accéder indirectement à une variable rend impossible sa réutilisation sans la réassigner. match fournit le symbole @ pour assigner des valeurs à des identificateurs :

1. // Une fonction `age` qui renvoie un entier de type `u32`.

2. fn age() -> u32 {

3. 15

4. }

6. fn main() {

7. println!("Dis-moi quel type de personne es-tu.");

9. match age() {

10. 0             => println!("Je ne suis pas encore né, je suppose."),

11. // Nous aurions pu `match` 1 ... 12 directement mais il n'aurait pas

12. // été possible de connaître l'âge de l'enfant

13. // À la place, nous assignons la valeur à `n`

14. // pour la séquence de 1 ... 12. L'âge peut désormais être affiché.

15. n @ 1  ... 12 => println!("Je suis un enfant de {:?} ans!", n),

16. n @ 13 ... 19 => println!("Je suis un adolescent de {:?} ans!", n),

17. // Pas de limite. On renvoie le résultat.

18. n             => println!("Je suis un adulte de {:?} ans!", n),

19. }

20. }

Voir aussi

Les fonctions.

7-6 - if let

Pour certains cas, match peut être « lourd ». Par exemple :

1. // Crée une valeur optionnelle de type `Option<i32>`.

2. let optional = Some(7);

4. match optional {

5. Some(i) => {

6. println!("Ceci est une très longue chaîne de caractères contenant un

7. `{:?}`", i);

8. // ^ Deux niveaux d'indentations sont nécessaires alors

9. // que nous aurions pu simplement déstructurer `i`.

10. },

11. _ => {},

12. // ^ Nécessaire parce que `match` est exhaustif. Cette branche vous

13. // paraît-elle utile?

14. };

if let est plus adapté à ce genre de cas et permet la création de plusieurs branches en cas d'erreur :

1. fn main() {

2. // Toutes les variables sont de type `Option<i32>`.

3. let number = Some(7);

4. let letter: Option<i32> = None;

5. let emoticon: Option<i32> = None;

7. // L'ensemble `if let` se déroule de cette manière:

8. // `if let` déstructure `number` et assigne sa valeur à `i` et exécute

9. // le bloc (`{}`).

10. if let Some(i) = number {

11. println!("{:?} a été trouvé!", i);

12. }

14. // Si vous devez spécifier un cas d'erreur, utilisez un `else`:

15. if let Some(i) = letter {

16. println!("{:?} a été trouvé!", i);

17. } else {

18. // Déstructuration ratée. On exécute le `else`.

19. println!("Aucun nombre n'a été trouvé.

20. Cherchons une lettre!");

21. };

23. // Fournit une condition alternative.

24. let i_like_letters = false;

26. if let Some(i) = emoticon {

27. println!("{:?} a été trouvé!", i);

28. // Déstructuration ratée. Passe à une condition `else if` pour tester si

29. // la condition alternative est vraie.

30. } else if i_like_letters {

31. println!("Aucun nombre n'a été trouvé.

32. Cherchons une lettre!");

33. } else {

34. // La condition évaluée est fausse. Branche par défaut:

35. println!("Je n'aime pas les lettres. Cherchons une emoticône :)!");

36. };

37. }

Voir aussi

Les énumérations, l'énumération Option et la RFC de if let.

7-7 - while let

Ayant un fonctionnement similaire à if let, while let peut alléger la syntaxe de match lorsqu'il n'est pas nécessaire de passer par le pattern matching. Voici une séquence qui incrémente i :

1. // Crée une valeur optionnelle de type `Option<i32>`.

2. let mut optional = Some(0);

4. // On répète le test.

5. loop {

6. match optional {

7. // Si il est possible de déstructurer `optional`,

8. // le bloc sera exécuté.

9. Some(i) => {

10. if i > 9 {

11. println!("Plus grand que 9, on quitte!");

12. optional = None;

13. } else {

14. println!("`i` est égal à `{:?}`. On réitère.", i);

15. optional = Some(i + 1);

16. }

17. // ^ Nécessite trois niveaux d'indentations.

18. },

19. // On quitte la boucle si la déstructuration

20. // a échoué:

21. _ => { break; }

22. // ^ Pourquoi cette instruction devrait être nécessaire ?

23. // Il doit y avoir une solution plus adaptée!

24. }

25. }

En utilisant while let, cela rend la séquence plus lisible :

1. fn main() {

2. // Crée une valeur optionnelle de type `Option<i32>`.

3. let mut optional = Some(0);

5. // Fonctionnement: "`while let` déstructure `optional` pour assigner sa valeur

6. // à Some(i) puis exécute le bloc (`{}`). Sinon, on sort de la boucle."

7. while let Some(i) = optional {

8. if i > 9 {

9. println!("Plus grand que 9, on quitte!");

10. optional = None;

11. } else {

12. println!("`i` est égal à `{:?}`. On réitère.", i);

13. optional = Some(i + 1);

14. }

15. // Moins explicite, il n'est plus nécessaire de gérer

16. // le cas où la déstructuration échoue.

17. }

18. // ^ `if let` permet d'ajouter des branches `else`/`else if`

19. // optionnelles. `while let` ne le permet pas, en revanche.

20. }

Voir aussi

Les énumérations, l'énumération Option et la RFC de while let.

8 - Les fonctions

Les fonctions sont déclarées à l'aide du mot-clé fn. Leurs arguments sont typés, tout comme les variables, et, si la fonction renvoie une valeur, le type renvoyé doit être spécifié à la suite d'une flèche ->.

La dernière expression se trouvant dans le corps de la fonction sera utilisée pour inférer le type de renvoi. Il est également possible d'utiliser l'instruction return pour effectuer un renvoi prématuré dans la fonction (peut être utilisé dans les boucles et les structures conditionnelles).

Réécrivons les règles de FizzBuzz en utilisant les fonctions !

1. // Contrairement à C ou C++, l'ordre de déclaration des fonctions

2. // n'est pas important.

3. fn main() {

4. // Nous pouvons appeler cette fonction ici et l'implémenter ailleurs.

5. fizzbuzz_to(100);

6. }

8. // Une fonction qui renvoie une valeur booléenne.

9. fn is_divisible_by(lhs: u32, rhs: u32) -> bool {

10. // Comportement imprévisible, renvoi prématuré.

11. if rhs == 0 {

12. return false;

13. }

15. // C'est une expression, le mot-clé `return` n'est pas nécessaire ici.

16. lhs % rhs == 0

17. }

18. // Les fonctions qui n'ont pas de type de renvoi défini renvoient par défaut

19. // un tuple vide `()`.

20. fn fizzbuzz(n: u32) -> () {

21. if is_divisible_by(n, 15) {

22. println!("fizzbuzz");

23. } else if is_divisible_by(n, 3) {

24. println!("fizz");

25. } else if is_divisible_by(n, 5) {

26. println!("buzz");

27. } else {

28. println!("{}", n);

29. }

30. }

32. // Quand une fonction ne possède pas de type de renvoi, le tuple vide `()`

33. // peut être omis.

34. fn fizzbuzz_to(n: u32) /* type de renvoi omis */ {

35. for n in 1..n + 1 {

36. fizzbuzz(n);

37. }

38. }

8-1 - Les méthodes

Les méthodes sont des fonctions rattachées à des structures et objets. Ces méthodes ont un accès aux données de l'objet ainsi qu'à ses autres méthodes par le biais du mot-clé self. Les méthodes sont déclarées dans un bloc impl.

1. struct Point {

2. x: f64,

3. y: f64,

4. }

6. // Toutes les méthodes de la structure `Point` sont implémentées ici.

7. impl Point {

8. // Ceci est une méthode statique.

9. // Les méthodes statiques ne sont pas dépendantes des instances.

10. // Il est courant d'utiliser les méthodes statiques comme constructeurs.

11. fn origin() -> Point {

12. Point { x: 0.0, y: 0.0 }

13. }

15. // Une autre méthode statique possédant

16. // deux paramètres.

17. fn new(x: f64, y: f64) -> Point {

18. Point { x: x, y: y }

19. }

20. }

22. struct Rectangle {

23. p1: Point,

24. p2: Point,

25. }

27. impl Rectangle {

28. // Ceci est une méthode dépendante d'une instance (méthode d'instance).

29. // `&self` est le sucre syntaxique de `self: &Self` où `Self` est le

30. // type de l'objet qui appelle les méthodes. En l'occurrence

31. // `Self` = `Rectangle`.

32. fn area(&self) -> f64 {

33. // `self` donne accès aux champs de la structure via la notation pointée.

34. let Point { x: x1, y: y1 } = self.p1;

35. let Point { x: x2, y: y2 } = self.p2;

37. // `abs` est une méthode renvoyant un réel de type f64 qui représente

38. // la valeur absolue du primitif.

39. ((x1 - x2) * (y1 - y2)).abs()

40. }

42. fn perimeter(&self) -> f64 {

43. let Point { x: x1, y: y1 } = self.p1;

44. let Point { x: x2, y: y2 } = self.p2;

46. 2.0 * ((x1 - x2).abs() + (y1 - y2).abs())

47. }

49. // Cette méthode a besoin d'opérer sur une référence mutable

50. // de l'instance courante `&mut self`.

51. // La syntaxe non-raccourcie est `self: &mut Self`.

52. fn translate(&mut self, x: f64, y: f64) {

53. self.p1.x += x;

54. self.p2.x += x;

56. self.p1.y += y;

57. self.p2.y += y;

58. }

59. }

61. // `Pair` possède deux entiers alloués dans le tas.

62. struct Pair(Box<i32>, Box<i32>);

64. impl Pair {

65. // Cette méthode "consomme" les ressources de l'instance courante.

66. // `self` est un sucre syntaxique de `self: Self`.

67. fn destroy(self) {

68. // Déstructure `self` (i.e. récupère les champs désirés).

69. let Pair(first, second) = self;

71. println!("Destroying Pair({}, {})", first, second);

73. // La mémoire occupée par `first` et `second` sera libérée

74. // une fois que l'exécution de la méthode prendra fin.

75. }

76. }

78. fn main() {

79. let rectangle = Rectangle {

80. // Les méthodes statiques sont appelées par le biais

81. // d'une paire de deux points `::`.

82. p1: Point::origin(),

83. p2: Point::new(3.0, 4.0),

84. };

86. // Vous devez, en revanche, utiliser la notation pointée pour appeler

87. // les méthodes d'instance.

88. // Notez que le premier argument passé (implicitement) est `&self`

89. // (i.e. `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)`).

90. println!("Rectangle perimeter: {}", rectangle.perimeter());

91. println!("Rectangle area: {}", rectangle.area());

93. let mut square = Rectangle {

94. p1: Point::origin(),

95. p2: Point::new(1.0, 1.0),

96. };

98. // Erreur! `rectangle` est immuable alors que cette méthode

99. // nécessite une référence mutable de l'objet.

100. // rectangle.translate(1.0, 0.0);

101. // TODO ^ Essayez de décommenter cette ligne.

103. // C'est bon! Les objets mutables peuvent appeler les méthodes

104. // "mutables".

105. square.translate(1.0, 1.0);

107. let pair = Pair(Box::new(1), Box::new(2));

109. pair.destroy();

111. // Erreur! L'appel de la méthode `destroy` a consommé

112. // l'instance `pair`.

113. // pair.destroy();

114. // TODO ^ Essayez de décommenter cette ligne.

115. }

Définition : valeur absolue

8-2 - Les closures

Les closures en Rust, également appelées « lambdas », sont des fonctions qui peuvent capturer l'environnement qui les entoure. Par exemple, voici une closure qui capture la variable x :

|val| val + x

La syntaxe ainsi que les capacités des closures les rendent adéquates aux déclarations et utilisations à la volée. Appeler une closure se fait de la même manière qu'une fonction classique. En revanche, les types reçus en entrée (i.e. les types des paramètres passés) et le type de renvoi peuvent être inférés et les identificateurs des paramètres doivent être spécifiés.

D'autres caractéristiques spécifiques aux closures :

- L'utilisation du couple || plutôt que de () pour entourer les paramètres ;

- La délimitation {} du corps de la closure optionnelle pour une seule expression (sinon obligatoire) ;

- La capacité à capturer des variables appartenant au contexte dans lequel la closure est imbriquée.

1. fn main() {

2. // Incrémentation avec les closures et fonctions.

3. fn  function            (i: i32) -> i32 { i + 1 }

5. // Les closures sont anonymes, ici nous assignons leurs références.

6. // Les closures sont typées de la même manière qu'une fonction classique

7. // mais le typage est optionnel. Chaque version (raccourcie et non-raccourcie)

8. // est assignée à un identificateur approprié.

9. let closure_annotated = |i: i32| -> i32 { i + 1 };

10. let closure_inferred  = |i     |          i + 1  ;

12. let i = 1;

13. // Appelle la fonction et les closures.

14. println!("function: {}", function(i));

15. println!("closure_annotated: {}", closure_annotated(i));

16. println!("closure_inferred: {}", closure_inferred(i));

18. // Une closure qui ne prend aucun argument et renvoie un

19. // entier de type `i32`.

20. // Le type de renvoi est inféré.

21. let one = || 1;

22. println!("closure returning one: {}", one());

24. }

8-2-1 - Capture

Les closures sont naturellement flexibles et feront leur possible pour fonctionner sans typage explicite. Ceci permet à la capture de s'adapter au contexte : parfois en prenant possession des ressources, parfois seulement en les empruntant. Les closures peuvent capturer les variables :

- Par référence : &T ;

- Par référence mutable : &mut T ;

- Par valeur T.

Par défaut, elles privilégient la capture par référence s'il n'est pas nécessaire de prendre possession des ressources.

1. fn main() {

2. use std::mem;

4. let color = "green";

6. // Une closure destinée à afficher la variable `color` qui emprunte

7. // (`&`) `color` et stocke l'emprunt ainsi que la closure

8. // dans la variable `print`. Elle (`color`) restera "empruntée"

9. // jusqu'à ce que l'exécution de `print` prend fin.

10. // La macro println! ne fait qu'emprunter les ressources, cela

11. // n'impose pas de contraintes supplémentaires pour la closure.

12. let print = || println!("`color`: {}", color);

14. // On appelle la closure en empruntant `color`.

15. print();

16. print();

18. let mut count = 0;

20. // Une closure qui incrémente la variable `count`. Cette dernière

21. // pourrait être exploitée par référence `&mut count` ou valeur

22. // `count`, mais puisque `&mut count` est moins restrictif la capture

23. // par référence mutable sera choisie.

24. //

25. // La variable `inc` est annotée comme `mut` car une référence mutable

26. // `&mut` est stockée à l'intérieur.

27. // Appeler la closure(du moins, dans ce contexte) modifie

28. // son propre état et donc requiert un `mut`.

30. let mut inc = || {

31. count += 1;

32. println!("`count`: {}", count);

33. };

35. // Appelle la closure.

36. inc();

37. inc();

39. // let reborrow = &mut count;

40. // ^ TODO: Essayez de décommenter cette ligne.

42. // Un entier non-copiable.

43. let movable = Box::new(3);

45. // `mem::drop` prend possession de ses paramètres.

46. // Un type pouvant être copié devrait être copié dans la closure,

47. // laissant la ressource originale intacte. Un type qui ne peut pas

48. // être copié doit être déplacé et donc `movable` appartiendra à la closure.

49. let consume = || {

50. println!("`movable`: {:?}", movable);

51. mem::drop(movable);

52. };

54. // `consume` prend possession de la variable et ne peut donc être appelée qu'une seule fois.

55. consume();

56. // consume();

57. // ^ TODO: Essayez de décommenter ce second appel.

58. }

Voir aussi

Box et std::mem::drop.

8-2-2 - Les closures passées en paramètres

Alors que Rust se charge de choisir, pour les closures, la manière de capturer les variables sans forcer le typage, lorsque c'est possible, cette ambiguïté n'est pas permise au sein des fonctions. Lorsqu'une closure est passée en paramètre à une fonction, le type de ses paramètres ainsi que celui de sa valeur de retour doivent être précisés en utilisant des traits. Dans l'ordre du plus restrictif au plus « laxiste » :

1. Fn : La closure capture par référence (&T) ;

2. FnMut : La closure capture par référence mutable (&mut T) ;

3. FnOnce : La closure capture par valeur (T).

En se fiant au contexte, le compilateur va capturer les variables en privilégiant le « régime » le moins restrictif possible.

Par exemple, prenez un paramètre typé avec le trait FnOnce. Cela signifie que la closure peut capturer ses variables par référence &T, référence mutable &mut T, ou valeur T mais le compilateur reste encore le seul juge quant au régime à adopter, en fonction du contexte.

C'est pourquoi si un transfert (move) est possible alors n'importe quel type d'emprunts devrait être possible, notez que l'inverse n'est pas vrai. Si le paramètre est typé Fn alors les captures par référence mutable &mut T ou par valeur T ne sont pas permises.

Dans l'exemple suivant, essayez de modifier le type de capture (i.e. Fn, FnMut et FnOnce) pour voir ce qu'il se passe :

1. // Une fonction qui prend une closure en paramètre et appelle cette dernière.

2. fn apply<F>(f: F) where

3. // La closure ne prend rien et ne renvoie rien.

4. F: FnOnce() {

5. // ^ TODO: Essayez de remplacer ce trait par `Fn` ou `FnMut`.

7. f();

8. }

10. // Une fonction qui prend une closure en paramètre et renvoie un entier

11. // de type `i32`.

12. fn apply_to_3<F>(f: F) -> i32 where

13. // La closure prend en paramètre un `i32` et renvoie

14. // un `i32`.

15. F: Fn(i32) -> i32 {

17. f(3)

18. }

20. fn main() {

21. use std::mem;

23. let greeting = "hello";

24. // Un type qui ne peut pas être copié.

25. // `to_owned` crée une ressource dont

26. // l'assignation `farewell` sera responsable, à partir d'une ressource empruntée.

27. let mut farewell = "goodbye".to_owned();

29. // Capture deux variables: `greeting` par référence et

30. // `farewell` par valeur.

31. let diary = || {

32. // `greeting` est capturé par référence: requiert `Fn`.

33. println!("I said {}.", greeting);

35. // Le fait de modifier `farewell` rend obligatoire

36. // la capture par référence mutable, le compilateur choisira

37. // donc `FnMut`.

38. farewell.push_str("!!!");

39. println!("Then I screamed {}.", farewell);

40. println!("Now I can sleep. zzzzz");

42. // Appeler manuellement la fonction `drop` nécessite

43. // désormais de capturer par valeur `farewell`, le compilateur

44. // choisira alors `FnOnce`.

45. mem::drop(farewell);

46. };

48. // On appelle la fonction qui prend en paramètre la closure.

49. apply(diary);

51. // `double` satisfait les conditions du trait soumis à `apply_to_3`.

52. let double = |x| 2 * x;

54. println!("3 doubled: {}", apply_to_3(double));

55. }

Voir aussi

La fonction std::mem::drop et les traits Fn, FnMut et FnOnce.

8-2-3 - Les types anonymes

Les closures capturent succinctement les variables se trouvant dans les contextes qui les ont engendré. Cela a-t-il des conséquences ? Certainement. Nous remarquons qu'une fonction prête à recevoir une closure doit posséder un paramètre générique pour définir le « régime » de capture que la closure adoptera :

1. // `F` doit être générique.

2. fn apply<F>(f: F)

3. where

4. F: FnOnce(),

5. {

6. f();

7. }

Quand une closure est définie, le compilateur crée implicitement une structure anonyme pour stocker les variables capturées par la closure. Cette structure implémentera également l'un des traits rencontrés précédemment : Fn, FnMut ou FnOnce. Ce type anonyme est assigné à la variable stockée jusqu'à ce que la closure soit appelée.

Puisque le type créé implicitement est inconnu, son utilisation dans le corps d'une fonction nécessitera un paramètre générique. Cependant, un paramètre <T> dont le trait n'est pas précisé pourrait toujours être ambiguë et rejeté par le compilateur. Il est donc nécessaire de préciser quels services (i.e. Fn, FnMut ou FnOnce) il implémentera.

1. // `F` doit implémenter `Fn` pour une closure qui ne prend aucun

2. // argument et ne renvoie rien - exactement ce qui est nécessaire

3. // pour `print`.

4. fn apply<F>(f: F) where

5. F: Fn() {

6. f();

7. }

9. fn main() {

10. let x = 7;

12. // Capture la variable `x` dans une structure anonyme

13. // et implémente `Fn` pour cette dernière. On stocke dans `print`.

14. let print = || println!("{}", x);

16. apply(print);

17. }

Voir aussi

Une analyse complète des closures, Fn, FnMut et FnOnce.

8-2-4 - Fonctions passées en paramètres

Les closures peuvent être soumises en entrée aux fonctions, mais vous pourriez vous demander si nous pouvons faire de même avec d'autres fonctions. C'est le cas ! Si vous déclarez une fonction qui prend une closure en paramètre alors n'importe quelle fonction implémentant les traits requis peut être passée en paramètre.

1. // On déclare une fonction qui prend l'argument générique `F`

2. // délimité par le trait `Fn` et appelle la fonction (ou closure).

3. fn call_me<F: Fn()>(f: F) {

4. f();

5. }

7. // On déclare une fonction qui satisfait la délimitation (hériter de `Fn`).

8. fn function() {

9. println!("I'm a function!");

10. }

12. fn main() {

13. // On déclare une closure qui satisfait la délimitation (hériter de `Fn`).

14. let closure = || println!("I'm a closure!");

16. call_me(closure);

17. call_me(function);

18. }

Voir aussi

Fn, FnMut et FnOnce.

8-2-5 - Renvoyer une closure

Les closures peuvent être passées en paramètre à une fonction, donc les renvoyer devrait être possible. Cependant, renvoyer un « type » de closure est problématique car, actuellement, Rust ne supporte le renvoi que de types concrets (i.e. non-génériques). Le type anonyme d'une closure est, par définition, inconnu donc le renvoi d'une closure ne peut être fait qu'en rendant son type concret.

Les traits destinés à valider le renvoi d'une closure sont quelque peu différents :

- Fn : pas de changements pour ce trait ;

- FnMut : pas de changements pour ce trait ;

- FnOnce : Différentes choses entrent en jeu ici, donc le type FnBox doit être utilisé à la place de FnOnce. Notez toutefois que FnBox est taggé instable et que des modifications pourraient être apportées dans le futur.

En dehors de cela, le mot-clé move doit être utilisé, indiquant que toutes les captures se feront par valeur pour la closure courante. Il est nécessaire d'utiliser move car aucune capture par référence ne pourrait être libérée aussitôt la fonction terminée, laissant des références invalides dans la closure.

1. fn create_fn() -> Box<Fn()> {

2. let text = "Fn".to_owned();

4. Box::new(move || println!("This is a: {}", text))

5. }

7. fn create_fnmut() -> Box<FnMut()> {

8. let text = "FnMut".to_owned();

10. Box::new(move || println!("This is a: {}", text))

11. }

13. fn main() {

14. let fn_plain = create_fn();

15. let mut fn_mut = create_fnmut();

17. fn_plain();

18. fn_mut();

19. }

Voir aussi

Box, Fn, FnMut et la généricité.

8-2-6 - Exemples de la bibliothèque standard

Cette section contient quelques exemples d'utilisation de closures avec des outils fournis par la bibliothèque standard.

8-2-6-1 - Iterator::any

Iterator::any est une fonction qui, lorsqu'un itérateur est passé en paramètre, renvoie true si au moins un élément satisfait le prédicat, autrement false. Voici sa signature :

1. pub trait Iterator {

2. // Le type sur lequel on va itérer.

3. type Item;

5. // `any` prend en paramètre une référence mutable `&mut self` de

6. // l'instance courante qui sera empruntée et modifiée, mais pas consommée

7. // (possédée).

8. fn any<F>(&mut self, f: F) -> bool

9. where

10. F: FnMut(Self::Item) -> bool,

11. {

12. true

13. }

14. }

1. fn main() {

2. let vec1 = vec![1, 2, 3];

3. let vec2 = vec![4, 5, 6];

5. // `iter()`, pour les vecteurs, fournit la référence de chaque

6. // élément `&i32`.

7. println!("2 in vec1: {}", vec1.iter()     .any(|&x| x == 2));

8. // `into_iter()`, pour les vecteurs, fournit la valeur de chaque élément `i32`.

9. // L'itérateur est consommé.

10. println!("2 in vec2: {}", vec2.into_iter().any(| x| x == 2));

11. let array1 = [1, 2, 3];

12. let array2 = [4, 5, 6];

14. // `iter()` fournit la référence de chaque élément du tableau `&i32`.

15. println!("2 in array1: {}", array1.iter()     .any(|&x| x == 2));

16. // `into_iter()` fournit, exceptionnellement, la référence de chaque élément

17. // du tableau `&i32` (le type i32 implémente les traits requis).

18. println!("2 in array2: {}", array2.into_iter().any(|&x| x == 2));

19. }

Voir aussi

std::iter::Iterator::any.

8-2-6-2 - Iterator::find

Iterator::find est une fonction qui renvoie le premier élément correspondant au prédicat.

1. pub trait Iterator {

2. // Le type sur lequel on va itérer.

3. type Item;

5. // `find` prend en paramètre une référence mutable de l'instance courante

6. // `&mut self`. Elle sera donc empruntée et modifiée, mais pas consommée.

7. fn find<P>(&mut self, predicate: P) -> Option<Self::Item>

8. where

9. P: FnMut(&Self::Item) -> bool,

10. {

11. None

12. }

13. }

1. fn main() {

2. let vec1 = vec![1, 2, 3];

3. let vec2 = vec![4, 5, 6];

5. // `iter()` fournit la référence de chaque élément `&i32` du vecteur.

6. let mut iter = vec1.iter();

7. // `into_iter()` fournit la valeur de chaque élément du vecteur.

8. let mut into_iter = vec2.into_iter();

10. // Référence fournie par `iter`: `&&i32`. On déstructure la référence

11. // de la référence pour obtenir l'entier `i32`.

12. println!("Find 2 in vec1: {:?}", iter     .find(|&&x| x == 2));

13. // Référence fournie par `into_iter`: `&i32`. On déstructure la référence

14. // pour obtenir l'entier `i32`.

15. println!("Find 2 in vec2: {:?}", into_iter.find(| &x| x == 2));

17. let array1 = [1, 2, 3];

18. let array2 = [4, 5, 6];

20. // `iter()` fournit la référence de chaque élément du tableau `&i32`.

21. println!("Find 2 in array1: {:?}", array1.iter()     .find(|&&x| x == 2));

22. // `into_iter()` fournit, exceptionnellement, la référence de chaque élément

23. // du tableau `&i32` (le type i32 implémente les traits requis).

24. println!("Find 2 in array2: {:?}", array2.into_iter().find(|&&x| x == 2));

25. }

Voir aussi

std::iter::Iterator::find.

8-3 - Les fonctions d'ordre supérieur

Rust supporte les fonctions d'ordre supérieur (HOF). Ces fonctions prennent en paramètre une ou plusieurs fonctions et renvoie une autre fonction. Ce sont les HOF ainsi que les « itérateurs laxistes » qui donnent cet aspect fonctionnel à Rust.

1. fn is_odd(n: u32) -> bool {

2. n % 2 == 1

3. }

5. fn main() {

6. println!("Find the sum of all the squared odd numbers under 1000");

7. let upper = 1000;

9. // Approche impérative.

10. // On déclare un accumulateur.

11. let mut acc = 0; // 0, 1, 2, ... ∞

12. for n in 0.. {

13. // Le carré du nombre.

14. let n_squared = n * n;

16. if n_squared >= upper {

17. // On sort de la boucle si le carré de `n_squared`

18. // dépasse la limite imposée par `upper`.

19. break;

20. } else if is_odd(n_squared) {

21. // On accumule le carré de `n_squared` si c'est impair.

22. acc += n_squared;

23. }

24. }

25. println!("imperative style: {}", acc);

27. // Approche fonctionnelle.

28. let sum_of_squared_odd_numbers: u32 =

29. (0..).map(|n| n * n)             // On calcule le carré de chaque nombre.

30. .take_while(|&n| n < upper) // On vérifie que le carré se trouve toujours sous la limite de `upper`.

31. .filter(|&n| is_odd(n))     // On récupère le nombre si il est impair.

32. .fold(0, |sum, i| sum + i); // On accumule le carré du nombre impair.

33. println!("functional style: {}", sum_of_squared_odd_numbers);

34. }

L'énumération Option et le trait Iterator implémentent leur lot d'HOF.

9 - Les modules

Rust fournit un puissant système de modules qui peut être utilisé pour hiérarchiser et diviser logiquement le code en plusieurs sous-modules et gérer la visibilité des ressources (publiques ou privées).

Un module est un ensemble d'éléments (e.g. ensemble de fonctions, de structures, de traits, de blocs « impl » et même d'autres modules).

9-1 - La visibilité

Par défaut, tout ce qui peut être contenu dans un module est privé. Nous pouvons remédier à cela en utilisant le mot-clé pub. Seuls les items publics d'un module peuvent être sollicités en dehors du contexte du module.

1. // Un module nommé `my`.

2. mod my {

3. // Les items se trouvant dans le module sont privés, par défaut.

4. fn private_function() {

5. println!("called `my::private_function()`");

6. }

8. // Utilisez le mot-clé `pub` pour modifier la visibilité par défaut.

9. pub fn function() {

10. println!("called `my::function()`");

11. }

13. // Des items se trouvant dans le même module peuvent se solliciter

14. // entre-eux, même lorsqu'ils sont privés.

15. pub fn indirect_access() {

16. print!("called `my::indirect_access()`, that\n> ");

17. private_function();

18. }

20. // Les modules peuvent également être imbriqués.

21. pub mod nested {

22. pub fn function() {

23. println!("called `my::nested::function()`");

24. }

26. #[allow(dead_code)]

27. fn private_function() {

28. println!("called `my::nested::private_function()`");

29. }

30. }

31. // Les modules imbriqués suivent les mêmes règles vis-à-vis de la

32. // visibilité.

33. mod private_nested {

34. #[allow(dead_code)]

35. pub fn function() {

36. println!("called `my::private_nested::function()`");

37. }

38. }

39. }

41. fn function() {

42. println!("called `function()`");

43. }

45. fn main() {

46. // Les noms des modules rattachés à une ressource peuvent être explicités

47. // pour supprimer toute ambiguïté entre deux ressources possédant le même

48. // nom.

49. function();

50. my::function();

52. // Les items publics, y compris ceux qui se trouvent dans les modules

53. // imbriqués, peuvent être sollicités en dehors du module parent.

54. my::indirect_access();

55. my::nested::function();

58. // Les items privés d'un module ne peuvent pas être directement sollicités,

59. // même si ils sont imbriqués dans un module public:

61. // Erreur! `private_function` est privée.

62. // my::private_function();

63. // TODO ^ Essayez de décommenter cette ligne.

65. // Erreur! `private_function` est privée.

66. // my::nested::private_function();

67. // TODO ^ Essayez de décommenter cette ligne.

69. // Erreur! `private_nested` est un module privé.

70. // my::private_nested::function();

71. // TODO ^ Essayez de décommenter cette ligne.

72. }

9-2 - La visibilité des structures

Les structures disposent d'un niveau supplémentaire de visibilité dédié à leurs champs. Comme pour les autres ressources, les champs d'une structure sont privés par défaut, mais peuvent être rendus publics en utilisant, encore une fois, le mot-clé pub. La visibilité des champs ne s'applique, bien entendu, que lorsqu'une structure est sollicitée en dehors du module où elle a été déclarée et a pour but de masquer les données (encapsulation).

1. mod my {

2. // Une structure publique avec un champ public générique de type `T`.

3. pub struct WhiteBox<T> {

4. pub contents: T,

5. }

7. // Une structure publique avec un champ privé générique de type `T`.

8. #[allow(dead_code)]

9. pub struct BlackBox<T> {

10. contents: T,

11. }

13. impl<T> BlackBox<T> {

14. // Constructeur public.

15. pub fn new(contents: T) -> BlackBox<T> {

16. BlackBox {

17. contents: contents,

18. }

19. }

20. }

21. }

23. fn main() {

24. // Les structures publiques possédant des champs publics

25. // peuvent être instanciées avec les séparateurs `{}`.

26. let white_box = my::WhiteBox { contents: "public information" };

28. // et leurs champs peuvent être sollicités normalement.

29. println!("The white box contains: {}", white_box.contents);

31. // Les structures publiques composées de champs privés ne peuvent pas être

32. // instanciées de manière "classique" (i.e. en précisant le nom des champs).

33. // Erreur! `BlackBox` possèdent des champs privés.

34. // let black_box = my::BlackBox { contents: "classified information" };

35. // TODO ^ Essayez de décommenter cette ligne.

37. // En revanche, elles peuvent être créées avec un constructeur public.

38. let _black_box = my::BlackBox::new("classified information");

40. // Les champs privés d'une structure publique ne peuvent pas être

41. // sollicités directement.

42. // Erreur! Le champ `contents` est privé.

43. // println!("The black box contains: {}", _black_box.contents);

44. // TODO ^ Essayez de décommenter cette ligne.

45. }

Voir aussi

La généricité et les méthodes.

9-3 - La déclaration use

La déclaration use peut être utilisée pour assigner un chemin à un nouveau nom, pour y accéder plus rapidement.

1. // Assigne le chemin `deeply::nested::function` à l'identificateur

2. // `other_function`.

3. use deeply::nested::function as other_function;

5. fn function() {

6. println!("called `function()`");

7. }

9. mod deeply {

10. pub mod nested {

11. pub fn function() {

12. println!("called `deeply::nested::function()`")

13. }

14. }

15. }

17. fn main() {

18. // Accès moins verbeux à `deeply::nested::function`.

19. other_function();

21. println!("Entering block");

22. {

23. // Ceci est équivalent à `use deeply::nested::function as function`.

24. // La nouvelle assignation `function()` prendra le pas

25. // sur `deeply::nested::function()`.

26. use deeply::nested::function;

27. function();

29. // Les assignations `use` ne sont disponibles que dans le contexte

30. // où elles ont vu le jour. Dans ce cas, où `function()` occulte

31. // le chemin de base, ce "shadowing" n'est effectif que dans ce bloc.

32. println!("Leaving block");

33. }

35. function();

36. }

9-4 - Les mot-clés super et self

Les mot-clés super et self peuvent être utilisés pour lever l'ambiguïté sur la provenance d'une ressource et éviter de réécrire leur chemin respectif(i.e. le chemin de la ressource).

1. fn function() {

2. println!("called `function()`");

3. }

5. mod cool {

6. pub fn function() {

7. println!("called `cool::function()`");

8. }

9. }

11. mod my {

12. fn function() {

13. println!("called `my::function()`");

14. }

16. mod cool {

17. pub fn function() {

18. println!("called `my::cool::function()`");

19. }

20. }

22. pub fn indirect_call() {

23. // Appelons toutes les fonctions nommées `fonction` depuis ce

24. // contexte!

25. print!("called `my::indirect_call()`, that\n> ");

27. // Le mot-clé `self` fait référence au module courant (en l'occurrence

28. // `my`). Appeler `self::function()` ou `function()` revient exactement

29. // au même, puisqu'ils font tous deux référence à la même fonction.

30. self::function();

31. function();

33. // Vous pouvez également utiliser `self` pour accéder à un autre module

34. // imbriqué dans `my`.

35. self::cool::function();

37. // Le mot-clé `super` fait référence au contexte parent (en-dehors du

38. // module `my`, pour cet exemple).

39. super::function();

41. // Si vous ne spécifiez aucun des deux mot-clés, le compilateur

42. // comprendra que vous essayez d'utiliser une ressource se trouvant

43. // dans le contexte de la crate.

44. // On va donc assigner un nouvel identificateur à `cool::function()` se

45. // trouvant dans le contexte de la crate.

46. // Le contexte de la crate représente tout ce qui se trouve en-dehors des modules.

47. {

48. use cool::function as root_function;

49. root_function();

50. }

51. }

52. }

54. fn main() {

55. my::indirect_call();

56. }

9-5 - La hiérarchie des fichiers

Il est possible de transformer nos modules en un ensemble de fichiers et de répertoires. Recréons l'exemple utilisé pour illustrer le concept de la visibilité en un ensemble de fichiers :

1. $ tree .

2. .

3. |-- my

4. |   |-- inaccessible.rs

5. |   |-- mod.rs

6. |   `-- nested.rs

7. `-- split.rs

1. // Dans le fichier my/mod.rs

2. // De la même manière `mod inaccessible` et `mod nested` vont essayer de localiser

3. // les fichiers `nested.rs` et `inaccessible.rs` pour récupérer et ajouter leur contenu

4. // dans leur module respectif.

5. mod inaccessible;

6. pub mod nested;

8. pub fn function() {

9. println!("called `my::function()`");

10. }

12. fn private_function() {

13. println!("called `my::private_function()`");

14. }

16. pub fn indirect_access() {

17. print!("called `my::indirect_access()`, that\n> ");

19. private_function();

20. }

1. // Dans le fichier my/nested.rs

2. pub fn function() {

3. println!("called `my::nested::function()`");

4. }

6. #[allow(dead_code)]

7. fn private_function() {

8. println!("called `my::nested::private_function()`");

9. }

1. // Dans le fichier my/inaccessible.rs

2. #[allow(dead_code)]

3. pub fn public_function() {

4. println!("called `my::inaccessible::public_function()`");

5. }

1. $ rustc split.rs && ./split

2. called `my::function()`

3. called `function()`

4. called `my::indirect_access()`, that

5. > called `my::private_function()`

6. called `my::nested::function()`

10 - Les crates

Une crate est une unité de compilation en Rust. Lorsque rustc some_file.rs est appelé, some_file.rs est considéré comme étant un «fichier paquet» (i.e. un fichier fédérant les autres). Si some_file.rs possède plusieurs modules en son sein, chacun d'entre eux verra son contenu fusionné dans ce paquet avant que la compilation n'ait lieu. Autrement dit, les modules ne sont pas compilés individuellement, mais en tant que paquet, en tant qu'ensemble de ressources.

Une crate peut être compilée en tant qu'exécutable ou bibliothèque. Par défaut, rustc produira un exécutable mais cela peut être modifié en passant le flag --crate-type au compilateur.

10-1 - Créer une bibliothèque

Commençons par créer une bibliothèque dont nous nous servirons ensuite pour l'importer dans une autre crate.

1. // Dans le fichier rary.rs

2. pub fn public_function() {

3. println!("called rary's `public_function()`");

4. }

6. fn private_function() {

7. println!("called rary's `private_function()`");

8. }

10. pub fn indirect_access() {

11. print!("called rary's `indirect_access()`, that\n> ");

13. private_function();

14. }

$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib

Les bibliothèques sont préfixées par la séquence « lib » et possèdent, par défaut, le nom du fichier utilisé pour créer la crate (en l'occurrence rary.rs). Ce comportement peut, bien entendu, être modifié en utilisant l'attribut crate_name.

10-2 - La déclaration extern crate

Pour importer une crate à cette nouvelle bibliothèque, il vous faudra utiliser la déclaration extern crate. Cette déclaration a aussi pour effet d'importer toutes les ressources sous un même module, possédant le même nom que la bibliothèque. Les règles régissant la visibilité des ressources s'appliquent également aux modules des bibliothèques importées.

1. // Dans le fichier executable.rs

2. // On importe la bibliothèque `library` et ses ressources

3. // sous un module nommé `rary`.

4. extern crate rary;

6. fn main() {

7. rary::public_function();

9. // Error! `private_function` est privée.

10. // rary::private_function();

12. rary::indirect_access();

13. }

1. # Où `library.rlib` est le chemin de la bibliothèque compilée, nous admetterons ici

2. # que la bibliothèque se trouve dans le répertoire courant.

3. $ rustc executable.rs --extern rary=library.rlib && ./executable

4. called rary′s `public_function()`

5. called rary′s `indirect_access()`, that

6. > called rary′s `private_function()`

11 - Les attributs

Un attribut est une méta-donnée pouvant être appliqué à plusieurs sortes d'éléments (e.g. modules, crates). Ces méta-données peuvent être utilisées pour :

- Ajouter des conditions à la compilation;

- Établir le nom, la version et le type (i.e. exécutable ou bibliothèque) d'une crate ;

- Désactiver certains avertissements de l'analyse;

- Activer des fonctionnalités (e.g. macros, imports globaux) propres au compilateur ;

- Importer une bibliothèque d'un autre langage (i.e. FFI);

- Signaler des fonctions utilisées pour exécuter des tests unitaires ;

- Signaler des fonctions utilisées dans le cadre d'un benchmark.

Quand les attributs sont appliqués à une crate toute entière, leur syntaxe est la suivante #![crate_attribute]. Lorsqu'ils sont appliqués à un module ou un autre élément, la syntaxe est la suivante #[item_attribute](vous noterez la disparation du point d'exclamation).

Les attributs peuvent prendre des arguments sous différentes syntaxes :

- #[attribute = "value"] ;

- #[attribute(key = "value")] ;

- #[attribute(value)].

11-1 - L'avertissement dead_code

La compilateur fournit la lintdead_code qui vous avertira lorsqu'une instruction (ou une fonction) ne sera jamais exécutée. Un attribut peut être utilisé pour désactiver cette lint.

1. fn used_function() {}

3. // L'attribut `#[allow(dead_code)]` désactive la lint `dead_code`.

4. #[allow(dead_code)]

5. fn unused_function() {}

7. fn noisy_unused_function() {}

8. // FIXME ^ Ajoutez un attribut pour supprimer l'avertissement.

10. fn main() {

11. used_function();

12. }

Gardez tout de même à l'esprit que, dans un programme destiné à être mis en production, il est préférable de supprimer le code mort. Ici, le code mort sera conservé simplement pour l'exemple.

11-2 - Méta-données relatives aux crates

L'attribut crate_type peut être utilisé pour renseigner au compilateur le type de la crate (i.e. exécutable ou bibliothèque(et quel type de bibliothèque)) et l'attribut crate_name est utilisé pour renseigner le nom de la crate.

1. // Dans le fichier lib.rs

2. // Cette crate est une bibliothèque.

3. #![crate_type = "lib"]

4. // Cette bibliothèque est nommée "rary".

5. #![crate_name = "rary"]

7. pub fn public_function() {

8. println!("called rary's `public_function()`");

9. }

11. fn private_function() {

12. println!("called rary's `private_function()`");

13. }

15. pub fn indirect_access() {

16. print!("called rary's `indirect_access()`, that\n> ");

18. private_function();

19. }

Lorsque l'attribut crate_type est utilisé vous n'avez, bien entendu, plus besoin de passer le flag --crate-type à rustc.

$ rustc lib.rs
$ ls lib*
library.rlib

11-3 - L'attribut cfg

La compilation conditionnelle est possible grâce à deux opérateurs :

1. L'attribut cfg : #[cfg(…)] ;

2. La macro cfg! : cfg!(…) en tant qu'expression booléenne.

Tous deux possèdent la même syntaxe :

1. // Cette fonction ne sera compilée que sur des distributions Linux.

2. #[cfg(target_os = "linux")]

3. fn are_you_on_linux() {

4. println!("You are running linux!")

5. }

7. // Et cette fonction sera compilée seulement sur des plateformes qui ne sont

8. // *pas* des distributions Linux.

9. #[cfg(not(target_os = "linux"))]

10. fn are_you_on_linux() {

11. println!("You are *not* running linux!")

12. }

14. fn main() {

15. are_you_on_linux();

17. println!("Are you sure?");

18. if cfg!(target_os = "linux") {

19. println!("Yes. It's definitely linux!");

20. } else {

21. println!("Yes. It's definitely *not* linux!");

22. }

23. }

Voir aussi

La référence de l'attribut cfg, std::cfg! et les macros.

11-3-1 - Condition personnalisée

Certaines conditions (e.g. target_os) sont fournies par rustc. Il est toutefois possible de passer des conditions personnalisées à rustc en utilisant le flag --cfg.

1. // Dans le fichier custom.rs

2. #[cfg(some_condition)]

3. fn conditional_function() {

4. println!("condition met!")

5. }

7. fn main() {

8. conditional_function();

9. }

Sans le flag personnalisé :

$ rustc custom.rs && ./custom
No such file or directory (os error 2)

Avec le flag personnalisé :

$ rustc --cfg some_condition custom.rs && ./custom
condition met!

12 - La généricité

Comme son nom l'indique, cette section abordera les types et fonctionnalités génériques. La généricité peut être très utile pour réduire les répétitions au sein du code dans de nombreux cas, mais vous demandera, en échange, d'apporter quelques précisions supplémentaires à propos de la syntaxe. Notez également que rendre une ressource générique signifie que n'importe quelle ressource sera traitée de la même manière, il est nécessaire de savoir quels types de ressources peuvent être réellement traités, dans les cas où il est nécessaire de le spécifier.

La généricité est principalement utilisée pour rendre générique un, ou plusieurs, paramètre passé à une fonction. Par convention, un paramètre générique doit avoir un identificateur respectant la convention de nommage CamelCase et être déclaré entre un chevron ouvrant (<) et un chevron fermant(>) : <Aaa, Bbb, ...>, qui est souvent représenté par le paramètre <T>. En déclarant un paramètre générique de type <T>, on accepte de recevoir un, ou plusieurs, paramètre de ce type. Tout paramètre déclaré comme générique est générique, tout le reste est concret (non-générique).

Par exemple, voici une fonction générique nommée foo qui prend un paramètre de type T (de n'importe quel type, donc) :

fn foo<T>(p: T) { ... }

1. // A est un type concret.

2. struct A;

4. // Lorsque nous déclarons `Single`, la première occurrence de `A` n'est

5. // pas précédée du type générique `<A>`. Le type `Single` et `A` sont donc

6. // concrets.

7. struct Single(A);

8. //            ^ Voici la première occurrence du type `A`.

10. // En revanche, ici, `<T>` précède la première occurrence `T`, donc le type

11. // `SingleGen` est générique. Puisque le type `T` est générique, cela pourrait être

12. // "n'importe quoi", y compris le type concret `A` déclaré au début du fichier.

13. struct SingleGen<T>(T);

15. fn main() {

16. // `Single` est un type concret et prend explicitement un paramètre

17. // de type `A`.

18. let _s = Single(A);

20. // On créé une variable nommée `_char` de type `SingleGen<char>`

21. // et on lui assigne la valeur `SingleGen('a')`.

22. // Le type requis du paramètre passé pour cette instance de `SingleGen`

23. // est spécifié, mais il peut être omis, exemple ---

24. let _char: SingleGen<char> = SingleGen('a');

26. // --->

27. let _t    = SingleGen(A); // On passe une instance

28. // du type `A` définit en haut.

29. let _i32  = SingleGen(6); // On passe un entier de type `i32`.

30. let _char = SingleGen('a'); // On passe un `char`.

31. }

Voir aussi

Les structures

12-1 - Les fonctions

Les règles précédemment présentées s'appliquent également aux fonctions : un type T est générique lorsqu'il est précédé par la déclaration <T> (La lettre varie bien entendu selon le nom du type générique que vous donnez).

Lorsque vous utilisez des fonctions génériques il peut, parfois, être nécessaire d'expliciter le type des paramètres dans le cas où, par exemple, la fonction appelée possède un type de renvoi générique, ou encore si le compilateur ne dispose pas d'assez d'informations pour inférer le type des paramètres.

Une fonction dont le type des paramètres est explicité devrait ressembler à ceci : fn::<A, B, ...>().

1. struct A;          // Le type `A` est concret.

2. struct S(A);       // Le type `S` est concret.

3. struct SGen<T>(T); // Le type `SGen` est générique.

5. // Toutes les fonctions qui suivront possèdent les paramètres

6. // qui leur sont passés et libèrent la mémoire aussitôt.

8. // On définit une fonction nommée `reg_fn` qui prend un argument `_s` de type

9. // `S`. Ce dernier n'est pas précédé d'un `<T>` (ou, en l'occurrence un `<S>`)

10. // donc le type n'est pas générique.

11. fn reg_fn(_s: S) {}

13. // On définit une fonction nommée `gen_spec_t` qui prend un argument `_s` de type

14. // `SGen<T>`.

15. // Le type d'argument imposé à la structure SGen<T> est précisé, mais puisque

16. // `A` n'est pas précédé par le type générique `<A>`, le type n'est pas générique.

17. fn gen_spec_t(_s: SGen<A>) {}

18. //          ^ il aurait fallu déclarer `A` comme générique: `<A>`.

20. // On définit une fonction nommée `gen_spec_i32` qui prend un argument `_s` de

21. // type `SGen<i32>`.

22. // Le type de paramètre supporté par la structure SGen a été spécifié, `i32`.

23. // Puisque `i32` n'est pas un type générique, cette fonction n'est pas générique non plus.

24. fn gen_spec_i32(_s: SGen<i32>) {}

26. // On définit une fonction nommée `generic` qui prend un paramètre `_s` de type

27. // `SGen<T>`.

28. // Puisque `SGen<T>` est précédé par le type générique `<T>`,

29. // cette fonction est donc générique.

30. fn generic<T>(_s: SGen<T>) {}

32. fn main() {

33. // Appel des fonctions qui ne sont pas génériques.

34. reg_fn(S(A));          // On passe en paramètre un type concret.

35. gen_spec_t(SGen(A));   // Type `A` implicitement spécifié car la fonction

36. // n'accepte que la signature `SGen<A>`.

37. gen_spec_i32(SGen(6)); // Type `i32` implicitement spécifié car la fonction

38. // n'accepte que la signature `SGen<i32>`.

40. // Type du paramètre explicitement spécifié pour

41. // la fonction `generic()`.

42. // Le type peut être omis car le compilateur peut inférer

43. // le type à partir du littéral.

44. generic::<char>(SGen('a'));

46. // Type du paramètre implicitement spécifié pour la fonction

47. // `generic()` car le compilateur est capable d'inférer le type

48. // à partir du littéral.

49. generic(SGen('c'));

50. }

Voir aussi

Les fonctions, les structures.

12-2 - Implémentation générique

Tout comme les fonctions, les implémentations nécessitent quelques précisions pour être génériques.

1. struct S; // On déclare le type concret `S`.

2. struct GenericVal<T>(T); // On déclare le type générique `GenericVal`.

4. // Implémentation de GenericVal où nous précisons que cette méthode doit être

5. // implémentée uniquement pour le type `f32`.

6. impl GenericVal<f32> {

7. fn say_hello_f32(&self) -> (){

8. println!("I'm a float! :D");

9. }

10. } // On spécifie `f32`

11. impl GenericVal<S> {

12. fn say_hello_s(&self) -> (){

13. println!("I'm a S object! :D");

14. }

15. } // On spécifie le type `S` pour les mêmes raisons qu'au-dessus.

16. // `<T>` doit précéder le type pour le rendre générique.

17. impl <T> GenericVal<T> {

18. fn say_hello(&self) -> (){

19. println!("I'm a generic object! :D");

20. }

21. }

23. struct Val {

24. val: f64

25. }

27. struct GenVal<T>{

28. gen_val: T

29. }

31. // Implémentation de Val.

32. impl Val {

33. fn value(&self) -> &f64 { &self.val }

34. }

36. // Implémentation de GenVal pour le type générique `T`.

37. impl <T> GenVal<T> {

38. fn value(&self) -> &T { &self.gen_val }

39. }

41. fn main() {

42. let x = Val { val: 3.0 };

43. let y = GenVal { gen_val: 3i32 };

45. println!("{}, {}", x.value(), y.value());

46. GenericVal(1.0).say_hello_f32();

47. GenericVal(S).say_hello_s();

48. GenericVal("prout").say_hello();

49. }

Voir aussi

Renvoi de références, impl, les structures.

12-3 - Les traits

Bien entendu, les traits peuvent également être génériques. Dans cette section, nous allons en créer un qui ré-implémente le trait Drop qui proposera une méthode générique qui aura pour fonction de libérer l'instance qui l'appelle ainsi qu'un paramètre passé.

1. // Types non-copiables.

2. struct Empty;

3. struct Null;

5. // Un trait générique qui reçoit un type générique `T`.

6. trait DoubleDrop<T> {

7. // On déclare une méthode qui sera implémentée par la structure

8. // appelante (caller) et prendra un paramètre `T` en entrée mais n'en fera rien (juste le libérer).

9. fn double_drop(self, _: T);

10. }

12. // On implémente `DoubleDrop<T>` pour n'importe quel paramètre `T` et

13. // n'importe quelle structure appelante (`U`).

14. impl<T, U> DoubleDrop<T> for U {

15. // Cette méthode prend "possession" des deux paramètres (`U` et `T`),

16. // et sont donc tous deux libérés.

17. fn double_drop(self, _: T) {}

18. }

20. fn main() {

21. let empty = Empty;

22. let null  = Null;

24. // `empty` et `null` sont désalloués.

25. empty.double_drop(null);

27. // empty;

28. // null;

29. // ^ TODO: Essayez de décommenter ces lignes.

30. }

Voir aussi

La documentation du trait Drop, le chapitre sur les structures et les traits.

12-4 - Les restrictions

Lorsque nous travaillons avec la généricité, il est courant d'assigner une « restriction » à un type générique pour spécifier quel trait il doit implémenter. Dans l'exemple suivant, nous utilisons le trait Display pour afficher quelque chose en console, il est alors assigné au type T. Autrement dit, T doit implémenter Display.

1. // On déclare une fonction nommée `printer` qui prend, en entrée,

2. // un type générique `T` qui doit implémenter le trait `Display`.

3. fn printer<T: Display>(t: T) {

4. println!("{}", t);

5. }

Les ressources passées en paramètre sont toutes soumises à ces restrictions et doivent forcément remplir les conditions :

1. struct S<T: Display>(T);

3. // Erreur! `Vec<T>` n'implémente pas le trait `Display`.

4. let s = S(vec![1]);

Les instances des types génériques peuvent également accéder aux méthodes appartenant au(x) trait(s) présent(s) dans les restrictions du type. Par exemple :

1. // Un trait qui implémente le marqueur `{:?}`.

2. use std::fmt::Debug;

4. trait HasArea {

5. fn area(&self) -> f64;

6. }

8. impl HasArea for Rectangle {

9. fn area(&self) -> f64 { self.length * self.height }

10. }

12. #[derive(Debug)]

13. struct Rectangle { length: f64, height: f64 }

14. #[allow(dead_code)]

15. struct Triangle  { length: f64, height: f64 }

17. // Le type générique `T` doit implémenter le trait `Debug`.

18. // Qu'importe le type de `T`, cela fonctionnera.

19. fn print_debug<T: Debug>(t: &T) {

20. println!("{:?}", t);

21. }

23. // `T` doit implémenter le trait `HasArea`. N'importe quelle

24. // structure remplissant les conditions d'entrée peut accéder

25. // à la méthode `area` du trait `HasArea`.

26. fn area<T: HasArea>(t: &T) -> f64 { t.area() }

28. fn main() {

29. let rectangle = Rectangle { length: 3.0, height: 4.0 };

30. let _triangle = Triangle  { length: 3.0, height: 4.0 };

32. print_debug(&rectangle);

33. println!("Area: {}", area(&rectangle));

35. // print_debug(&_triangle);

36. // println!("Area: {}", area(&_triangle));

37. // ^ TODO: Essayez de décommenter ces lignes.

38. // | Erreur: N'implémente pas l'un de ces traits: `Debug` ou `HasArea`.

39. }

Les conditions d'entrée pour les paramètres génériques peuvent également être spécifiées en utilisant le mot-clé where, les rendant plus explicites, plus lisibles.

Voir aussi

Les traits dédiés à l'affichage et le formatage std::fmt, les structures et les traits.

12-4-1 - Exemple d'utilisation : Traits sans services

Puisqu'il est possible d'imposer des conditions aux types génériques grâce aux traits, même si ces derniers ne possèdent aucune fonctionnalité (i.e. aucun service, aucune méthode), il est toujours possible de vous en servir comme simple « filtre ». Eq et Ord font partie de ces traits « vides » fournis par la bibliothèque standard.

1. struct Cardinal;

2. struct BlueJay;

3. struct Turkey;

5. trait Red {}

6. trait Blue {}

8. impl Red for Cardinal {}

9. impl Blue for BlueJay {}

11. // Ces fonctions ne prendront en entrée que des ressources

12. // ayant implémenté les traits `Red` ou `Blue`.

13. // Le fait que ces derniers soient vides n'a que peu d'importance.

14. fn red<T: Red>(_: &T)   -> &'static str { "red" }

15. fn blue<T: Blue>(_: &T) -> &'static str { "blue" }

17. fn main() {

18. let cardinal = Cardinal;

19. let blue_jay = BlueJay;

20. let _turkey   = Turkey;

22. // La fonction `red()` ne fonctionnera pas sur une instance

23. // de la structure `BlueJay`, et vice versa, à cause des

24. // restrictions imposées par les fonctions (i.e. `red()` et `blue()`).

25. println!("A cardinal is {}", red(&cardinal));

26. println!("A blue jay is {}", blue(&blue_jay));

27. //println!("A turkey is {}", red(&_turkey));

28. // ^ TODO: Essayez de décommenter cette ligne.

29. }

Voir aussi

La documentation du trait Eq, la documentation du trait Ord et les traits.

12-5 - Restrictions multiples

Il est possible d'additionner les conditions grâce à l'opérateur +. Tout comme les types concrets, les types génériques sont séparés par une virgule ,.

1. use std::fmt::{Debug, Display};

3. fn compare_prints<T: Debug + Display>(t: &T) {

4. println!("Debug: `{:?}`", t);

5. println!("Display: `{}`", t);

6. }

8. fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) {

9. println!("t: `{:?}", t);

10. println!("u: `{:?}", u);

11. }

13. fn main() {

14. let string = "words";

15. let array = [1, 2, 3];

16. let vec = vec![1, 2, 3];

18. compare_prints(&string);

19. // compare_prints(&array);

20. // TODO ^ Essayez de décommenter cette ligne.

22. compare_types(&array, &vec);

23. }

Voir aussi

std::fmt, les traits.

12-6 - La clause where

Une restriction peut également être explicitée par la clause where. Cette dernière se trouvera alors avant l'accolade ouvrante ({) plutôt qu'à la déclaration du type(e.g. <A: Display, B: Debug, ...>). Avec where, vous pouvez également ajouter arbitrairement d'autres types en plus de spécifier les traits à implémenter pour les types génériques.

where peut être utile dans plusieurs cas :

- Lorsque vous ajoutez des restrictions aux types génériques, facilitant la lecture :

1. impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}

3. // Les restrictions sont explicitées

4. // par la condition `where`.

5. impl <A, D> MyTrait<A, D> for YourType where

6. A: TraitB + TraitC,

7. D: TraitE + TraitF {}

- Lorsque d'autres types sont ajoutés aux restrictions :

1. use std::fmt::{Debug, Formatter, Result};

3. trait PrintInOption {

4. fn print_in_option(self);

5. }

7. // Parce que nous pourrions modifier la restriction sans spécifier le

8. // conteneur `T: Debug` ou adopter une autre approche,

9. // il est nécessaire d'utiliser la condition `where`:

10. impl<T> PrintInOption for T where

11. Option<T>: Debug{

12. // Nous spécifions la restriction de type `Option<T>: Debug` parce que

13. // c'est ce que nous souhaitons afficher. Faire autrement pourrait nous

14. // induire en erreur quant au type de restriction à spécifier.

15. fn print_in_option(self) {

16. println!("{:?}", Some(self));

17. }

18. }

20. fn main() {

21. let vec = vec![1, 2, 3];

22. vec.print_in_option();

23. }

Voir aussi

RFC pour la condition/clause where, les structures, les traits.

12-7 - Les éléments associés

Le concept « d'éléments associés » est un ensemble de règles appliquées sur différents types d'éléments. C'est une extension des traits génériques, leur permettant de définir de nouveaux « éléments » en leur sein ; En l'occurrence, ces derniers sont nommés « types associés » et proposent une approche moins fastidieuse pour l'utilisation des patterns (e.g. déclaration des types) lorsqu'un trait reçoit des paramètres génériques en entrée.

Voir aussi

La RFC des éléments associés.

12-7-1 - Le problème

Un trait qui possède des types génériques en entrée doit respecter certaines règles : Les utilisateurs du trait doivent spécifier tous les types de données génériques supportés par le conteneur.

Dans l'exemple ci-dessous, le traitContains permet l'utilisation des types génériques A et B. Le trait est alors implémenté pour le type Container en spécifiant le type i32 pour les deux types génériques (i.e. A et B). Ils peuvent donc être soumis à la fonction fn difference().

Puisque le type Contains est générique, nous sommes obligés de déclarer tous les types génériques supportés par Contains pour la fonction difference(). En pratique, nous opterions pour une approche nous permettant d'inférer les types génériques supportés par Contains à partir de l'entrée C. C'est ce que nous verrons dans la section suivante, car les types associés autorisent cette approche.

1. struct Container(i32, i32);

3. // Nous déclarons un trait qui vérifie si deux items sont contenus

4. // par le conteneur.

5. // Le conteneur pourra également fournir la première ou dernière

6. // valeur.

7. trait Contains<A, B> {

8. fn contains(&self, &A, &B) -> bool;

9. fn first(&self) -> i32;

10. fn last(&self) -> i32;

11. }

13. impl Contains<i32, i32> for Container {

14. // Renvoie `true` si les nombres stockés sont égaux.

15. fn contains(&self, number_1: &i32, number_2: &i32) -> bool {

16. (&self.0 == number_1) && (&self.1 == number_2)

17. }

19. // On récupère la première valeur.

20. fn first(&self) -> i32 { self.0 }

22. // On récupère la dernière valeur.

23. fn last(&self) -> i32 { self.1 }

24. }

26. // `C` contient `A` et `B`. Les déclarer une nouvelle fois dans les paramètres

27. // génériques de la fonction est fastidieux.

28. fn difference<A, B, C>(container: &C) -> i32 where

29. C: Contains<A, B> {

30. container.last() - container.first()

31. }

33. fn main() {

34. let number_1 = 3;

35. let number_2 = 10;

37. let container = Container(number_1, number_2);

39. println!("Does container contain {} and {}: {}",

40. &number_1, &number_2,

41. container.contains(&number_1, &number_2));

42. println!("First number: {}", container.first());

43. println!("Last number: {}", container.last());

45. println!("The difference is: {}", difference(&container));

46. }

Voir aussi

Les structures.

12-7-2 - Les types associés

L'utilisation des « types associés » améliore la lisibilité du code en assignant les types génériques, au sein du trait, comme « types de sortie ». La syntaxe pour les déclarer est la suivante :

1. // `A` et `B` sont déclarés au sein du trait par le biais du mot-clé `type`.

2. // Notez toutefois que `type`, dans ce contexte, n'a pas la même fonction

3. // que le `type` utilisé pour créer des alias.

4. trait Contains {

5. type A;

6. type B;

8. // La syntaxe a été modifiée pour faire référence aux types génériques.

9. fn contains(&self, &Self::A, &Self::B) -> bool;

10. }

Notez que les fonctions qui utilisent le trait Contains n'ont plus du tout besoin de déclarer les types génériques A et B :

1. // Sans les types associés.

2. fn difference<A, B, C>(container: &C) -> i32 where

3. C: Contains<A, B> { ... }

5. // Avec les types associés.

6. fn difference<C: Contains>(container: &C) -> i32 { ... }

Éditons l'exemple de la section précédente en utilisant les types associés :

1. struct Container(i32, i32);

3. // Nous déclarons un trait qui vérifie si deux items sont stockés

4. // dans le conteneur.

5. // Le conteneur pourra également fournir la première ou dernière valeur.

6. trait Contains {

7. // Nous définissons une bonne fois pour tous les types génériques

8. // que les méthodes/fonctions pourront utiliser.

9. type A;

10. type B;

12. fn contains(&self, &Self::A, &Self::B) -> bool;

13. fn first(&self) -> i32;

14. fn last(&self) -> i32;

15. }

17. impl Contains for Container {

18. // On précise le type de `A` et `B`. Si le type d'entrée est

19. // `Container(i32, i32)`, les types de sorties seront alors

20. // typés tous les deux `i32`.

21. type A = i32; // typé i32

22. type B = i32; // typé i32

24. // `&Self::A` et `&Self::B` sont également valides ici.

25. // Essayez de remplacer la référence number_1(`&i32`) par `&Self::A`,

26. // et la référence number_2(`&i32) par `&Self::B`!

27. fn contains(&self, number_1: &i32, number_2: &i32) -> bool {

28. (&self.0 == number_1) && (&self.1 == number_2)

29. }

30. // Récupère la première valeur.

31. fn first(&self) -> i32 { self.0 }

33. // Récupère la dernière valeur.

34. fn last(&self) -> i32 { self.1 }

35. }

37. fn difference<C: Contains>(container: &C) -> i32 {

38. container.last() - container.first()

39. }

41. fn main() {

42. let number_1 = 3;

43. let number_2 = 10;

45. let container = Container(number_1, number_2);

47. println!("Does container contain {} and {}: {}",

48. &number_1, &number_2,

49. container.contains(&number_1, &number_2));

50. println!("First number: {}", container.first());

51. println!("Last number: {}", container.last());

53. println!("The difference is: {}", difference(&container));

54. }

12-8 - Les paramètres fantômes

Un type de paramètre fantôme n'est pas utilisé à l'exécution, mais est vérifié statiquement (et seulement) au moment de la compilation.

Les types de données peuvent utiliser des types de paramètres génériques supplémentaires pour agir en tant que « marqueurs » ou pour effectuer une vérification du/des type(s) au moment de la compilation. Ces paramètres « supplémentaires » ne stockent aucune ressource et sont inactifs à l'exécution.

Dans l'exemple ci-dessous, nous présentons la structure std::marker::PhantomData avec le concept de « type de paramètre fantôme » pour créer des tuples contenant différents types de données.

1. use std::marker::PhantomData;

3. // Un tuple qui prend un type générique `A` et un paramètre fantôme `B`.

4. #[derive(PartialEq)] // Permet de tester l'égalité entre les instances du type.

5. struct PhantomTuple<A, B>(A,PhantomData<B>);

7. // Un tuple qui prend un type générique `A` et un paramètre fantôme `B`.

8. #[derive(PartialEq)] // Permet de tester l'égalité entre les instances du type.

9. struct PhantomStruct<A, B> { first: A, phantom: PhantomData<B> }

11. // Note: De la mémoire sera allouée pour le type générique `A`, mais pas pour `B`.

12. //       En revanche, `B` ne pourra pas être utilisé à l'exécution.

14. fn main() {

15. // Ici, les types `f32` et `f64` sont des paramètres fantômes.

16. // Types spécifiés pour PhantomTuple: `<char, f32>`.

17. let _tuple1: PhantomTuple<char, f32> = PhantomTuple('Q', PhantomData);

18. // Types spécifiés pour PhantomTuple: `<char, f64>`.

19. let _tuple2: PhantomTuple<char, f64> = PhantomTuple('Q', PhantomData);

21. // Types spécifiés pour PhantomStruct: `<char, f32>`.

22. let _struct1: PhantomStruct<char, f32> = PhantomStruct {

23. first: 'Q',

24. phantom: PhantomData,

25. };

26. // Types spécifiés pour PhantomStruct: `<char, f64>`.

27. let _struct2: PhantomStruct<char, f64> = PhantomStruct {

28. first: 'Q',

29. phantom: PhantomData,

30. };

32. // Erreur! Les deux ressources ne peuvent pas être comparées:

33. // println!("_tuple1 == _tuple2 yields: {}",

34. //          _tuple1 == _tuple2);

36. // Erreur! Les deux ressources ne peuvent pas être comparées:

37. // println!("_struct1 == _struct2 yields: {}",

38. //          _struct1 == _struct2);

39. }

Voir aussi

L'attribut Derive, les structures et les tuples.

12-8-1 - Exemple d'utilisation  Petite précision

Nous allons créer une méthode chargée de calculer dans deux unités de mesure différentes (le pied et le millimètre) et nous implémenterons le traitAdd avec un type générique fantôme. Voici l'implémentation du traitAdd :

1. // Cette implémentation devrait imposer: `Self + RHS = Output`

2. // où `Self` est la valeur par défaut de `RHS` si elle n'est pas spécifiée

3. // dans l'implémentation.

5. pub trait Add<RHS = Self> {

6. type Output;

8. fn add(self, rhs: RHS) -> Self::Output;

9. }

11. // `Output` doit être de type `T<U>` pour que `T<U> + T<U> = T<U>`.

12. impl<U> Add for T<U> {

13. type Output = T<U>;

14. ...

15. }

Voici l'implémentation complète :

1. use std::ops::Add;

2. use std::marker::PhantomData;

4. /// On créé des énumérations vides pour déclarer le type des

5. // unités de mesures.

6. #[derive(Debug, Clone, Copy)]

7. enum Inch {}

8. #[derive(Debug, Clone, Copy)]

9. enum Mm {}

11. /// `Length` est une structure prenant un type générique fantôme `Unit`,

12. /// `f64` implémente déjà les traits `Clone` et `Copy`.

13. #[derive(Debug, Clone, Copy)]

14. struct Length<Unit>(f64, PhantomData<Unit>);

16. /// Le trait `Add` définit le comportement de l'opérateur `+`.

17. impl<Unit> Add for Length<Unit> {

18. type Output = Length<Unit>;

20. // La méthode add() renvoie une nouvelle instance de la

21. // structure `Length` contenant la somme.

22. fn add(self, rhs: Length<Unit>) -> Length<Unit> {

23. // L'opérateur `+` appelle l'implémentation

24. // du trait `Add` pour le type `f64`.

25. Length(self.0 + rhs.0, PhantomData)

26. }

27. }

29. fn main() {

30. // On initialise `one_foot` pour avoir un type générique fantôme `Inch`.

31. // Ce "fantôme" sert de marqueur et classe cette instance dans

32. // l'unité de mesure "Pied".

33. let one_foot:  Length<Inch> = Length(12.0, PhantomData);

34. // On initialise `one_meter` pour avoir un type générique fantôme `Mm`.

35. // Ce "fantôme" sert de marqueur et classe cette instance dans

36. // l'unité de mesure "Millimètre".

37. let one_meter: Length<Mm>   = Length(1000.0, PhantomData);

39. // L'opérateur `+` appelle la méthode `add()` que nous avons

40. // précédemment implémentée pour `Length<Unit>`.

41. // Maintenant que `Length` implémente le trait `Copy`, `add()` ne

42. // prend pas possession (ne consomme pas) `one_foot` et `one_meter` mais

43. // les copie dans `self` et `rhs`.

44. let two_feet = one_foot + one_foot;

45. let two_meters = one_meter + one_meter;

47. // L'addition fonctionne.

48. println!("one foot + one_foot = {:?} in", two_feet.0);

49. println!("one meter + one_meter = {:?} mm", two_meters.0);

51. // Les opérations illogiques échouent comme prévu:

52. // Erreur à la compilation: mismatched type.

53. // let one_feter = one_foot + one_meter;

54. }

Voir aussi

Le système d'emprunts, les restrictions, les énumérations, impl et self, la surcharge des opérateurs, le pattern ref, les traits et les tuples.

13 - Les contextes

Les contextes jouent un rôle important dans le fonctionnement du systèmes de propriété (ownership), d'emprunts (borrowing) et de durées de vie (lifetime). Ils témoignent de la validité (ou non) d'un emprunt, indiquent au compilateur lorsqu'une ressource peut être libérée et lorsqu'une variable est créée ou détruite.

13-1 - Le RAII

En Rust, les variables ne stockent pas seulement leurs données dans la pile : Elles sont responsables de leurs ressources (e.g. le conteneur Box<T> possède, alloue de la mémoire dans le tas). Rust imposant l'approche du RAII, lorsqu'un objet sort du contexte, son destructeur est appelé et les ressources, possédées par l'objet, sont libérées.

Ce fonctionnement prévient les problèmes de fuites mémoire et nous dispense donc de gérer manuellement la mémoire. Voici un exemple :

1. // Dans le fichier raii.rs

2. fn create_box() {

3. // Allocation d'un entier dans le tas.

4. let _box1 = Box::new(3i32);

6. // `_box1` est détruit ici, et la mémoire est libérée.

7. }

9. fn main() {

10. // Allocation d'un entier dans le tas.

11. let _box2 = Box::new(5i32);

13. // Contexte imbriqué:

14. {

15. // Allocation d'un entier dans le tas.

16. let _box3 = Box::new(4i32);

18. // `_box3` est détruit ici, et la mémoire est libérée.

19. }

21. // On créé ici un grand nombre de "box" juste pour l'exemple.

22. // Il n'y a pas besoin de libérer manuellement la mémoire !

23. for _ in 0u32..1_000 {

24. create_box();

25. }

27. // `_box2` est détruit ici, et la mémoire est libérée.

28. }

Bien entendu, vous pouvez vérifier par vous-même si des fuites sont présentes en utilisant valgrind :

1. $ rustc raii.rs && valgrind ./raii

2. ==26873== Memcheck, a memory error detector

3. ==26873== Copyright (C) 2002-2013, and GNU GPL′d, by Julian Seward et al.

4. ==26873== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info

5. ==26873== Command: ./raii

6. ==26873==

7. ==26873==

8. ==26873== HEAP SUMMARY:

9. ==26873==     in use at exit: 0 bytes in 0 blocks

10. ==26873==   total heap usage: 1,013 allocs, 1,013 frees, 8,696 bytes allocated

11. ==26873==

12. ==26873== All heap blocks were freed -- no leaks are possible

13. ==26873==

14. ==26873== For counts of detected and suppressed errors, rerun with: -v

15. ==26873== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

Voir aussi

Box.

13-2 - Ownership et transferts

Parce que les variables sont responsables de la libération de leurs ressources, les ressources ne peuvent avoir qu'un seul propriétaire/responsable.

Cette règle évite également au développeur de libérer plus d'une fois une ressource. Notez toutefois que toutes les variables ne possèdent pas leurs propres ressources (e.g. les références).

Lorsque nous assignons quelque chose à une variable (let x = y) ou passons un (ou des) argument à une fonction par valeur (foo(x)), l'ownership des ressources est transféré. Dans le jargon, cette action est nommée « move » (transfert).

Après avoir transféré une ressource, l'ancien propriétaire ne peut plus être utilisé. Cela prévient la création de « dangling pointers » (i.e. des pointeurs sur une ressource qui n'est plus valide).

1. // Cette fonction prend possession d'un entier alloué dans le tas.

2. fn destroy_box(c: Box<i32>) {

3. println!("Destroying a box that contains {}", c);

5. // `c` est détruit et la mémoire va être libérée.

6. }

8. fn main() {

9. // Entier alloué dans la pile.

10. let x = 5u32;

12. // On copie `x` dans `y` - aucune ressource n'a été transféré

13. // (l'ownership n'a pas été transféré).

14. let y = x;

16. // Les deux valeurs peuvent être utilisées indépendamment.

17. println!("x is {}, and y is {}", x, y);

19. // `a` est un pointeur sur un entier alloué dans le tas.

20. let a = Box::new(5i32);

22. println!("a contains: {}", a);

24. // On transfert `a` dans `b`.

25. let b = a;

26. // L'adresse du pointeur `a` est copié (et non la donnée) dans `b`.

27. // `a` et `b` sont désormais des pointeurs sur la même donnée allouée dans le

28. // tas, mais `b` la possède, désormais.

30. // Erreur! `a` ne peut plus accéder à la donnée car il ne possède plus

31. // le bloc mémoire.

32. // println!("a contains: {}", a);

33. // TODO ^ Essayez de décommenter cette ligne.

35. // Cette fonction prend possession de la mémoire allouée dans le tas

36. // à partir de `b`.

37. destroy_box(b);

39. // Puisque la mémoire allouée a été libérée à partir d'ici,

40. // cette action consisterait à déréférencer de la mémoire libérée,

41. // mais cela est interdit par le compilateur.

42. // Erreur! `b` ne peut plus accéder à la donnée car il ne possède plus

43. // le bloc mémoire.

44. // println!("b contains: {}", b);

45. // TODO ^ Essayez de décommenter cette ligne.

46. }

13-2-1 - Mutabilité

La mutabilité d'une donnée peut être altérée lorsque l'ownership est transféré.

1. fn main() {

2. let immutable_box = Box::new(5u32);

4. println!("immutable_box contains {}", immutable_box);

6. // Erreur: `immutable_box` ne peut pas être déréférencé.

7. // *immutable_box = 4;

9. // On créé une copie mutable de `immutable_box`.

10. let mut mutable_box = immutable_box;

12. println!("mutable_box contains {}", mutable_box);

14. // On modifie le contenu de la box.

15. *mutable_box = 4;

17. println!("mutable_box now contains {}", mutable_box);

18. }

13-3 - Système d'emprunts

Très souvent, nous souhaiterions accéder à une ressource sans en prendre possession. Pour ce faire, Rust utilise un système d'emprunts. Plutôt que de passer un objet par valeur (T), il peut être passé par référence (&T).

Le compilateur garantit (grâce au vérificateur d'emprunts) que les références sont toujours valides. Tant qu'une référence de l'objet existe, il ne sera pas détruit.

1. // Cette fonction prend possession d'une box et la détruit.

2. fn eat_box_i32(boxed_i32: Box<i32>) {

3. println!("Destroying box that contains {}", boxed_i32);

4. }

6. // Cette fonction emprunte un entier signé

7. // codé sur 32 bits.

8. fn borrow_i32(borrowed_i32: &i32) {

9. println!("This int is: {}", borrowed_i32);

10. }

12. fn main() {

13. // On créé entier alloué dans le tas

14. // et un autre alloué dans la pile.

15. let boxed_i32 = Box::new(5_i32);

16. let stacked_i32 = 6_i32;

18. // `borrow_i32()` emprunte le contenu de la box.

19. // La fonction ne prend pas possession des ressources,

20. // donc le contenu peut être de nouveau emprunté.

21. borrow_i32(&boxed_i32);

22. borrow_i32(&stacked_i32);

24. {

25. // Récupère une référence de l'entier contenu dans la box.

26. let _ref_to_i32: &i32 = &boxed_i32;

28. // Erreur!

29. // Vous ne pouvez pas détruire `boxed_i32` tant que la valeur

30. // qu'il contient est empruntée.

31. // eat_box_i32(boxed_i32);

32. // FIXME ^ Essayez de décommenter cette ligne.

34. // La durée de vie de `_ref_to_i32` prend fin ici,

35. // l'entier n'est donc plus emprunté.

36. }

38. // `boxed_i32` peut désormais être possédé par `eat_box()` et peut donc

39. // être détruit.

40. eat_box_i32(boxed_i32);

41. }

13-3-1 - Mutabilité

Une ressource mutable peut être modifiée, tout en étant empruntée, en utilisant &mut T. Nous passons alors une référence mutable de la ressource, donnant un accès en lecture et en écriture à l'emprunteur. En revanche, une référence dont la mutabilité n'est pas précisée (i.e. que le mot-clé mut n'est pas présent) empêche l'emprunteur d'accéder en écriture à la ressource.

1. #[allow(dead_code)]

2. #[derive(Clone, Copy)]

3. struct Book {

4. // `&'static str` est une référence d'une chaîne de caractères allouée

5. // dans un bloc mémoire qui ne peut être accédé qu'en lecture.

6. author: &'static str,

7. title: &'static str,

8. year: u32,

9. }

11. // Cette fonction prend une référence d'une instance

12. // de la structure `Book` en paramètre.

13. fn borrow_book(book: &Book) {

14. println!("I immutably borrowed {} - {} edition", book.title, book.year);

15. }

17. // Cette fonction prend une référence mutable d'une instance de la

18. // structure `Book` en paramètre, et initialise sa date de publication

19. // à 2014.

20. fn new_edition(book: &mut Book) {

21. book.year = 2014;

22. println!("I mutably borrowed {} - {} edition", book.title, book.year);

23. }

25. fn main() {

26. // On créé une instance immuable de la

27. // structure `Book` nommée `immutabook`.

28. let immutabook = Book {

29. // Les chaînes littérales sont typées `&'static str`.

30. author: "Douglas Hofstadter",

31. title: "Gödel, Escher, Bach",

32. year: 1979,

33. };

35. // On créé une copie mutable de `immutabook` nommée

36. // `mutabook`.

37. let mut mutabook = immutabook;

39. // Emprunte un objet en lecture seule.

40. borrow_book(&immutabook);

42. // Emprunte un objet en lecture seule.

43. borrow_book(&mutabook);

45. // Récupère une référence mutable d'un objet mutable.

46. new_edition(&mut mutabook);

48. // Erreur! Vous ne pouvez pas récupérer une référence

49. // mutable d'un objet immuable.

50. // new_edition(&mut immutabook);

51. // FIXME ^ Décommentez cette ligne.

52. }

Voir aussi

La lifetime 'static.

13-3-2 - Verrouillage des ressources

Lorsqu'une référence immuable d'une ressource est récupérée, cette dernière est également « gelée », « verrouillée ». Lorsqu'une ressource est gelée, l'objet initial (celui à partir duquel une référence a été récupérée) ne peut être modifié jusqu'à ce que toutes les références soient détruites(i.e. ne figurent plus dans le contexte).

1. fn main() {

2. let mut _mutable_integer = 7i32;

4. {

5. // On emprunte `_mutable_integer`.

6. // Accès en lecture uniquement.

7. let _large_integer = &_mutable_integer;

9. // Erreur! `_mutable_integer` est gelé dans ce contexte

10. // (i.e. la ressource est empruntée).

11. // _mutable_integer = 50;

12. // FIXME ^ Décommentez/commentez cette ligne.

14. // On sort du contexte de `_large_integer`.

15. }

17. // Aucun problème! `_mutable_integer` n'est plus gelé dans ce contexte.

18. _mutable_integer = 3;

19. }

13-3-3 - Limitations des accès lecture/écriture

Il est possible de faire autant « d'emprunts immuables » (i.e. récupérer une référence immuable) qu'on le souhaite. Toutefois, tant qu'il y a des accès en lecture par référence, l'objet original ne peut pas être modifié. Par contre, un seul accès en écriture, par ressource, est permis tant que le dernier emprunt par référence mutable n'est pas terminé (i.e. tant que la référence mutable est toujours dans le contexte).

1. struct Point { x: i32, y: i32, z: i32 }

3. fn main() {

4. let mut point = Point { x: 0, y: 0, z: 0 };

6. {

7. let borrowed_point = &point;

8. let another_borrow = &point;

10. // Vous pouvez accéder à la ressource par le biais des références

11. // créées et par l'objet original.

12. println!("Point has coordinates: ({}, {}, {})",

13. borrowed_point.x, another_borrow.y, point.z);

15. // Erreur! Vous ne pouvez pas avoir des accès en écriture à une ressource

16. // qui est déjà empruntée par une référence immuable (i.e. accès en lecture).

17. // let mutable_borrow = &mut point;

18. // TODO ^ Essayez de décommenter cette ligne.

20. // On sort du contexte des références

21. // immuables.

22. }

24. {

25. let mutable_borrow = &mut point;

27. // On modifie la ressource par le biais d'une

28. // référence mutable.

29. mutable_borrow.x = 5;

30. mutable_borrow.y = 2;

31. mutable_borrow.z = 1;

33. // Erreur! Vous ne pouvez pas accéder à `point` en lecture

34. // alors que la ressource est potentiellement en train d'être modifiée.

35. // let y = &point.y;

36. // TODO ^ Essayez de décommenter cette ligne.

38. // Erreur! On ne peut pas afficher `point.z` en utilisant

39. // la macro `println!` car elle prend en paramètre une référence

40. // immuable.

41. // println!("Point Z coordinate is {}", point.z);

42. // TODO ^ Essayez de décommenter cette ligne.

44. // Ok! Les références mutables peuvent être passées comme

45. // références immuables à `println!`.

46. println!("Point has coordinates: ({}, {}, {})",

47. mutable_borrow.x, mutable_borrow.y, mutable_borrow.z);

49. // On sort du contexte des références

50. // mutables.

51. }

53. // Les accès en lecture sur `point` sont de nouveau

54. // permis par le vérificateur d'emprunts.

55. let borrowed_point = &point;

56. println!("Point now has coordinates: ({}, {}, {})",

57. borrowed_point.x, borrowed_point.y, borrowed_point.z);

58. }

13-3-4 - Le pattern ref

Lorsque vous vous servez du pattern matching ou de la déstructuration dans une assignation (let), le mot-clé ref peut être utilisé pour récupérer une référence d'un (ou plusieurs) champ d'une structure et/ou d'un tuple. Le code ci-dessous propose des exemples où ce modèle peut être utile :

1. #[derive(Clone, Copy)]

2. struct Point { x: i32, y: i32 }

4. fn main() {

5. let c = 'Q';

7. // Un emprunt effectué avec le mot-clé `ref` (placé dans la l-value)

8. // est équivalent à une assignation "C-like" avec le `&` (placé dans la r-value).

9. let ref ref_c1 = c;

10. let ref_c2 = &c;

12. println!("ref_c1 equals ref_c2: {}", *ref_c1 == *ref_c2);

14. let point = Point { x: 0, y: 0 };

16. // `ref` peut également être utilisé lors de la déstructuration

17. // d'une structure (aussi valable pour les tuples(structures et littéraux)).

18. let _copy_of_x = {

19. // `ref_to_x` est une référence du champ `x` contenu dans

20. // l'instance `point`.

21. let Point { x: ref ref_to_x, y: _ } = point;

23. // Renvoie une copie du champ `x` de l'instance `point`.

24. *ref_to_x

25. };

27. // Copie mutable de l'instance `point`.

28. let mut mutable_point = point;

30. {

31. // `ref` peut être combiné avec `mut` pour récupérer

32. // une référence mutable.

33. let Point { x: _, y: ref mut mut_ref_to_y } = mutable_point;

35. // On modifie le champ `y` de l'instance `mutable_point` par

36. // le biais de la référence mutable qu'on a récupéré.

37. *mut_ref_to_y = 1;

38. }

40. println!("point is ({}, {})", point.x, point.y);

41. println!("mutable_point is ({}, {})", mutable_point.x, mutable_point.y);

43. // Un tuple mutable qui contient un pointeur et un entier non-signé

44. // codé sur 32 bits.

45. let mut mutable_tuple = (Box::new(5u32), 3u32);

47. {

48. // Nous déstructurons le tuple `mutable_tuple` pour modifier

49. // la valeur de `last` (dernière élément indexé par le tuple).

50. let (_, ref mut last) = mutable_tuple;

51. *last = 2u32;

52. // `_` est ajouté dans la valeur de gauche car on ne souhaite

53. // pas en récupérer le contenu.

54. }

56. println!("tuple is {:?}", mutable_tuple);

57. }

13-4 - Système de durée de vie

Une « lifetime » est une construction utilisée par le compilateur pour s'assurer que tous les emprunts (i.e. références immuables/mutables) sont valides. Plus précisément, la durée de vie d'une variable commence lorsqu'elle est créée et se termine une fois cette dernière détruite.

Note : Bien que les lifetimes et les contextes soient très souvent cités ensembles cela reste, malgré tout, deux concepts bien distincts.

1. // La création et destruction des lifetimes sont illustrées ci-dessous par des lignes.

2. // `i` possède la plus grande lifetime car son contexte englobe

3. // `borrow1` et `borrow2`. La durée de vie de `borrow1` ne peut pas être

4. // comparée à celle de `borrow2` car elle ne se trouve pas dans le

5. // même contexte.

6. fn main() {

7. let i = 3; // Lifetime for `i` starts. ────────────────┐

8. //                                                     │

9. { //                                                   │

10. let borrow1 = &i; // `borrow1` lifetime starts. ──┐│

11. //                                                ││

12. println!("borrow1: {}", borrow1); //              ││

13. } // `borrow1 ends. ──────────────────────────────────┘│

14. //                                                     │

15. //                                                     │

16. { //                                                   │

17. let borrow2 = &i; // `borrow2` lifetime starts. ──┐│

18. //                                                ││

19. println!("borrow2: {}", borrow2); //              ││

20. } // `borrow2` ends. ─────────────────────────────────┘│

21. //                                                     │

22. }   // Lifetime ends. ─────────────────────────────────────┘

Notez qu'aucun nom ou type n'est assigné aux labels de lifetimes. Nous allons apprendre, dans la prochaine section, à nous servir des durées de vie (et appréhender l'utilisation des labels).

13-4-1 - Les labels

Le compilateur se sert des « annotations explicites » (labels) pour déterminer la durée de validité d'une référence. Dans le cas où les lifetimes ne peuvent pas être omises (note : Elles peuvent l'être grâce au concept d'élision), Rust dispose d'annotations explicites (labels) pour déterminer quelle devrait être la durée de vie d'une référence. Les labels sont précédés d'une 'apostrophe :

foo<'a>
// `foo` dispose de la durée de vie `'a`.

Tout comme les closures, pour utiliser les labels, vous devrez avoir recours à la généricité. De plus, cette syntaxe permet de préciser que la durée de vie de foo ne peut pas excéder celle de 'a.

Voici la syntaxe des labels lorsqu'ils sont appliqués à un type : &'a T (ou &'a mut T) où 'a est un label déclaré près de l'identificateur de la fonction.

Si vous devez déclarer plusieurs lifetimes, la syntaxe est tout aussi simple :

foo<'a, 'b>
// `foo` dispose des lifetimes `'a` et `'b`.

Dans ce cas, la durée de vie de foo ne peut pas excéder la lifetime 'a ou 'b.

1. // `print_refs` prend deux références d'entiers possédant une durée de

2. // vie différente chacune (i.e. `'a` et `'b`). Ces deux lifetimes doivent être

3. // au moins aussi longues que celle de la fonction `print_refs`.

4. fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {

5. println!("x is {} and y is {}", x, y);

6. }

8. // Une fonction qui ne prend aucun argument, mais qui déclare quand même

9. // une lifetime `'a`.

10. fn failed_borrow<'a>() {

11. let _x = 12;

13. // ERREUR: `_x` ne vit pas assez longtemps.

14. // let y: &'a i32 = &_x;

15. // Tenter d'utiliser la lifetime `'a` sur une ressource au sein de la fonction

16. // ne fonctionnera pas car la durée de vie de `&_x` est plus courte que celle

17. // de `y` .

18. // Une courte durée de vie ne peut être rallongée en cours de route.

19. }

21. fn main() {

22. // On crée ces variables pour les emprunter

23. // un peu plus bas.

24. let (four, nine) = (4, 9);

26. // `print_refs` emprunte (`&`) les deux ressources précédemment

27. // créées.

28. print_refs(&four, &nine);

29. // Une ressource empruntée doit survivre à la fonction qui l'emprunte.

30. // Autrement dit, la durée de vie de `four` et `nine` doit être

31. // plus longue que celle de `print_refs`.

33. failed_borrow();

35. // `failed_borrow` ne contient aucune référence qui force la lifetime `'a` à

36. // être plus longue que celle de la fonction, mais `'a` reste plus longue.

37. // Parce que la durée de vie d'une ressource (empruntée) n'est jamais imposée,

38. // la durée de vie par défaut est `'static`.

39. }

Voir aussi

La généricité et les closures.

13-4-2 - Les fonctions

Lorsque le concept d'élision ne peut pas être appliqué, les signatures de fonctions comportant des (labels de) lifetimes sont régies par quelques règles :

1. Toute référence doit être annotée d'une lifetime ;

2. Toute référence renvoyée doit posséder la même lifetime qu'en entrée ou la lifetime 'static.

Notez également que, si une fonction n'a pas de références en entrée, le renvoi de références est interdit car cela pourrait conduire à l'utilisation de ressources invalides.

1. // La référence passée en paramètre possédant la lifetime `'a`

2. // doit vivre au moins aussi longtemps que le fonction.

3. fn print_one<'a>(x: &'a i32) {

4. println!("`print_one`: x is {}", x);

5. }

7. // L'utilisation des références mutables est également possible

8. // avec les lifetimes.

9. fn add_one<'a>(x: &'a mut i32) {

10. *x += 1;

11. }

13. // Plusieurs entrées avec différentes lifetimes. Dans ce cas,

14. // il serait plus simple, pour les deux paramètres, d'avoir

15. // la même durée de vie (`'a`), mais dans des cas plus délicats,

16. // plusieurs lifetimes peuvent être requises.

17. fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {

18. println!("`print_multi`: x is {}, y is {}", x, y);

19. }

21. // Renvoyer des références qui ont été passées en paramètre est légal.

22. // Toutefois, veillez à renvoyer la bonne lifetime.

23. fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }

25. // fn invalid_output<'a>() -> &'a i32 { &7 }

26. // La déclaration ci-dessus est invalide: `'a` doit, au moins,

27. // survivre à la fonction. Ici, `&7` créerait un entier et récupérerait

28. // sa référence. La ressource serait alors perdue une fois l'exécution

29. // de la fonction terminée, renvoyant une référence d'une ressource qui n'existe plus.

31. fn main() {

32. let x = 7;

33. let y = 9;

35. print_one(&x);

36. print_multi(&x, &y);

38. let z = pass_x(&x, &y);

39. print_one(z);

41. let mut t = 3;

42. add_one(&mut t);

43. print_one(&t);

44. }

Voir aussi

Les fonctions.

13-4-3 - Les méthodes

Les méthodes sont annotées de la même manière que les fonctions :

1. struct Owner(i32);

3. impl Owner {

4. // On déclare et annote les lifetimes comme

5. // dans une fonction traditionnelle.

6. fn add_one<'a>(&'a mut self) { self.0 += 1; }

7. fn print<'a>(&'a self) {

8. println!("`print`: {}", self.0);

9. }

10. }

12. fn main() {

13. let mut owner  = Owner(18);

15. owner.add_one();

16. owner.print();

17. }

Voir aussi

Les méthodes.

13-4-4 - Les structures

La déclaration des labels pour les structures ne diffère pas beaucoup non plus de celle des fonctions :

1. // On créé un type nommé `Borrowed` qui a pour attribut

2. // une référence d'un entier codé sur 32 bits. La référence

3. // doit survivre à l'instance de la structure `Borrowed`.

4. #[derive(Debug)]

5. struct Borrowed<'a>(&'a i32);

7. // Même combat, ces deux références doivent survivre à l'instance

8. // (ou aux instances)  de la structure `NamedBorrowed`.

9. #[derive(Debug)]

10. struct NamedBorrowed<'a> {

11. x: &'a i32,

12. y: &'a i32,

13. }

15. // On créé une énumération qui contient deux variantes :

16. // 1. Un tuple qui prend en entrée un entier codé sur 32 bits;

17. // 2. Un tuple qui prend en entrée une référence d'un `i32`.

18. #[derive(Debug)]

19. enum Either<'a> {

20. Num(i32),

21. Ref(&'a i32),

22. }

24. fn main() {

25. let x = 18;

26. let y = 15;

28. let single = Borrowed(&x);

29. let double = NamedBorrowed { x: &x, y: &y };

30. let reference = Either::Ref(&x);

31. let number    = Either::Num(y);

33. println!("x is borrowed in {:?}", single);

34. println!("x and y are borrowed in {:?}", double);

35. println!("x is borrowed in {:?}", reference);

36. println!("y is *not* borrowed in {:?}", number);

37. }

Voir aussi

Les structures.

13-4-5 - Les restrictions

Tout comme les types génériques, les lifetimes (elles-mêmes génériques) utilisent les restrictions. Ici, le caractère : a une signification quelque peu différente, mais + possède la même fonction. La déclaration se lit comme suit :

1. T: 'a : Toutes les références dans le type T doivent, au moins, survivre à la lifetime 'a ;

2. T: Trait + 'a : Le type T doit implémenter le trait Trait et toutes les références doivent survivre à la lifetime 'a.

L'exemple ci-dessous illustre les explications précédentes :

1. use std::fmt::Debug; // On importe le trait `Debug`.

2. // Trait avec lequel nous allons filtrer les entrées

3. // des fonctions génériques.

5. #[derive(Debug)]

6. struct Ref<'a, T: 'a>(&'a T);

7. // `Ref` contient la référence d'un type générique `T` qui possède

8. // une lifetime inconnue nommée `'a`. Toutes les références contenues par

9. // le type `T` sont contraintes à survivre à la lifetime `'a`. De plus,

10. // la durée de vie de `Ref` ne peut pas excéder la lifetime `'a`.

12. // Une fonction générique qui affiche le résultat de l'utilisation

13. // du trait `Debug`.

14. fn print<T>(t: T) where

15. T: Debug {

16. println!("`print`: t is {:?}", t);

17. }

19. // Ici, nous avons une référence de type `T` en entrée où

20. // `T` implémente le trait `Debug` et toutes les références

21. // contenues par le type `T` survivent à la lifetime `'a`. La lifetime

22. // `'a` doit survivre à l'appel de la fonction.

23. fn print_ref<'a, T>(t: &'a T) where

24. T: Debug + 'a {

25. println!("`print_ref`: t is {:?}", t);

26. }

28. fn main() {

29. let x = 7;

30. let ref_x = Ref(&x);

32. print_ref(&ref_x);

33. print(ref_x);

34. }

Voir aussi

La généricité, les restrictions et les restrictions multiples.

13-4-6 - La coercition

Une longue durée de vie peut être raccourcie afin d'opérer dans un contexte où elle ne fonctionnerait pas en temps normal. Cette opération peut être effectuée par inférence du compilateur ou en déclarant une différence de durée de vie(e.g. l'une est, au moins, aussi longue que l'autre, sinon plus).

1. // Ici, Rust va inférer une durée de vie aussi courte que possible.

2. // Les deux références sont alors assignées à cette durée de vie.

3. fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {

4. first * second

5. }

7. // `<'a: 'b, 'b>` se lit comme suit: La lifetime `'a` est au moins aussi longue

8. // que `'b`. Ici, nous prenons en entrée un entier soumis à la lifetime

9. // `&'a i32` et renvoyons un entier (le même entier, en fait) soumis à la lifetime

10. // `&'b i32` comme résultat de la coercition.

11. // Note: Nous pouvons renvoyer l'entier taggé avec la lifetime `'b` car `'a` et `'b`

12. // ont une durée de vie aussi longue, pour le temps de l'exécution de la fonction,

13. // tout du moins.

14. fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {

15. first

16. }

18. fn main() {

19. let first = 2; // Longue lifetime.

21. {

22. let second = 3; // Courte lifetime.

24. // Note: Ici `first` et `second` possèdent la même durée de vie.

25. println!("The product is {}", multiply(&first, &second));

26. println!("{} is the first", choose_first(&first, &second));

27. };

28. }

13-4-7 - La lifetime 'static

Une ressource annotée du label 'static possède la plus longue durée de vie qu'on puisse assigner. La durée de vie de 'static est égale à celle du programme lui-même et peut également être raccourcie selon le contexte.

Il y a deux façons d'assigner la lifetime 'static, et toutes deux stockeront la ressource directement dans le binaire en lecture seule :

1. Créer une constante avec la déclaration static ;

2. Créer une chaîne de caractères avec le "littéral" en l'annotant du type &'static str.

Voici un exemple pour illustrer les deux manières de faire :

1. // On créé une constante avec la lifetime `'static` en utilisant

2. // le mot-clé `static`.

3. static NUM: i32 = 18;

5. // Renvoie une référence de `NUM` où sa lifetime `'static`

6. // est obligée de s'aligner avec la durée de vie du paramètre

7. // passé à la fonction.

8. fn coerce_static<'a>(_: &'a i32) -> &'a i32 {

9. &NUM

10. }

12. fn main() {

13. {

14. // On créé une chaîne de caractères littérale, primitive

15. // et on l'affiche.

16. let static_string = "I'm in read-only memory";

17. println!("static_string: {}", static_string);

19. // Lorsque `static_string` sortira du contexte, la référence

20. // ne pourra plus être utilisée, mais la ressource restera

21. // présente dans le binaire.

22. }

24. {

25. // On créé un entier à passer à la

26. // fonction `coerce_static`:

27. let lifetime_num = 9;

29. // On aligne, adapte la durée de vie de `NUM à

30. // celle de `lifetime_num`:

31. let coerced_static = coerce_static(&lifetime_num);

33. println!("coerced_static: {}", coerced_static);

34. }

36. println!("NUM: {} stays accessible!", NUM);

37. }

Voir aussi

Les constantes.

13-4-8 - Annotation implicite

En règle générale, décrire explicitement la durée de vie d'une référence n'est pas nécessaire et nous préférerons passer la main au compilateur, qui se chargera de nous épargner l'écriture des annotations et d'améliorer la lisibilité du code. Ce processus d'annotation implicite se nomme l'élision. Il s'agit ici d'omettre volontairement les annotations pour que le compilateur le fasse à notre place. L'élision ne peut être appliquée que lorsqu'un pattern de durée de vie est commun, simple à deviner.

Le code source qui suit présente quelques exemples où nous avons volontairement omis les annotations. Pour une description plus exhaustive du concept d'élision, n'hésitez pas à consulter la section lifetime elision du livre.

1. // `elided_input` et `annotated_input` ont fondamentalement la même signature,

2. // sauf que la lifetime de l'entrée de la fonction `elided_input` a été omise

3. // et ajoutée par le compilateur.

4. fn elided_input(x: &i32) {

5. println!("`elided_input`: {}", x)

6. }

8. fn annotated_input<'a>(x: &'a i32) {

9. println!("`annotated_input`: {}", x)

10. }

12. // Même combat, `elided_pass` et `annotated_pass` possèdent la même signature

13. // sauf que la lifetime de `x`, pour la fonction `elided_pass`, a été omise

14. // et ajoutée par le compilateur. Annotation implicite.

15. fn elided_pass(x: &i32) -> &i32 { x }

17. fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x }

19. fn main() {

20. let x = 3;

22. elided_input(&x);

23. annotated_input(&x);

25. println!("`elided_pass`: {}", elided_pass(&x));

26. println!("`annotated_pass`: {}", annotated_pass(&x));

27. }

Voir aussi

Le chapitre du livre sur l'élision.

14 - Les traits

Un trait est un agrégat de méthodes définies pour un type inconnu : Self. Elles peuvent accéder aux autres méthodes déclarées dans le même trait.

Les traits peuvent être implémentés pour n'importe quel type de donnée. Dans l'exemple ci-dessous, nous définissons Animal, un groupe de méthodes. Le traitAnimal est alors implémenté pour le type Sheep, permettant l'utilisation des méthodes de Animal avec une instance du type Sheep.

1. struct Sheep { naked: bool, name: &'static str }

3. trait Animal {

4. // Méthode statique; `Self` fait référence au type ayant implémenté

5. // le trait.

6. fn new(name: &'static str) -> Self;

8. // Méthode d'instance; Elles renverront une chaîne de caractères.

9. fn name(&self) -> &'static str;

10. fn noise(&self) -> &'static str;

12. // Les traits peuvent fournir une implémentation par défaut.

13. fn talk(&self) {

14. println!("{} says {}", self.name(), self.noise());

15. }

16. }

18. impl Sheep {

19. fn is_naked(&self) -> bool {

20. self.naked

21. }

23. fn shear(&mut self) {

24. if self.is_naked() {

25. // Les méthodes de `Self` peuvent utiliser les méthodes déclarées

26. // par le trait.

27. println!("{} is already naked...", self.name());

28. } else {

29. println!("{} gets a haircut!", self.name);

31. self.naked = true;

32. }

33. }

34. }

36. // Implémentation des services du trait `Animal`

37. // pour le type `Sheep`.

38. impl Animal for Sheep {

39. // En l'occurrence, `Self` fait référence à `Sheep`.

40. fn new(name: &'static str) -> Sheep {

41. Sheep { name: name, naked: false }

42. }

44. fn name(&self) -> &'static str {

45. self.name

46. }

48. fn noise(&self) -> &'static str {

49. if self.is_naked() {

50. "baaaaah?"

51. } else {

52. "baaaaah!"

53. }

54. }

56. // L'implémentation par défaut fournie par le trait

57. // peut être réécrite.

58. fn talk(&self) {

59. // Par exemple, nous pourrions fournir une description plus précise.

60. println!("{} pauses briefly... {}", self.name, self.noise());

61. }

62. }

64. fn main() {

65. // Typer l'identificateur est nécessaire dans ce cas de figure.

66. let mut dolly: Sheep = Animal::new("Dolly");

67. // TODO ^ Essayez de supprimer le type annoté.

69. dolly.talk();

70. dolly.shear();

71. dolly.talk();

72. }

14-1 - L'attribut Derive

Le compilateur est capable de fournir de simples implémentations pour cetains traits par le biais de l'attribut#[derive]. Ces traits peuvent toujours être implémentés manuellement si un traitement plus complexe est attendu.

Voici une liste de traits pouvant être dérivés :

- Les traits de comparaison : Eq, PartialEq, Ord, PartialOrd;

- Clone, pour créer une instance (T) à partir d'une référence (&T) par copie ;

- Copy, pour permettre à un type d'être copié plutôt que transféré ;

- Hash, pour générer un hash depuis &T ;

- Debug, pour formater une valeur en utilisant le formatteur {:?}.

1. // `Centimeters` est un tuple qui peut être comparé.

2. #[derive(PartialEq, PartialOrd)]

3. struct Centimeters(f64);

5. // `Inches` est un tuple qui peut être affiché.

6. #[derive(Debug)]

7. struct Inches(i32);

9. impl Inches {

10. fn to_centimeters(&self) -> Centimeters {

11. let &Inches(inches) = self;

13. Centimeters(inches as f64 * 2.54)

14. }

15. }

17. // `Seconds` est un tuple ne possédant aucun attribut.

18. struct Seconds(i32);

20. fn main() {

21. let _one_second = Seconds(1);

23. // Erreur: `Seconds` ne peut pas être affiché; Il n'implémente pas le trait `Debug`.

24. // println!("One second looks like: {:?}", _one_second);

25. // TODO ^ Essayez de décommenter cette ligne.

27. // Erreur: `Seconds` ne peut pas être comparé; Il n'implémente pas le trait `PartialEq`.

28. // let _this_is_true = (_one_second == _one_second);

29. // TODO ^ Essayez de décommenter cette ligne.

31. let foot = Inches(12);

33. println!("One foot equals {:?}", foot);

35. let meter = Centimeters(100.0);

37. let cmp =

38. if foot.to_centimeters() < meter {

39. "smaller"

40. } else {

41. "bigger"

42. };

44. println!("One foot is {} than one meter.", cmp);

45. }

Voir aussi

Derive.

14-2 - La surcharge des opérateurs

Avec Rust, nombre d'opérateurs peuvent être surchargés via les traits. En d'autres termes, des opérateurs peuvent être utilisés pour accomplir différentes tâches en fonction des arguments passés en entrée. Cette manipulation est possible parce que les opérateurs sont des sucres syntaxes visant à masquer l'appel des méthodes liées à ces derniers. Par exemple, l'opérateur + dans l'expression a + b appelle la méthode add (a.add(b)). La méthode add appartient au trait Add, d'où l'utilisation de l'opérateur + par tous les types implémentant le trait.

Vous pouvez retrouver la liste des traits surchargeant des opérateurs [ici][operators].

1. use std::ops;

3. struct Foo;

4. struct Bar;

6. #[derive(Debug)]

7. struct FooBar;

9. #[derive(Debug)]

10. struct BarFoo;

12. // Le trait `std::ops::Add` est utilisé pour permettre la surcharge de `+`.

13. // Ici, nous spécifions `Add<Bar>` - cette implémentation sera appelée si l'opérande de droite est

14. // de type `Bar`.

15. // Le bloc ci-dessous implémente l'opération : `Foo + Bar = FooBar`.

16. impl ops::Add<Bar> for Foo {

17. type Output = FooBar;

19. fn add(self, _rhs: Bar) -> FooBar {

20. println!("> Foo.add(Bar) was called");

22. FooBar

23. }

24. }

26. // En inversant les types, nous nous retrouvons à implémenter une addition non-commutative.

27. // Ici, nous spécifions `Add<Foo>` - cette implémentation sera appelée si l'opérande de droite

28. // est de type `Foo`.

29. // Le bloc ci-dessous implémente l'opération: `Bar + Foo = BarFoo`.

30. impl ops::Add<Foo> for Bar {

31. type Output = BarFoo;

33. fn add(self, _rhs: Foo) -> BarFoo {

34. println!("> Bar.add(Foo) was called");

36. BarFoo

37. }

38. }

40. fn main() {

41. println!("Foo + Bar = {:?}", Foo + Bar);

42. println!("Bar + Foo = {:?}", Bar + Foo);

43. }

Voir aussi

Add, index de la syntaxe.

14-3 - Le trait Drop

Le trait Drop ne possède qu'une seule méthode : drop ; cette dernière est automatiquement appelée lorsqu'un objet sort du contexte. La fonction principale du trait Drop est de libérer les ressources que les instances, du type ayant implémenté le trait, possèdent.

Box, Vec, String, File et Process sont autant d'exemples de types qui implémentent le trait Drop pour libérer leurs ressources. Le trait Drop peut également être implémenté manuellement pour répondre aux besoins de vos propres types.

Dans l'exemple suivant, nous affichons quelque chose dans la console à partir de la méthode drop pour notifier chaque appel.

1. struct Droppable {

2. name: &'static str,

3. }

5. // Implémentation basique de `drop` qui affiche un message dans la console.

6. impl Drop for Droppable {

7. fn drop(&mut self) {

8. println!("> Dropping {}", self.name);

9. }

10. }

12. fn main() {

13. let _a = Droppable { name: "a" };

15. // block A

16. {

17. let _b = Droppable { name: "b" };

19. // block B

20. {

21. let _c = Droppable { name: "c" };

22. let _d = Droppable { name: "d" };

24. println!("Exiting block B");

25. }

26. println!("Just exited block B");

28. println!("Exiting block A");

29. }

30. println!("Just exited block A");

32. // La variable peut être libérée manuellement en utilisant la fonction `(std::mem::)drop`.

33. drop(_a);

34. // TODO ^ Essayez de commenter cette ligne.

36. println!("end of the main function");

38. // `_a` ne *sera pas* libérée une seconde fois ici puisque nous l'avons déjà fait

39. // plus haut, manuellement.

40. }

14-4 - Les itérateurs

Le trait Iterator est utilisé pour implémenter les itérateurs sur les collections tels que les tableaux.

Le trait nécessite seulement la définition d'une méthode pour l'élément next. Elle peut être implémentée manuellement en utilisant un bloc impl ou automatiquement (comme pour les tableaux et intervalles).

Pour les utilisations les plus communes, la boucle for peut convertir certaines collections en itérateurs en utilisant la méthode .into_iterator().

Les méthodes pouvant être appelées en utilisant le trait Iterator, en plus de celles présentées dans l'exemple ci-dessous, sont listées ici.

1. struct Fibonacci {

2. curr: u32,

3. next: u32,

4. }

6. // Implémentation de `Iterator` pour le type `Fibonacci`.

7. // Le trait `Iterator` nécessite l'implémentation d'une méthode seulement pour l'élément `next`.

8. impl Iterator for Fibonacci {

9. type Item = u32;

11. // Ici, nous définissons la séquence utilisant `.curr` et `.next`.

12. // Le type de renvoi est `Option<T>`:

13. //     * Lorsque l'`Iterator` est terminé, `None` est renvoyé;

14. //     * Autrement, la valeur suivante est enveloppé dans une instance `Some` et renvoyée.

15. fn next(&mut self) -> Option<u32> {

16. let new_next = self.curr + self.next;

18. self.curr = self.next;

19. self.next = new_next;

21. // Puisqu'il n'y a pas de limite à une suite de Fibonacci, l'`Iterator`

22. // ne renverra jamais `None`.

23. Some(self.curr)

24. }

25. }

27. // Renvoie un générateur de suites de Fibonacci.

28. fn fibonacci() -> Fibonacci {

29. Fibonacci { curr: 1, next: 1 }

30. }

32. fn main() {

33. // `0..3` est un `Iterator` qui génère: 0, 1, et 2 (i.e. intervalle `[0, 3[`).

34. let mut sequence = 0..3;

36. println!("Four consecutive `next` calls on 0..3");

37. println!("> {:?}", sequence.next());

38. println!("> {:?}", sequence.next());

39. println!("> {:?}", sequence.next());

40. println!("> {:?}", sequence.next());

43. // `for` parcourt un `Iterator` jusqu'à ce qu'il renvoie `None`.

44. // Chaque valeur contenue par un objet `Some` est libérée de son conteneur

45. // puis assignée à une variable (en l'occurrence, `i`).

46. println!("Iterate through 0..3 using `for`");

47. for i in 0..3 {

48. println!("> {}", i);

49. }

51. // La méthode `take(n)` réduit un `Iterator` à ces `n` premiers éléments.

52. println!("The first four terms of the Fibonacci sequence are: ");

53. for i in fibonacci().take(4) {

54. println!("> {}", i);

55. }

57. // La méthode `skip(n)` tronque les `n` premiers éléments d'un `Iterator`.

58. println!("The next four terms of the Fibonacci sequence are: ");

59. for i in fibonacci().skip(4).take(4) {

60. println!("> {}", i);

61. }

63. let array = [1u32, 3, 3, 7];

65. // La méthode `iter` construit un `Iterator` sur un(e) tableau/slice.

66. println!("Iterate the following array {:?}", &array);

67. for i in array.iter() {

68. println!("> {}", i);

69. }

70. }

14-5 - Le trait Clone

Lors du traitement des ressources, le comportement par défaut est de les transférer lors d'un assignement ou un appel de méthode. Cependant, il est parfois nécessaire d'effectuer une copie des ressources en question.

C'est exactement la fonction du trait Clone, qui nous permettra d'utiliser la méthode clone().

1. // Une structure unitaire sans ressources.

2. #[derive(Debug, Clone, Copy)]

3. struct Nil;

5. // Un tuple avec des ressources qui implémente le trait `Clone`.

6. #[derive(Clone, Debug)]

7. struct Pair(Box<i32>, Box<i32>);

9. fn main() {

10. // Un instancie `Nil`.

11. let nil = Nil;

12. // On copie l'objet `Nil`, aucune ressource à transférer.

13. let copied_nil = nil;

15. // Les deux instances peuvent être utilisées indépendament l'une de l'autre.

16. println!("original: {:?}", nil);

17. println!("copy: {:?}", copied_nil);

19. // On crée un objet `Pair`.

20. let pair = Pair(Box::new(1), Box::new(2));

21. println!("original: {:?}", pair);

23. // On copie `pair` dans `moved_pair`, transfert de ressources.

24. let moved_pair = pair;

25. println!("copy: {:?}", moved_pair);

27. // Erreur! `pair` ne possède plus ses ressources.

28. // println!("original: {:?}", pair);

29. // TODO ^ Essayez de décommenter cette ligne.

31. // On copie `moved_pair` dans `cloned_pair` (ressources incluses).

32. let cloned_pair = moved_pair.clone();

33. // On libère la ressource originale avec `std::mem::drop`.

34. drop(moved_pair);

36. // Erreur! `moved_pair` a été libérée.

37. // println!("copy: {:?}", moved_pair);

38. // TODO ^ Essayez de décommenter cette ligne.

40. // La copie obtenue par `.clone()` peut toujours être utilisée !

41. println!("clone: {:?}", cloned_pair);

42. }

15 - macro_rules!

Rust fournit un puissant système de macros qui permet la métaprogrammation. Comme vous l'avez vu dans les chapitres précédents, les macros ressemblent aux fonctions, excepté que leurs identificateurs se terminent par un point d'exclamation !, mais au lieu de générer un appel de fonction, les macros sont étendues dans le code source et compilées avec le reste du programme.

Il est possible de créer une macro en utilisant la macro macro_rules!.

1. // Une simple macro nommée `say_hello`.

2. macro_rules! say_hello {

3. // `()` indique que la macro ne prend aucun argument.

4. () => (

5. // La macro étendra le contenu de ce bloc.

6. println!("Hello!");

7. )

8. }

10. fn main() {

11. // Cet appel va étendre `println!("Hello");`.

12. say_hello!()

13. }

15-1 - Les indicateurs

Les arguments d'une macro sont préfixés par un $ et sont typés par le biais d'un indicateur :

1. macro_rules! create_function {

2. // Cette macro prend un argument possédant l'indicateur `ident`

3. // et créé une fonction nommée `$func_name`.

4. // L'indicateur `ident` est utilisé pour les identificateur de variables/fonctions.

5. ($func_name:ident) => (

6. fn $func_name() {

7. // La macro `stringify!` convertit un `ident` en chaîne de caractères.

8. println!("You called {:?}()",

9. stringify!($func_name))

10. }

11. )

12. }

14. // On créé les fonctions `foo` et `bar` avec la macro ci-dessus.

15. create_function!(foo);

16. create_function!(bar);

18. macro_rules! print_result {

19. // Cette macro prend une expression de type `expr` et

20. // affiche sa représentation sous forme de chaîne de caractères

21. // ainsi que son résultat.

22. ($expression:expr) => (

23. // `stringify!` va convertir l'expression *telle qu'elle est* dans une chaîne

24. // de caractères.

25. println!("{:?} = {:?}",

26. stringify!($expression),

27. $expression)

28. )

29. }

31. fn main() {

32. foo();

33. bar();

35. print_result!(1u32 + 1);

37. // Rappelez-vous que les blocs sont également des expressions !

38. print_result!({

39. let x = 1u32;

41. x * x + 2 * x - 1

42. });

43. }

Voici une liste exhaustive des indicateurs existants :

- block ;

- expr, pour les expressions ;

- ident, pour les identificateurs de variables et fonctions ;

- item ;

- pat (pattern) ;

- path ;

- stmt (déclaration) ;

- token (token tree) ;

- ty (type).

15-2 - Surcharge

Les macros peuvent être surchargées pour accepter différentes combinaisons d'arguments. Dans cet esprit, macro_rules! peut fonctionner de la même manière qu'un bloc match :

1. // `test!` va comparer `$left` et `$right` de différentes

2. // manières suivant son utilisation à l'invocation:

3. macro_rules! test {

4. // Il n'est pas nécessaire de séparer les arguments par des virgules.

5. // N'importe quel modèle peut être utilisé !

6. ($left:expr; and $right:expr) => (

7. println!("{:?} and {:?} is {:?}",

8. stringify!($left),

9. stringify!($right),

10. $left && $right)

11. );

12. // ^ Chaque branche doit se terminer par un point-virgule.

13. ($left:expr; or $right:expr) => (

14. println!("{:?} or {:?} is {:?}",

15. stringify!($left),

16. stringify!($right),

17. $left || $right)

18. );

19. }

21. fn main() {

22. test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);

23. test!(true; or false);

24. }

15-3 - Répétition

Les macros peuvent utiliser le quantificateur + dans la liste des arguments pour indiquer qu'un argument peut être répété au moins une(1) fois ou * pour indiquer que l'argument peut être répété zéro(0) ou plusieurs fois.

Dans l'exemple suivant, entourer le matcher avec $(...),+ va permettre la capture d'une ou plusieurs expressions, séparées par des virgules. Notez également que le point-virgule est optionnel dans le dernier cas (i.e. la dernière expression capturée).

1. // `min!` va calculer le minimum entre chaque argument passé, peu importe le nombre.

2. macro_rules! find_min {

3. // Expression de base:

4. ($x:expr) => ($x);

5. // `$x` suivi par au moins un `$y,`.

6. ($x:expr, $($y:expr),+) => (

7. // On appelle `find_min` sur les $y suivants`.

8. std::cmp::min($x, find_min!($($y),+))

9. )

10. }

12. fn main() {

13. println!("{}", find_min!(1u32));

14. println!("{}", find_min!(1u32 + 2 , 2u32));

15. println!("{}", find_min!(5u32, 2u32 * 3, 4u32));

16. }

15-4 - DRY (Don't Repeat Yourself)

Les macros permettent l'écriture de code DRY en recyclant les parties communes d'un ensemble de fonctions/tests. Voici un exemple qui implémente et test les opérateurs +=, *= et -= sur la structure Vec<T> :

1. // Dans le fichier dry.rs

2. use std::ops::{Add, Mul, Sub};

4. macro_rules! assert_equal_len {

5. // L'indicateur `tt` (token tree) est utilisé pour les opérateurs

6. // et les tokens.

7. ($a:ident, $b: ident, $func:ident, $op:tt) => (

8. assert!($a.len() == $b.len(),

9. "{:?}: dimension mismatch: {:?} {:?} {:?}",

10. stringify!($func),

11. ($a.len(),),

12. stringify!($op),

13. ($b.len(),));

14. )

15. }

17. macro_rules! op {

18. ($func:ident, $bound:ident, $op:tt, $method:ident) => (

19. fn $func<T: $bound<T, Output=T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) {

20. assert_equal_len!(xs, ys, $func, $op);

22. for (x, y) in xs.iter_mut().zip(ys.iter()) {

23. *x = $bound::$method(*x, *y);

24. // *x = x.$method(*y);

25. }

26. }

27. )

28. }

30. // On implémente les fonctions `add_assign`, `mul_assign` et `sub_assign`.

31. op!(add_assign, Add, +=, add);

32. op!(mul_assign, Mul, *=, mul);

33. op!(sub_assign, Sub, -=, sub);

35. mod test {

36. use std::iter;

37. macro_rules! test {

38. ($func: ident, $x:expr, $y:expr, $z:expr) => {

39. #[test]

40. fn $func() {

41. for size in 0usize..10 {

42. let mut x: Vec<_> = iter::repeat($x).take(size).collect();

43. let y: Vec<_> = iter::repeat($y).take(size).collect();

44. let z: Vec<_> = iter::repeat($z).take(size).collect();

46. super::$func(&mut x, &y);

48. assert_eq!(x, z);

49. }

50. }

51. }

52. }

54. // Test de `add_assign`, `mul_assign` et `sub_assign`.

55. test!(add_assign, 1u32, 2u32, 3u32);

56. test!(mul_assign, 2u32, 3u32, 6u32);

57. test!(sub_assign, 3u32, 2u32, 1u32);

58. }

1. $ rustc --test dry.rs && ./dry

2. running 3 tests

3. test test::mul_assign ... ok

4. test test::add_assign ... ok

5. test test::sub_assign ... ok

7. test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured

16 - La gestion des erreurs

La gestion d'erreur est le processus de gestion d'un échec potentiel. Par exemple, ne pas parvenir à lire un fichier et continuer à utiliser malgré tout cette mauvaise entrée serait clairement problématique. Cerner et gérer explicitement ces erreurs épargne le reste du programme d'un bon nombre de problèmes.

Pour une explicitation plus exhaustive à propos de la gestion des erreurs, référez-vous à la section dédiée à la gestion des erreurs dans le livre officiel.

16-1 - La macro panic

Le plus simple mécanisme de gestion d'erreur que nous allons voir est panic. Il affiche un message d'erreur, lance la tâche et généralement met fin au programme. Ici, nous appelons explicitement panic dans notre condition :

1. fn give_princess(gift: &str) {

2. // Les princesses détestent les serpents, donc nous devons tout arrêter si elles n'acceptent pas !

3. if gift == "snake" { panic!("AAAaaaaa!!!!"); }

5. println!("I love {}s!!!!!", gift);

6. }

8. fn main() {

9. give_princess("teddy bear");

10. give_princess("snake");

11. }

16-2 - L'enum Option et la méthode unwrap

Dans le dernier exemple, nous avons montré qu'il était possible de mettre en échec le programme quand bon nous semble. Nous disions que notre programme pouvait "paniquer" si la princesse recevait un présent inapproprié - un serpent. Mais qu'en est-il du cas où la princesse attendait un cadeau mais n'en reçoit pas ? Ce cas de figure serait tout bonnement irrecevable, donc inutile d'être géré.

Nous pourrions tester ce cas contre une chaîne de caractères vide (""), comme nous l'avons fait avec le serpent. Puisque nous utilisons Rust, laissons plutôt le compilateur gérer les cas où il n'y pas de cadeau.

Une enum nommée Option<T> dans la bibliothèque standard est utilisée lorsque "l'absence de" est une possibilité. Elle-même est représentée par deux variantes :

- Some(T) : Un élément de type T a été trouvé ;

- None : Aucun élément n'a été trouvé.

Ces cas peuvent être explicitement gérés par le biais de match ou implicitement avec unwrap. La gestion implicite renverra l'élément contenu en cas de succès, sinon un panic sera lancé.

Notez que s'il est possible de personnaliser manuellement le message d'erreur de panic, ce n'est pas le cas pour unwrap qui nous laissera avec des informations moins intelligibles qu'une gestion explicite. Dans l'exemple suivant, la gestion explicite offre un plus grand contrôle sur le résultat tout en permettant l'utilisation de panic, si désiré.

1. // Le roturier a déjà tout vu et accepte de bon coeur n'importe quel présent.

2. // Tous les présents sont gérés explicitement en utilisant `match`.

3. fn give_commoner(gift: Option<&str>) {

4. // On définit une action pour chaque cas.

5. match gift {

6. Some("snake") => println!("Yuck! I'm throwing that snake in a fire."),

7. Some(inner)   => println!("{}? How nice.", inner),

8. None          => println!("No gift? Oh well."),

9. }

10. }

12. // Notre précieuse princesse va `panic` à la vue des serpents.

13. // Tous les présents sont gérés implicitement en utilisant `unwrap`.

14. fn give_princess(gift: Option<&str>) {

15. // `unwrap` renvoie un `panic` lorsqu'il reçoit la variante `None`.

16. let inside = gift.unwrap();

17. if inside == "snake" { panic!("AAAaaaaa!!!!"); }

19. println!("I love {}s!!!!!", inside);

20. }

22. fn main() {

23. let food  = Some("cabbage");

24. let snake = Some("snake");

25. let void  = None;

27. give_commoner(food);

28. give_commoner(snake);

29. give_commoner(void);

31. let bird = Some("robin");

32. let nothing = None;

34. give_princess(bird);

35. give_princess(nothing);

36. }

16-2-1 - Les combinateurs: map

match est une approche valable pour gérer les Options. Cependant, vous pourriez trouver son utilisation fastidieuse, principalement avec des opérations qui ne sont valides qu'avec une entrée. Dans ces cas, les combinateurs peuvent être utilisés pour gérer le contrôle du flux de manière plus modulaire.

Option possède une méthode préfaite appelée map(), un combinateur pour la simple mise en corrélation de Some -> Some et None -> None. Les appels de la méthode map() peuvent être chaînés ensemble pour plus de flexibilité.

Dans l'exemple suivant, process() remplace toutes les fonctions précédentes tout en restant compacte.

1. #![allow(dead_code)]

3. #[derive(Debug)] enum Food { Apple, Carrot, Potato }

5. #[derive(Debug)] struct Peeled(Food);

6. #[derive(Debug)] struct Chopped(Food);

7. #[derive(Debug)] struct Cooked(Food);

10. // Épluchage de la nourriture. S'il n'y en a pas, alors on renvoie `None`.

11. // Sinon, on renvoie la nourriture épluchée.

12. fn peel(food: Option<Food>) -> Option<Peeled> {

13. match food {

14. Some(food) => Some(Peeled(food)),

15. None       => None,

16. }

17. }

19. // Éminçage de la nourriture. S'il n'y en a pas, alors on renvoie `None`.

20. // Sinon, on renvoie la nourriture émincée.

21. fn chop(peeled: Option<Peeled>) -> Option<Chopped> {

22. match peeled {

23. Some(Peeled(food)) => Some(Chopped(food)),

24. None               => None,

25. }

26. }

28. // Cuisson de la nourriture. Ici, nous utilisons `map()` au lieu de `match` pour la gestion des cas.

29. fn cook(chopped: Option<Chopped>) -> Option<Cooked> {

30. chopped.map(|Chopped(food)| Cooked(food))

31. }

33. // Une fonction pour éplucher, émincer et cuire la nourriture dans une seule séquence.

34. // Nous chaînons plusieurs appels de `map()` pour simplifier le code.

35. fn process(food: Option<Food>) -> Option<Cooked> {

36. food.map(|f| Peeled(f))

37. .map(|Peeled(f)| Chopped(f))

38. .map(|Chopped(f)| Cooked(f))

39. }

41. // On vérifie s'il y a de la nourritre ou pas avant d'essayer de la manger !

42. fn eat(food: Option<Cooked>) {

43. match food {

44. Some(food) => println!("Mmm. I love {:?}", food),

45. None       => println!("Oh no! It wasn't edible."),

46. }

47. }

49. fn main() {

50. let apple = Some(Food::Apple);

51. let carrot = Some(Food::Carrot);

52. let potato = None;

54. let cooked_apple = cook(chop(peel(apple)));

55. let cooked_carrot = cook(chop(peel(carrot)));

56. // Vous remarquerez que `process()` est bien plus lisible.

57. let cooked_potato = process(potato);

59. eat(cooked_apple);

60. eat(cooked_carrot);

61. eat(cooked_potato);

62. }

16-2-2 - Les combinateurs: and_then

map() a été décrite comme étant un moyen de chaîner les directives pour simplifier les déclarations match. Cependant, utiliser map() sur une fonction qui renvoie déjà une instance de Option<T> risque d'imbriquer le résultat dans une autre instance Option<Option<T>>. Chaîner des appels peut alors prêter à confusion. C'est là où un autre combinateur nommé and_then(), connu dans d'autres langages sous le nom de flatmap, entre en jeu.

and_then() appelle la fonction passée en entrée avec la valeur imbriquée et renvoie le résultat. Si le conteneur Option vaut None, alors elle renvoie None à la place.

Dans l'exemple suivant, cookable_v2() renvoie une instance de Option<Food>. Utiliser la méthode map() au lieu de and_then() donnerait une instance imbriquée Option<Option<Food>>, qui est un type invalide pour la fonction eat().

1. #![allow(dead_code)]

3. #[derive(Debug)] enum Food { CordonBleu, Steak, Sushi }

4. #[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }

6. // Nous n'avons pas les ingrédients pour faire des sushis.

7. fn have_ingredients(food: Food) -> Option<Food> {

8. match food {

9. Food::Sushi => None,

10. _           => Some(food),

11. }

12. }

14. // Nous avons la recette de tous les mets sauf celle du Cordon Bleu.

15. fn have_recipe(food: Food) -> Option<Food> {

16. match food {

17. Food::CordonBleu => None,

18. _                => Some(food),

19. }

20. }

23. // Pour faire un plat, nous avons besoin de deux ingrédients et d'une recette.

24. // Nous pouvons représenter la logique avec une chaîne de `match`s:

25. fn cookable_v1(food: Food) -> Option<Food> {

26. match have_ingredients(food) {

27. None       => None,

28. Some(food) => match have_recipe(food) {

29. None       => None,

30. Some(food) => Some(food),

31. },

32. }

33. }

35. // On peut rendre plus compacte cette implémentation en utilisant `and_then()`:

36. fn cookable_v2(food: Food) -> Option<Food> {

37. have_ingredients(food).and_then(have_recipe)

38. }

40. fn eat(food: Food, day: Day) {

41. match cookable_v2(food) {

42. Some(food) => println!("Yay! On {:?} we get to eat {:?}.", day, food),

43. None       => println!("Oh no. We don't get to eat on {:?}?", day),

44. }

45. }

47. fn main() {

48. let (cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi);

50. eat(cordon_bleu, Day::Monday);

51. eat(steak, Day::Tuesday);

52. eat(sushi, Day::Wednesday);

53. }

Voir aussi

Les closures, Option et Option::and_then().

16-3 - L'enum Result

Result est une version plus élaborée de l'enum Option qui conçoit une potentielle erreur plutôt qu'une potentielle absence.

Autrement dit, Result<T, E> pourrait adopter l'un de ces deux états :

- Ok<T> : Un élément T a été trouvé ;

- Err<E> : Une erreur a été trouvée avec l'élément E.

Par convention, la valeur de retour attendue est Ok jusqu'à la preuve du contraire (i.e. qu'une erreur (Err) est survenue).

Tout comme Option, Result possède de nombreuses méthodes associées à elle. unwrap(), par exemple, fournit l'élément T sinon déclenche panic. Pour la gestion des cas, Result possède nombre de combinateurs en commun avec Option.

En travaillant avec Rust, vous rencontrerez probablement des méthodes renvoyant le type Result, telles que la méthode parse(). Il n'est pas toujours possible de convertir une chaîne de caractères dans un autre type, donc parse() renvoie une instance de Result indiquant les potentielles erreurs.

Voyons ce qu'il se passe lorsque nous parvenons à convertir une chaîne de caractères et lorsque ce n'est pas le cas :

1. fn double_number(number_str: &str) -> i32 {

2. // Essayons d'utiliser la méthode `unwrap` pour récupérer le nombre.

3. // Va-t-elle nous mordre ?

4. 2 * number_str.parse::<i32>().unwrap()

5. }

7. fn main() {

8. let twenty = double_number("10");

9. println!("double is {}", twenty);

11. let tt = double_number("t");

12. println!("double is {}", tt);

13. }

Dans le cas où nous ne parvenons pas à la convertir, parse() nous laisse avec l'erreur sur laquelle unwrap a paniqué. Vous noterez également que le message d'erreur affiché par panic est assez désagréable.

Pour améliorer la qualité de notre message d'erreur, nous devrions être plus rigoureux quant à la valeur de retour et envisager de gérer explicitement l'erreur.

16-3-1 - La méthode map pour Result

Nous avons noté dans l'exemple précédent que le message d'erreur affiché lorsque le programme "panique" ne nous était pas d'une grande aide. Pour éviter cela, nous devons être plus précis concernant le type de renvoi. Ici, l'élément est de type i32. Pour déterminer le type de Err, jetons un oeil à la documentation de la méthode parse(), qui est implémentée avec le trait FromStr pour le type i32. Dans un résultat, le type de Err est ParseIntError.

Dans l'exemple ci-dessous, utiliser directement match mène à coder quelque chose de relativement lourd. Heureusement, la méthode map de Option est l'un de ces nombreux combinateurs ont également été implémentés pour Result. enum.Result en tient une liste complète.

1. use std::num::ParseIntError;

3. // Maintenant que le type de renvoi a été réécrit, nous utilisons le pattern matching

4. // sans la méthode `unwrap()`.

5. fn double_number(number_str: &str) -> Result<i32, ParseIntError> {

6. match number_str.parse::<i32>() {

7. Ok(n)  => Ok(2 * n),

8. Err(e) => Err(e),

9. }

10. }

13. // Tout comme avec `Option`, nous pouvons utiliser les combinateurs tels que `map()`.

14. // Cette fonction possède le même fonctionnement que celle ci-dessus et se lit comme suit:

15. // Modifie n si la valeur est valide, sinon renvoie une erreur.

16. fn double_number_map(number_str: &str) -> Result<i32, ParseIntError> {

17. number_str.parse::<i32>().map(|n| 2 * n)

18. }

20. fn print(result: Result<i32, ParseIntError>) {

21. match result {

22. Ok(n)  => println!("n is {}", n),

23. Err(e) => println!("Error: {}", e),

24. }

25. }

27. fn main() {

28. // Ceci fournit toujours une réponse valable.

29. let twenty = double_number("10");

30. print(twenty);

32. // Ce qui suit fournit désormais un message d'erreur plus intelligible.

33. let tt = double_number_map("t");

34. print(tt);

35. }

16-3-2 - Les alias de Result

Quid lorsque nous souhaitons réutiliser un type de Result bien précis ? Rappelez-vous que Rust nous permet de créer des alias. Nous pouvons alors aisément en définir un pour le type de Result en question.

À l'échelle d'un module, la création d'alias peut être salvatrice. Les erreurs pouvant être trouvées dans un module pécis ont souvent le même type (wrappé par Err), donc un seul alias peut définir l'intégralité des Results associés. C'est tellement utile que la bibliothèque standard en fournit un: io::Result !

Voici un petit exemple pour présenter la syntaxe :

1. use std::num::ParseIntError;

3. // On définit un alias générique pour un type de `Result` avec le type d'erreur

4. // `ParseIntError`.

5. type AliasedResult<T> = Result<T, ParseIntError>;

7. // On utilise l'alias ci-dessus pour faire référence à notre

8. // `Result`.

9. fn double_number(number_str: &str) -> AliasedResult<i32> {

10. number_str.parse::<i32>().map(|n| 2 * n)

11. }

13. // Ici, l'alias nous permet encore d'épargner de l'espace.

14. fn print(result: AliasedResult<i32>) {

15. match result {

16. Ok(n)  => println!("n is {}", n),

17. Err(e) => println!("Error: {}", e),

18. }

19. }

21. fn main() {

22. print(double_number("10"));

23. print(double_number("t"));

24. }

Voir aussi

Result et io::Result.

16-4 - Multiples types d'erreur

Les exemples précédents ont toujours été très basiques; Les (instances de) Result interagissent avec d'autres (instances de) Result et les Options interagissent avec d'autres Options.

Parfois une instance d'Option a besoin d'interagir avec un Result ou encore un Result<T, Error1> devant interagir avec un Result<T, Error2>. Dans ces cas, nous souhaitons gérer nos différents types de manière à pouvoir interagir simplement avec eux.

Dans le code suivant, deux instances de la méthode unwrap() génèrent deux types d'erreur différents. Vec::first renvoie une instance de Option, alors que parse::<i32> renvoie une instance de Result<i32, ParseIntError> :

1. fn double_first(vec: Vec<&str>) -> i32 {

2. let first = vec.first().unwrap(); // Génère la première erreur.

3. 2 * first.parse::<i32>().unwrap() // Génère la seconde erreur.

4. }

6. fn main() {

7. let empty = vec![];

8. let strings = vec!["tofu", "93", "18"];

10. println!("The first doubled is {}", double_first(empty));

11. // Erreur 1: Le vecteur passé en entrée est vide.

13. println!("The first doubled is {}", double_first(strings));

14. // Erreur 2: L'élément ne peut pas être converti en nombre.

15. }

En utilisant notre connaissance des combinateurs, nous pouvons réécrire ce qu'il y a au-dessus pour gérer explicitement les erreurs. Puisque deux types différents peuvent être rencontrés, nous nous devons de les convertir en un type commun tel que String.

Pour ce faire, nous convertissons les instances d'Option et de Result en Results puis nous convertissons leurs erreurs respectives sous le même type :

1. // Nous utiliserons `String` en tant que type d'erreur.

2. type Result<T> = std::result::Result<T, String>;

4. fn double_first(vec: Vec<&str>) -> Result<i32> {

5. vec.first()

6. // On convertit l'`Option` en un `Result` s'il y a une valeur.

7. // Autrement, on fournit une instance `Err` contenant la `String`.

8. .ok_or("Please use a vector with at least one element.".to_owned())

9. .and_then(|s| s.parse::<i32>()

10. // On convertit n'importe quelle erreur, générée par `parse`,

11. // en `String`.

12. .map_err(|e| e.to_string())

13. // `Result<T, String>` est le nouveau type de valeur,

14. // et nous pouvons doubler le nombre se trouvant dans le conteneur.

15. .map(|i| 2 * i))

16. }

18. fn print(result: Result<i32>) {

19. match result {

20. Ok(n)  => println!("The first doubled is {}", n),

21. Err(e) => println!("Error: {}", e),

22. }

23. }

25. fn main() {

26. let empty = vec![];

27. let strings = vec!["tofu", "93", "18"];

29. print(double_first(empty));

30. print(double_first(strings));

31. }

Dans la prochaine section, nous allons voir une méthode pour la gestion explicite de ces erreurs.

Voir aussi

Option::ok_or, Result::map_err.

16-4-1 - Retour prématuré

Dans l'exemple précédent, nous gérions explicitement les erreurs en utilisant les combinateurs. Une autre manière de répondre à cette analyse est d'utiliser une série de match et des retours prématurés.

Autrement dit, nous pouvons simplement mettre fin à l'exécution de la fonction et renvoyer l'erreur, s'il y en a une. Pour certains, cette manière de faire est plus simple à lire et écrire. Voici une nouvelle version de l'exemple précédent, réécrit en utilisant les retours prématurés :

1. // On utilise `String` comme type d'erreur.

2. type Result<T> = std::result::Result<T, String>;

4. fn double_first(vec: Vec<&str>) -> Result<i32> {

5. // On convertit l'`Option` en un `Result` s'il y a une valeur.

6. // Autrement, on fournit une instance `Err` contenant la `String`.

7. let first = match vec.first() {

8. Some(first) => first,

9. None => return Err("Please use a vector with at least one element.".to_owned()),

10. };

12. // On double le nombre dans le conteneur si `parse` fonctionne

13. // correctement.

14. // Sinon, on convertit n'importe quelle erreur, générée par `parse`,

15. // en `String`.

16. match first.parse::<i32>() {

17. Ok(i) => Ok(2 * i),

18. Err(e) => Err(e.to_string()),

19. }

20. }

22. fn print(result: Result<i32>) {

23. match result {

24. Ok(n) => println!("The first doubled is {}", n),

25. Err(e) => println!("Error: {}", e),

26. }

27. }

29. fn main() {

30. let empty = vec![];

31. let strings = vec!["tofu", "93", "18"];

33. print(double_first(empty));

34. print(double_first(strings));

35. }

À ce niveau, nous avons appris à gérer explicitement les erreurs en utilisant les combinateurs et les retours prématurés. Bien que, généralement, nous souhaitons éviter un plantage, la gestion explicite de toutes nos erreurs peut s'avérer relativement lourde.

Dans la section suivante, nous introduirons la macro try! pour couvrir les cas où nous souhaitons simplement utiliser unwrap() sans faire planter le programme.

16-4-2 - Introduction à try!

Parfois nous voulons la simplicité de unwrap() sans la possibilité de faire planter le programme. Jusqu'ici, unwrap() nous a dévié de ce que nous voulions vraiment : récupérer la variable. C'est exactement le but de try! que de régler ce souci.

Une fois l'instance Err trouvée, il y a deux actions possibles :

1. panic! que nous avons choisi d'éviter, si possible, avec try! ;

2. return parce qu'une Err ne peut être traitée.

La macro try! est presque(1) équivalent à la méthode unwrap() mais effectue un renvoi prématuré au lieu de planter lorsqu'un conteneur Err est récupéré. Voyons comment nous pouvons simplifier l'exemple précédent qui utilisait les combinateurs :

1. // On utilise une `String` comme type d'erreur.

2. type Result<T> = std::result::Result<T, String>;

4. fn double_first(vec: Vec<&str>) -> Result<i32> {

5. let first = try!(vec.first().ok_or(

6. "Please use a vector with at least one element.".to_owned(),

7. ));

9. let value = try!(first.parse::<i32>().map_err(|e| e.to_string()));

11. Ok(2 * value)

12. }

14. fn print(result: Result<i32>) {

15. match result {

16. Ok(n) => println!("The first doubled is {}", n),

17. Err(e) => println!("Error: {}", e),

18. }

19. }

21. fn main() {

22. let empty = vec![];

23. let strings = vec!["tofu", "93", "18"];

25. print(double_first(empty));

26. print(double_first(strings));

27. }

Notez que, jusqu'ici, nous avons utilisé les Strings pour les erreurs. Cependant, elles sont quelque peu limitées en tant que type d'erreur. Dans la prochaine section, nous apprendrons à créer des erreurs plus structurées, plus riches, en définissant leur propre type.

Voir aussi

Result et io::Result.

16-5 - Définition d'un type d'erreur

Rust nous permet de définir nos propres types d'erreur. En général, un « bon » type d'erreur :

- représente différentes erreurs avec le même type ;

- présente un message d'erreur intelligible pour l'utilisateur ;

- est facilement comparable aux autres types ;

- Bien : Err(EmptyVec),

- Pas bien : Err("Please use a vector with at least one element".to_owned()) ;

- peut supporter l'ajout d'informations à propos de l'erreur ;

- Bien : Err(BadChar(c, position)),

- Pas bien : Err("+ cannot be used here".to_owned()).

Notez qu'une String (que nous utilisions jusqu'ici) remplit les deux premiers critères, mais pas les deux derniers. Cela rend la création d'erreurs, simplement en utilisant String, verbeuse et difficile à maintenir. Il ne devrait pas être nécessaire de polluer la logique du code avec le formattage des chaînes de caractères pour avoir un affichage intelligible.

1. use std::num::ParseIntError;

2. use std::fmt;

4. type Result<T> = std::result::Result<T, DoubleError>;

6. #[derive(Debug)]

7. // On définit nos propres types d'erreur. Ces derniers peuvent être personnalisés pour

8. // notre gestion des cas.

9. // Maintenant nous serons capables d'écrire nos propres erreurs et nous reporter

10. // à leur implémentation.

11. enum DoubleError {

12. // Il n'est pas nécessaire de collecter plus d'informations

13. // pour détailler cette erreur.

14. EmptyVec,

15. // Nous allons nous reporter à l'implémentation couvrant l'erreur de conversion pour ce type.

16. // Soumettre des informations supplémentaires nécéssite l'ajout de données pour le type.

17. Parse(ParseIntError),

18. }

20. // La génération d'une erreur fait abstraction de la manière dont elle est affichée,

21. // nul besoin de s'occuper de la mécanique sous-jacente.

22. //

23. // Notez que nous ne stockons aucune information supplémentaire à propos de l'erreur.

24. // Cela signifie que nous ne pouvons préciser quelle chaîne de caractères n'a pas pu être

25. // convertie, sans modifier nos types pour apporter cette information.

26. impl fmt::Display for DoubleError {

27. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

28. match *self {

29. DoubleError::EmptyVec => write!(f, "please use a vector with at least one element"),

30. // Ceci est un wrapper, donc référrez-vous à l'implémentation de `fmt` respective

31. // à chaque type.

32. DoubleError::Parse(ref e) => e.fmt(f),

33. }

34. }

35. }

37. fn double_first(vec: Vec<&str>) -> Result<i32> {

38. vec.first()

39. // On remplace l'erreur par notre nouveau type.

40. .ok_or(DoubleError::EmptyVec)

41. .and_then(|s| s.parse::<i32>()

42. // On met également à jour le nouveau type d'erreur ici.

43. .map_err(DoubleError::Parse)

44. .map(|i| 2 * i))

45. }

47. fn print(result: Result<i32>) {

48. match result {

49. Ok(n) => println!("The first doubled is {}", n),

50. Err(e) => println!("Error: {}", e),

51. }

52. }

54. fn main() {

55. let numbers = vec!["93", "18"];

56. let empty = vec![];

57. let strings = vec!["tofu", "93", "18"];

59. print(double_first(numbers));

60. print(double_first(empty));

61. print(double_first(strings));

62. }

Voir aussi

Result et io::Result.

16-6 - D'autres cas d'utilisation de try!

Vous avez remarqué, dans l'exemple précédent, que notre réaction immédiate à l'appel de parse « était » de passer l'erreur de la bibliothèque dans notre nouveau type :

.and_then(|s| s.parse::<i32>()
.map_err(DoubleError::Parse)

Puisque c'est une opération plutôt commune, il ne serait pas inutile qu'elle soit élidée. Hélas, and_then n'étant pas suffisamment flexible pour cela, ce n'est pas possible. Nous pouvons, à la place, utiliser try!.

La macro try! a été précédemment présentée comme permettrant la récupération de la ressource (unwrap) ou le renvoi prématuré, si une erreur survient (return Err(err)). C'est plus ou moins vrai. En réalité, elle utilise soit unwrap soit return Err(From::from(err)). Puisque From::from est un utilitaire permettant la conversion entre différents types, cela signifie que si vous utilisez try! où l'erreur peut être convertie au type de retour, elle le sera automatiquement.

Ici, nous réécrivons l'exemple précédent en utilisant try!. Résultat, la méthode map_err disparaîtra lorsque From::from sera implémenté pour notre type d'erreur :

1. use std::num::ParseIntError;

2. use std::fmt;

4. type Result<T> = std::result::Result<T, DoubleError>;

6. #[derive(Debug)]

7. enum DoubleError {

8. EmptyVec,

9. Parse(ParseIntError),

10. }

12. // On implémente la conversion du type `ParseIntError` au type `DoubleError`.

13. // La conversion sera automatiquement appelée par `try!` si une instance `ParseIntError`

14. // doit être convertie en `DoubleError`.

15. impl From<ParseIntError> for DoubleError {

16. fn from(err: ParseIntError) -> DoubleError {

17. DoubleError::Parse(err)

18. }

19. }

21. impl fmt::Display for DoubleError {

22. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

23. match *self {

24. DoubleError::EmptyVec =>

25. write!(f, "please use a vector with at least one element"),

26. DoubleError::Parse(ref e) => e.fmt(f),

27. }

28. }

29. }

31. // La même structure qu'avant mais, plutôt que de chaîner les instances `Result`

32. // et `Option` tout du long, nous utilisons `try!` pour récupérer immédiatement

33. // la valeur contenue.

34. fn double_first(vec: Vec<&str>) -> Result<i32> {

35. // Convertit toujours en `Result` tout en renseignant comment convertir

36. // un `None`.

37. let first = try!(vec.first().ok_or(DoubleError::EmptyVec));

38. let parsed = try!(first.parse::<i32>());

40. Ok(2 * parsed)

41. }

43. fn print(result: Result<i32>) {

44. match result {

45. Ok(n)  => println!("The first doubled is {}", n),

46. Err(e) => println!("Error: {}", e),

47. }

48. }

50. fn main() {

51. let numbers = vec!["93", "18"];

52. let empty = vec![];

53. let strings = vec!["tofu", "93", "18"];

55. print(double_first(numbers));

56. print(double_first(empty));

57. print(double_first(strings));

58. }

C'est effectivement plus acceptable. Comparé à l'original panic, en remplaçant les appels de unwrap par try! nous conservons une utilisation assez familière, à l'exception que try! renvoie les types dans un conteneur Result, rendant leur déstructuration plus abstraite.

Notez toutefois que vous ne devriez pas systématiquement gérer les erreurs de cette manière pour remplacer les appels de unwrap. Cette méthode de gestion d'erreur a triplé le nombre de lignes de code et ne peut pas être considéré comme « simple » (même si la taille du code n'est pas énorme).

En effet, modifier une bibliothèque de 1000 lignes pour remplacer des appels de unwrap et établir une gestion des erreurs plus « propre » pourrait être faisable en une centaine de lignes supplémentaires. En revanche, le recyclage nécessaire en aval ne serait pas évident.

Nombreuses sont les bibliothèques qui pourraient s'en sortir en implémentant seulement Display et ajouter From comme base pour la gestion. Cependant, pour des bibliothèques plus importantes, l'implémentation de la gestion des erreurs peut répondre à des besoins plus spécifiques.

Voir aussi

From::from et try!.

16-7 - Boxing des erreurs

En implémentant Display et Form pour notre type d'erreur, nous avons usé de presque tous les outils dédiés à la gestion d'erreur de la bibliothèque standard. Nous avons cependant oublié quelque chose : la capacité à simplement Box notre type.

La bibliothèque standard convertit n'importe quel type qui implémente le trait Error et sera pris en charge par le type Box<Error>, via From. Pour l'utilisateur d'une bibliothèque, ceci permet aisément une manoeuvre de ce genre :

fn foo(...) -> Result<T, Box<Error>> { ... }

Un utilisateur peut utiliser nombre de bibliothèques externes, chacune fournissant leurs propres types d'erreur. Pour définir un type de Result<T, E> valide, l'utilisateur a plusieurs options :

- Définir un nouveau wrapper englobant les types d'erreur de la bibliothèque ;

- Convertir les types d'erreur en String ou vers un autre type intermédiaire ;

- Box les types dans Box<Error>.

Le "boxing" du type d'erreur est un choix plutôt habituel. Le problème est que le type de l'erreur sous-jacente est connu à l'exécution et n'est pas déterminé statiquement. Comme mentionné plus haut, tout ce qu'il y a à faire c'est d'implémenter le trait Error :

1. trait Error: Debug + Display {

2. fn description(&self) -> &str;

3. fn cause(&self) -> Option<&Error>;

4. }

Avec cette implémentation, jetons un oeil à notre exemple récemment présenté. Notez qu'il est tout aussi fonctionnel avec le type Box<Error> qu'avec DoubleError :

1. use std::error;

2. use std::fmt;

3. use std::num::ParseIntError;

5. // On modifie l'alias pour ajouter `Box<error::Error>`.

6. type Result<T> = std::result::Result<T, Box<error::Error>>;

8. #[derive(Debug)]

9. enum DoubleError {

10. EmptyVec,

11. Parse(ParseIntError),

12. }

14. impl From<ParseIntError> for DoubleError {

15. fn from(err: ParseIntError) -> DoubleError {

16. DoubleError::Parse(err)

17. }

18. }

20. impl fmt::Display for DoubleError {

21. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

22. match *self {

23. DoubleError::EmptyVec =>

24. write!(f, "please use a vector with at least one element"),

25. DoubleError::Parse(ref e) => e.fmt(f),

26. }

27. }

28. }

30. impl error::Error for DoubleError {

31. fn description(&self) -> &str {

32. match *self {

33. // Courte description de l'erreur.

34. // Vous n'êtes pas obligé de renseigner la même description que

35. // pour `Display`.

36. DoubleError::EmptyVec => "empty vectors not allowed",

37. // Ceci implémente déjà `Error`, on se reporte à sa propre implémentation.

38. DoubleError::Parse(ref e) => e.description(),

39. }

40. }

42. fn cause(&self) -> Option<&error::Error> {

43. match *self {

44. // Pas de cause (i.e. pas d'autre erreur)

45. // sous-jacente au déclenchement de cette erreur, donc on renvoie `None`.

46. DoubleError::EmptyVec => None,

47. // La cause est l'implémentation sous-jacente du type d'erreur.

48. // Il (le type) est implicitement casté en `&error::Error`.

49. // Ca fonctionne parce que le type en question a déjà implémenté le trait `Error`.

50. DoubleError::Parse(ref e) => Some(e),

51. }

52. }

53. }

55. fn double_first(vec: Vec<&str>) -> Result<i32> {

56. let first = try!(vec.first().ok_or(DoubleError::EmptyVec));

57. let parsed = try!(first.parse::<i32>());

59. Ok(2 * parsed)

60. }

62. fn print(result: Result<i32>) {

63. match result {

64. Ok(n)  => println!("The first doubled is {}", n),

65. Err(e) => println!("Error: {}", e),

66. }

67. }

69. fn main() {

70. let numbers = vec!["93", "18"];

71. let empty = vec![];

72. let strings = vec!["tofu", "93", "18"];

74. print(double_first(numbers));

75. print(double_first(empty));

76. print(double_first(strings));

77. }

Voir aussi

Distribution dynamique et le trait Error.

17 - Les types de la bibliothèque standard

La bibliothèque standard fournit de nombreux types complexes qui étendent drastiquement l'utilisation des primitifs. Voici certains d'entre-eux :

- Les chaînes de caractères redimensionnables String : "hello world" ;

- Les vecteurs redimensionnables : [1, 2, 3] ;

- Les types optionnels : Option<i32> ;

- Les types dédiés à la gestion des erreurs : Result<i32, i32> ;

- Les pointeurs alloués dans le tas : Box<i32>.

Voir aussi

Les primitifs et la bibliothèque standard.

17-1 - Les Box, la pile et le tas

En Rust, toutes les valeurs sont allouées dans la pile, par défaut. Les valeurs peuvent être boxées (i.e. allouées dans le tas) en créant une instance de Box<T>. Une "box" est un pointeur intelligent sur une ressource de type T allouée dans le tas. Lorsqu'une box sort du contexte, son destructeur est appelé, l'objet à charge est détruit et la mémoire du tas libérée.

Les valeurs « boxées » peuvent être déréférencées en utilisant l'opérateur * ; Ceci supprime un niveau d'indirection.

1. use std::mem;

3. #[derive(Clone, Copy)]

4. struct Point {

5. x: f64,

6. y: f64,

7. }

9. #[allow(dead_code)]

10. struct Rectangle {

11. p1: Point,

12. p2: Point,

13. }

15. fn origin() -> Point {

16. Point { x: 0.0, y: 0.0 }

17. }

19. fn boxed_origin() -> Box<Point> {

20. // Alloue cette instance de `Point` dans le tas et renvoie un pointeur

21. // sur cette dernière.

22. Box::new(Point { x: 0.0, y: 0.0 })

23. }

25. fn main() {

26. // (Ici le typage est superflu)

27. // Variables allouées dans la pile.

28. let point: Point = origin();

29. let rectangle: Rectangle = Rectangle {

30. p1: origin(),

31. p2: Point { x: 3.0, y: 4.0 }

32. };

34. // Rectangle alloué dans le tas.

35. let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle {

36. p1: origin(),

37. p2: origin()

38. });

40. // Le résultat des fonctions peut être boxé également.

41. let boxed_point: Box<Point> = Box::new(origin());

43. // Double indirection

44. let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());

46. println!("Point occupies {} bytes in the stack",

47. mem::size_of_val(&point));

48. println!("Rectangle occupies {} bytes in the stack",

49. mem::size_of_val(&rectangle));

51. // La taille du pointeur est égale à la taille de la Box.

52. println!("Boxed point occupies {} bytes in the stack",

53. mem::size_of_val(&boxed_point));

54. println!("Boxed rectangle occupies {} bytes in the stack",

55. mem::size_of_val(&boxed_rectangle));

56. println!("Boxed box occupies {} bytes in the stack",

57. mem::size_of_val(&box_in_a_box));

59. // La ressource contenue dans `boxed_point` est copiée dans

60. // `unboxed_point`.

61. let unboxed_point: Point = *boxed_point;

62. println!("Unboxed point occupies {} bytes in the stack",

63. mem::size_of_val(&unboxed_point));

64. }

17-2 - Les vecteurs

Les vecteurs sont des tableaux redimensionnables. Tout comme les slices, leur taille n'est pas connue à la compilation mais ils peuvent être agrandis ou tronqués au cours de l'exécution. Un vecteur est représenté par trois (3) mots : un pointeur sur la ressource, sa taille et sa capacité. La capacité indique la quantité de mémoire réservée au vecteur. La taille peut augmenter à volonté, tant qu'elle est inférieure à la capacité. Lorsqu'il est nécessaire de franchir cette limite, le vecteur est réalloué avec une capacité plus importante.

1. fn main() {

2. // Les éléments des itérateurs peuvent être collectés et

3. // ajoutés dans un vecteur.

4. let collected_iterator: Vec<i32> = (0..10).collect();

5. println!("Collected (0..10) into: {:?}", collected_iterator);

7. // La macro `vec!` peut être utilisée pour initialiser un vecteur.

8. let mut xs = vec![1i32, 2, 3];

9. println!("Initial vector: {:?}", xs);

11. // On ajoute un nouvel élément à la fin du vecteur.

12. println!("Push 4 into the vector");

13. xs.push(4);

14. println!("Vector: {:?}", xs);

16. // Erreur! Les vecteurs immuables ne peuvent pas être

17. // agrandis.

18. // collected_iterator.push(0);

19. // FIXME ^ Commentez/décommentez cette ligne

21. // La méthode `len` renvoie la taille actuelle du vecteur.

22. println!("Vector size: {}", xs.len());

24. // L'indexation peut être faite à l'aide des "[]" (l'indexaction débute à 0).

25. println!("Second element: {}", xs[1]);

27. // `pop` supprime le dernier élément du vecteur et le renvoie.

28. println!("Pop last element: {:?}", xs.pop());

30. // Une indexaction hors des capacités du vecteur

31. // mène à un plantage du programme.

32. println!("Fourth element: {}", xs[3]);

33. }

Les méthodes rattachées à la structure Vec peuvent être trouvées au sein du module std::vec.

17-3 - Les chaînes de caractères

Il y a deux types de chaînes de caractères en Rust : String et &str.

Une instance de String est stockée en tant que vecteur d'octets (Vec<u8>) mais garantit de toujours fournir une séquence valide encodée en UTF-8. String est alloué dans le tas, redimensionnable et non-nul.

&str est une slice (&[u8]) qui pointe toujours sur une séquence UTF-8 valide et peut être utilisée comme une vue sur une String. Tout comme &[T] est une vue sur une instance Vec<T>.

1. fn main() {

2. // (Le typage est optionnel)

3. // Une référence d'une chaîne de caractères immuable.

4. let pangram: &'static str = "the quick brown fox jumps over the lazy dog";

5. println!("Pangram: {}", pangram);

7. // On itère sur les mots dans le sens inverse, aucune nouvelle instance

8. // n'est créée.

9. println!("Words in reverse");

10. for word in pangram.split_whitespace().rev() {

11. println!("> {}", word);

12. }

14. // On copie les caractères dans un vecteur, les trie et supprime

15. // les occurrences multiples.

16. let mut chars: Vec<char> = pangram.chars().collect();

17. chars.sort();

18. chars.dedup();

20. // On créé une instance de `String` vide et mutable.

21. let mut string = String::new();

22. for c in chars {

23. // On ajoute un caractère à la fin de la chaîne.

24. string.push(c);

25. // On ajoute une nouvelle chaîne à la fin de la chaîne initiale.

26. string.push_str(", ");

27. }

29. // La chaîne tronquée est une slice de la chaîne originale, il n'y a

30. // pas de nouvelle allocation.

31. let chars_to_trim: &[char] = &[' ', ','];

32. let trimmed_str: &str = string.trim_matches(chars_to_trim);

33. println!("Used characters: {}", trimmed_str);

35. // Chaîne allouée dans le tas.

36. let alice = String::from("I like dogs");

37. // Nouvelle allocation mémoire et stockage de la chaîne modifiée

38. // à cet endroit.

39. let bob: String = alice.replace("dog", "cat");

41. println!("Alice says: {}", alice);

42. println!("Bob says: {}", bob);

43. }

Les méthodes rattachées à str/String peuvent être trouvées dans les modules std::str et std::string.

17-4 - L'énumération Option

Il est parfois désirable de rattraper les erreurs provenants de différentes parties du programme plutôt que d'appeler panic! ; pour ce faire, l'enum Option prend le relais.

L'enum Option<T> possède deux variantes :

1. None, pour signaler une erreur ou l'absence d'une valeur, et

2. Some(value), un tuple qui enveloppe, contient une valeur de type T.

1. // Une division entre deux entiers qui ne plante pas.

2. fn checked_division(dividend: i32, divisor: i32) -> Option<i32> {

3. if divisor == 0 {

4. // L'échec est représenté par la variante `None`.

5. None

6. } else {

7. // Le résultat est enveloppé dans une instance `Some`.

8. Some(dividend / divisor)

9. }

10. }

12. // Cette fonction gère une division qui peut ne pas fonctionner.

13. fn try_division(dividend: i32, divisor: i32) {

14. // Les valeurs d'`Option` peuvent être matchées, tout comme les autres enums.

15. match checked_division(dividend, divisor) {

16. None => println!("{} / {} failed!", dividend, divisor),

17. Some(quotient) => {

18. println!("{} / {} = {}", dividend, divisor, quotient)

19. },

20. }

21. }

23. fn main() {

24. try_division(4, 2);

25. try_division(1, 0);

27. // L'assignation de `None` à une variable nécessite de typer cette dernière.

28. let none: Option<i32> = None;

29. let _equivalent_none = None::<i32>;

31. let optional_float = Some(0f32);

33. // "dé-wrapper" une instance de `Some` va extraire la valeur contenue.

34. println!("{:?} unwraps to {:?}", optional_float, optional_float.unwrap());

36. // Tenter d'utiliser `unwrap` sur la variante `None` fera planter le programme.

37. println!("{:?} unwraps to {:?}", none, none.unwrap());

38. }

17-5 - L'énumération Result

Nous avons vu que l'enum Option peut être utilisée en tant que valeur de retour depuis les fonctions pouvant échouer, où None peut être renvoyé pour indiquer un échec. Il est parfois important d'expliquer pourquoi une opération a échoué. Pour ce faire, nous pouvons utiliser Result.

Result<T, E> possède deux variantes :

1. Ok(valeur) qui signale que l'opération s'est correctement déroulée et enveloppe la valeur renvoyée par l'opération (valeur est de type T);

2. Err(pourquoi) qui signale que l'opération a échoué et enveloppe le pourquoi, qui (espérons-le) nous renseigne sur la cause de l'échec (pourquoi est de type E).

1. mod checked {

2. // Les "erreurs" mathématiques que nous voulons gérer.

3. #[derive(Debug)]

4. pub enum MathError {

5. DivisionByZero,

6. NonPositiveLogarithm,

7. NegativeSquareRoot,

8. }

10. pub type MathResult = Result<f64, MathError>;

12. pub fn div(x: f64, y: f64) -> MathResult {

13. if y == 0.0 {

14. // Cette opération échouerait, nous wrappons l'erreur dans une instance

15. // `Err` à la place.

16. Err(MathError::DivisionByZero)

17. } else {

18. // Cette opération est valide, nous renvoyons le résultat wrappé dans `Ok`.

19. Ok(x / y)

20. }

21. }

23. pub fn sqrt(x: f64) -> MathResult {

24. if x < 0.0 {

25. Err(MathError::NegativeSquareRoot)

26. } else {

27. Ok(x.sqrt())

28. }

29. }

31. pub fn ln(x: f64) -> MathResult {

32. if x <= 0.0 {

33. Err(MathError::NonPositiveLogarithm)

34. } else {

35. Ok(x.ln())

36. }

37. }

38. }

40. // `op(x, y)` === `sqrt(ln(x / y))`

41. fn op(x: f64, y: f64) -> f64 {

42. // Ceci est une pyramide de match à trois niveaux !

43. match checked::div(x, y) {

44. Err(why) => panic!("{:?}", why),

45. Ok(ratio) => match checked::ln(ratio) {

46. Err(why) => panic!("{:?}", why),

47. Ok(ln) => match checked::sqrt(ln) {

48. Err(why) => panic!("{:?}", why),

49. Ok(sqrt) => sqrt,

50. },

51. },

52. }

53. }

55. fn main() {

56. // Cette opération va-t-elle échouer ?

57. println!("{}", op(1.0, 10.0));

58. }

17-5-1 - La macro try!

Chaîner les résultats en utilisant match peut être très chaotique ; heureusement, la macro try! peut être utilisée pour soigner l'écriture. La macro try! étend une expression de match où la branche Err(err) étend un retour prématuré (return Err(err)) et la branche Ok(ok) étend une expression ok et fournit la ressource.

1. mod checked {

2. #[derive(Debug)]

3. enum MathError {

4. DivisionByZero,

5. NegativeLogarithm,

6. NegativeSquareRoot,

7. }

9. type MathResult = Result<f64, MathError>;

11. fn div(x: f64, y: f64) -> MathResult {

12. if y == 0.0 {

13. Err(MathError::DivisionByZero)

14. } else {

15. Ok(x / y)

16. }

17. }

19. fn sqrt(x: f64) -> MathResult {

20. if x < 0.0 {

21. Err(MathError::NegativeSquareRoot)

22. } else {

23. Ok(x.sqrt())

24. }

25. }

27. fn ln(x: f64) -> MathResult {

28. if x < 0.0 {

29. Err(MathError::NegativeLogarithm)

30. } else {

31. Ok(x.ln())

32. }

33. }

35. // Fonction intermédiaire.

36. fn op_(x: f64, y: f64) -> MathResult {

37. // Si la fonction `div` échoue, alors `DivisionByZero` sera renvoyée.

38. let ratio = try!(div(x, y));

40. // Si `ln` échoue, alors `NegativeLogarithm` sera renvoyée.

41. let ln = try!(ln(ratio));

43. sqrt(ln)

44. }

46. pub fn op(x: f64, y: f64) {

47. match op_(x, y) {

48. Err(why) => panic!(match why {

49. MathError::NegativeLogarithm

50. => "logarithm of negative number",

51. MathError::DivisionByZero

52. => "division by zero",

53. MathError::NegativeSquareRoot

54. => "square root of negative number",

55. }),

56. Ok(value) => println!("{}", value),

57. }

58. }

59. }

61. fn main() {

62. checked::op(1.0, 10.0);

63. }

N'hésitez pas à consulter la documentation, de nombreuses méthodes sont disponibles pour créer et gérer les Result.

17-6 - La macro panic!

La macro panic! peut être utilisée pour générer un plantage et dérouler la pile. Pendant le déroulement de la pile, l'exécution prendra soin de libérer toutes les ressources possédées par le fil d'exécution en appelant le destructeur de chaque objet.

Puisque nous interagissons avec nos programmes en n'utilisant qu'un seul fil d'exécution, panic! renverra un message d'erreur puis mettra un terme à l'exécution.

1. // Ré-implémentation de la division d'entiers (/).

2. fn division(dividend: i32, divisor: i32) -> i32 {

3. if divisor == 0 {

4. // La division par zéro fait planter le thread courant.

5. panic!("division by zero");

6. } else {

7. dividend / divisor

8. }

9. }

12. fn main() {

13. // Entier alloué dans le tas.

14. let _x = Box::new(0i32);

16. // Cette opération va déclencher la procédure d'abandon.

17. division(3, 0);

19. println!("This point won't be reached!");

21. // `_x` devrait être détruit à ce niveau.

22. }

Vérifions que la macro panic! ne cause aucune fuite mémoire.

1. $ rustc panic.rs && valgrind ./panic

2. ==4401== Memcheck, a memory error detector

3. ==4401== Copyright (C) 2002-2013, and GNU GPL′d, by Julian Seward et al.

4. ==4401== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info

5. ==4401== Command: ./panic

6. ==4401==

7. thread '<main>' panicked at 'division by zero', panic.rs:5

8. ==4401==

9. ==4401== HEAP SUMMARY:

10. ==4401==     in use at exit: 0 bytes in 0 blocks

11. ==4401==   total heap usage: 18 allocs, 18 frees, 1,648 bytes allocated

12. ==4401==

13. ==4401== All heap blocks were freed -- no leaks are possible

14. ==4401==

15. ==4401== For counts of detected and suppressed errors, rerun with: -v

16. ==4401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

17-7 - La structure HashMap

Là où les vecteurs stockent leurs valeurs en utilisant un index entier, les HashMaps stockent leurs valeurs en utilisant des clés. Les clés d'une HashMap peuvent être des booléens, des chaînes de caractères ou n'importe quel autre type qui implémente les traits Eq et Hash. Nous y reviendrons dans la section suivante.

Tout comme les vecteurs, les HashMap sont redimensionnables mais peuvent également se tronquer elles-mêmes lorsqu'elles atteignent la limite de leur capacité. Vous pouvez créer une HashMap avec une capacité donnée en utilisant HashMap::with_capacity(uint), ou utiliser HashMap::new() pour récupérer une instance avec une capacité initiale par défaut (recommandé).

1. use std::collections::HashMap;

3. fn call(number: &str) -> &str {

4. match number {

5. "798-1364" => "We're sorry, the call cannot be completed as dialed.

6. Please hang up and try again.",

7. "645-7689" => "Hello, this is Mr. Awesome's Pizza. My name is Fred.

8. What can I get for you today?",

9. _ => "Hi! Who is this again?"

10. }

11. }

13. fn main() {

14. let mut contacts = HashMap::new();

16. contacts.insert("Daniel", "798-1364");

17. contacts.insert("Ashley", "645-7689");

18. contacts.insert("Katie", "435-8291");

19. contacts.insert("Robert", "956-1745");

21. // Prend une référence en entrée et renvoie un conteneur `Option<&V>`.

22. match contacts.get(&"Daniel") {

23. Some(&number) => println!("Calling Daniel: {}", call(number)),

24. _ => println!("Don't have Daniel's number."),

25. }

28. // La méthode `HashMap::insert()` renvoie `None`

29. // si la valeur insérée est nouvelle, sinon `Some(value)`.

30. contacts.insert("Daniel", "164-6743");

32. match contacts.get(&"Ashley") {

33. Some(&number) => println!("Calling Ashley: {}", call(number)),

34. _ => println!("Don't have Ashley's number."),

35. }

37. contacts.remove(&("Ashley"));

39. // La méthode `HashMap::iter()` renvoie un itérateur qui fournit

40. // les paires (&'a key, &'a value) dans un ordre arbitraire.

41. for (contact, &number) in contacts.iter() {

42. println!("Calling {}: {}", contact, call(number));

43. }

44. }

Pour plus d"informations à propos du fonctionnement du hashage et des hash maps (parfois appelées hash tables), consultez la page wikipédia dédiée aux Hash Tables.

17-7-1 - Personnaliser les types de clé

N'importe quel type implémentant les traits Eq et Hash peuvent être une clé dans une HashMap. Ce qui inclut :

- Le type bool (Bien que peut utile puisqu'il n'y a que deux clés possibles);

- Le type int, uint et toutes les variantes de ces derniers ;

- String et &str (note : vous pouvez avoir une HashMap recevant en entrée des String et appeler la méthode .get() avec une &str).

Notez que f32 et f64 n'implémentent pas Hash, sûrement parce les erreurs de précision rendrait leur utilisation en tant que clé d'une hashmap poserait des soucis.

Toutes les classes représentant une collection implémentent Eq et Hash si le type qu'elles contiennent implémentent également ces deux traits. Par exemple, Vec<T> implémentera Hash si T l'implémente.

Vous pouvez facilement implémenter Eq et Hash pour un nouveau type avec cette seule ligne : #[derive(PartialEq, Hash)].

Le compilateur fera le reste. Si vous souhaitez avoir plus de contrôle sur les détails, vous pouvez implémenter Eq et/ou Hash vous-même. Ce guide ne couvre pas les implémentations spécifiques de Hash.

Pour tester l'utilisation d'une struct dans une HashMap, créons un simple système d'identification :

1. use std::collections::HashMap;

3. // `Eq` nécessite de dériver `PartialEq` sur le type.

4. #[derive(PartialEq, Eq, Hash)]

5. struct Account<'a>{

6. username: &'a str,

7. password: &'a str,

8. }

10. struct AccountInfo<'a>{

11. name: &'a str,

12. email: &'a str,

13. }

15. type Accounts<'a> = HashMap<Account<'a>, AccountInfo<'a>>;

17. fn try_logon<'a>(accounts: &Accounts<'a>,

18. username: &'a str, password: &'a str){

19. println!("Username: {}", username);

20. println!("Password: {}", password);

21. println!("Attempting logon...");

23. let logon = Account {

24. username: username,

25. password: password,

26. };

28. match accounts.get(&logon) {

29. Some(account_info) => {

30. println!("Successful logon!");

31. println!("Name: {}", account_info.name);

32. println!("Email: {}", account_info.email);

33. },

34. _ => println!("Login failed!"),

35. }

36. }

38. fn main(){

39. let mut accounts: Accounts = HashMap::new();

41. let account = Account {

42. username: "j.everyman",

43. password: "password123",

44. };

46. let account_info = AccountInfo {

47. name: "John Everyman",

48. email: "j.everyman@email.com",

49. };

51. accounts.insert(account, account_info);

53. try_logon(&accounts, "j.everyman", "psasword123");

55. try_logon(&accounts, "j.everyman", "password123");

56. }

17-7-2 - La structure HashSet

Voyez une HashSet comme une HashMap où nous nous soucions uniquement des clés (HashSet<T> est, en réalité, simplement un wrapper de HashMap<T, ()>).

Vous pourriez vous demander "Quel est le but ? Je pourrais simplement stocker mes clés dans un Vec".

La fonctionnalité unique de HashSet est qu'elle garantit l'inexistance d'éléments dupliqués. C'est le contrat que n'importe quel ensemble remplit. HashSet n'est qu'une implémentation (voir aussi : BTreeSet).

Si vous ajoutez une valeur déjà présente dans l'instance HashSet, (i.e. la nouvelle valeur est égale à l'existante et ont toutes deux le même hash), alors la nouvelle valeur remplacera l'ancienne.

C'est pratique lorsque vous ne souhaitez jamais plus d'une occurrence de quelque chose ou lorsque vous voulez savoir si vous possédez déjà quelque chose. Mais les ensembles peuvent faire bien plus que cela.

Les ensembles ont quatre (4) opérations inhérentes (chacune renvoie un itérateur) :

1. union : Récupère tous les éléments dans les deux ensembles ;

2. difference : Récupère tous les éléments qui sont dans le premier ensemble mais pas dans le second ;

3. intersection : Récupère uniquement les éléments présents dans les deux ensembles ;

4. symmetric_difference : Récupère tous éléments qui sont dans le premier ou second ensemble mais pas les deux.

Essayons tout cela dans l'exemple suivant.

1. use std::collections::HashSet;

3. fn main() {

4. let mut a: HashSet<i32> = vec!(1i32, 2, 3).into_iter().collect();

5. let mut b: HashSet<i32> = vec!(2i32, 3, 4).into_iter().collect();

7. assert!(a.insert(4));

8. assert!(a.contains(&4));

10. // `HashSet::insert()` renvoie false si

11. // une valeur était déjà présente.

12. // assert!(b.insert(4), "Value 4 is already in set B!");

13. // FIXME ^ Commentez/décommentez cette ligne

15. b.insert(5);

17. // Si le type d'un élément de la collection implémente le trait `Debug`,

18. // alors la collection devra, elle aussi, implémenter `Debug`.

19. // Elle affiche généralement ses éléments dans le format `[elem1, elem2, ...]`.

20. println!("A: {:?}", a);

21. println!("B: {:?}", b);

23. // Affiche [1, 2, 3, 4, 5] dans un ordre arbitraire.

24. println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>());

26. // Ceci devrait afficher [1].

27. println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>());

29. // Affiche [2, 3, 4] dans un ordre arbitraire.

30. println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>());

32. // Affiche [1, 5].

33. println!("Symmetric Difference: {:?}",

34. a.symmetric_difference(&b).collect::<Vec<&i32>>());

35. }

Les exemples originaux proviennent de la documentation.

18 - Outils standards

Bien d'autres types sont fournis par la bibliothèque standard pour supporter des choses telles que :

- Les fils d'exécution ;

- Les canaux ;

- Les opérations sur le système de fichiers.

Ces types vont bien au-de-là de ce que les primitifs fournissent.

Voir aussi

Les primitifs et la bibliothèque standard.

18-1 - Les fils d'exécution

Rust fournit un méchanisme de création de fils d'exécution natifs via la fonction spawn. L'argument de cette fonction est une closure transférable.

1. use std::thread;

3. static NTHREADS: i32 = 10;

5. // Ceci est le thread `main`.

6. fn main() {

7. // On créé un vecteur pour récupérer tous les threads

8. // enfants qui ont été créés.

9. let mut children = vec![];

11. for i in 0..NTHREADS {

12. // On passe à un autre thread.

13. children.push(thread::spawn(move || {

14. println!("this is thread number {}", i)

15. }));

16. }

18. for child in children {

19. // On attend que le thread se termine. Renvoie un résultat.

20. let _ = child.join();

21. }

22. }

Ces threads seront programmés par le système d'exploitation.

18-2 - Les canaux

Rust fournit les canaux asynchrones pour la communication entre les threads. Les canaux permettent l'envoi d'un flux unidirectionnel d'information entre deux extrémités : l'envoyeur (Sender) et le receveur (Receiver).

1. use std::sync::mpsc::{Sender, Receiver};

2. use std::sync::mpsc;

3. use std::thread;

5. static NTHREADS: i32 = 3;

7. fn main() {

8. // Les canaux possèdent deux extrémités: l'envoyeur (`Sender<T>`) et le receveur (`Receiver<T>`),

9. // où `T` est le type du message à envoyer.

10. // (Le typage est optionnel)

11. let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();

13. for id in 0..NTHREADS {

14. // L'envoyeur peut être copié.

15. let thread_tx = tx.clone();

17. // Chaque thread va envoyer son identifiant par le biais du canal.

18. thread::spawn(move || {

19. // Le thread prend l'ownership sur `thread_tx`.

20. // Chaque thread va ajouter un message dans le file d'attente

21. // dans le canal.

22. thread_tx.send(id).unwrap();

24. // L'envoi est une opération non-bloquante, le thread continuera à

25. // s'exécuter après l'envoi de son message.

26. println!("thread {} finished", id);

27. });

28. }

30. // Ici, on récupère tous les messages.

31. let mut ids = Vec::with_capacity(NTHREADS as usize);

32. for _ in 0..NTHREADS {

33. // La méthode `recv` choisit une message se trouvant dans le canal et

34. // gêlera le thread courant s'il n'y a aucun message.

35. ids.push(rx.recv());

36. }

38. // Montre l'ordre dans lequel les messages ont été envoyés.

39. println!("{:?}", ids);

40. }

18-3 - La structure Path

La structure Path représente les chemins de fichiers dans le système de fichiers sous-jacent. Il y a deux variantes de Path :

1. posix::Path, pour les systèmes UNIX-like ;

2. windows::Path, pour Windows.

Le prélude exporte la variante de Path adaptée à la plateforme.

Une instance de Path peut être créée à partir du type OsStr et fournit de nombreuses méthodes pour obtenir des informations à propos du fichier/répertoire sur lequel le chemin pointe.

Notez que, en interne, un objet Path n'est pas représenté par une chaîne de caractères UTF-8 mais est stocké dans un vecteur d'octets (Vec<u8>). En conséquence, la conversion d'un objet Path en &str n'est pas gratuite et peut échouer (un objet Option est renvoyé).

1. use std::path::{Path, PathBuf};

2. use std::ffi;

4. fn main() {

5. // Création d'un objet `Path` à partir d'une `&'static str`.

6. let path = Path::new(".");

8. // La méthode `display` renvoie une structure présentable.

9. let display = path.display();

11. // La méthode `join()` fusionne un chemin avec toutes les ressources

12. // possédant une implémentation de la méthode `as_ref()` pour le type `Path` et

13. // renvoie un nouveau chemin avec le séparateur spécifique à l'OS.

14. let new_path = path.join("a").join("b");

16. // Convertit le chemin en une vue sur une string.

17. match new_path.to_str() {

18. None => panic!("new path is not a valid UTF-8 sequence"),

19. Some(s) => println!("new path is {}", s),

20. }

21. }

N'hésitez pas à cosnulter les autres méthodes de Path et la structure Metadata.

Voir aussi

OsStr et Metadata.

18-4 - La structure File

La structure File représente un fichier qui a été ouvert (contient un descripteur de fichier), et donne les accès lecture et/ou écriture sur le fichier sous-jacent.

Puisque de nombreuses choses peuvent mal se passer lorsqu'une opération est effectuée, toutes les méthodes de File renvoie le type io::Result<T>, lequel étant un alias pour Result<T, io::Error>.

Ceci couvre les potentielles erreurs de toutes les opérations d'entrée/sortie explicites. Grâce à cela, le programmeur peut visualiser toutes les erreurs possibles et est encouragé à les anticiper.

18-4-1 - La méthode open

La méthode statique open() peut être utilisé pour ouvrir un fichier en lecture seule.

Un objet File est responsable d'une ressource, du descripteur de fichier et prend soin de fermer le fichier lorsqu'il est libéré.

1. // Dans le fichier open.rs

2. use std::error::Error;

3. use std::fs::File;

4. use std::io::prelude::*;

5. use std::path::Path;

7. fn main() {

8. // Crée un chemin vers le fichier désiré.

9. let path = Path::new("hello.txt");

10. let display = path.display();

12. // Ouvre le chemin en lecture seule, renvoie un objet `io::Result<File>`.

13. let mut file = match File::open(&path) {

14. // La méthode `description` de `io::Error` renvoie une chaîne de caractères

15. // qui décrit l'erreur.

16. Err(why) => panic!("couldn't open {}: {}", display,

17. why.description()),

18. Ok(file) => file,

19. };

21. // Lit le contenu du fichier dans une chaîne de caractères, renvoie un objet `io::Result<usize>`.

22. let mut s = String::new();

23. match file.read_to_string(&mut s) {

24. Err(why) => panic!("couldn't read {}: {}", display,

25. why.description()),

26. Ok(_) => print!("{} contains:\n{}", display, s),

27. }

29. // `file` sort du contexte, le flux ouvert sur le fichier "hello.txt"

30. // va être fermé.

31. }

Voici le résultat attendu :

1. $ echo "Hello World!" > hello.txt

2. $ rustc open.rs && ./open

3. hello.txt contains:

4. Hello World!

Nous vous encourageons à confronter l'exemple précédent à des cas d'échec différents (e.g. hello.txt n'existe pas, hello.txt ne peut pas être lu).

18-4-2 - La méthode create

La méthode statique create() ouvre un fichier en écriture seule. Si le fichier existe déjà, le contenu sera écrasé. Autrement, un nouveau fichier sera créé.

1. // Dans le fichier create.rs

2. static LOREM_IPSUM: &'static str =

3. "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod

4. tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,

5. quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo

6. consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse

7. cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non

8. proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

9. ";

11. use std::error::Error;

12. use std::io::prelude::*;

13. use std::fs::File;

14. use std::path::Path;

16. fn main() {

17. let path = Path::new("out/lorem_ipsum.txt");

18. let display = path.display();

20. // Ouvre un fichier en écriture seule, renvoie un objet `io::Result<File>`.

21. let mut file = match File::create(&path) {

22. Err(why) => panic!("couldn't create {}: {}",

23. display,

24. why.description()),

25. Ok(file) => file,

26. };

28. // Écrit la chaîne de caractères de `LOREM_IPSUM` dans `file`, renvoie

29. // un objet `io::Result<()>`.

30. match file.write_all(LOREM_IPSUM.as_bytes()) {

31. Err(why) => {

32. panic!("couldn't write to {}: {}", display,

33. why.description())

34. },

35. Ok(_) => println!("successfully wrote to {}", display),

36. }

37. }

Voici le résultat attendu :

1. $ mkdir out

2. $ rustc create.rs && ./create

3. successfully wrote to out/lorem_ipsum.txt

4. $ cat out/lorem_ipsum.txt

5. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod

6. tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,

7. quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo

8. consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse

9. cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non

10. proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Comme pour l'exemple précédent, nous vous encourageons à confronter l'exemple à d'autres cas où l'opération pourrait échouer.

Il existe également d'autres outils pouvant ouvrir des fichiers dans des modes différents tels que : read+write, append, etc.

18-5 - Les sous-processus

La structure process::Output représente la sortie d'un sous-processus terminé et la structure process::Command est un constructeur de processus.

1. use std::process::Command;

3. fn main() {

4. let output = Command::new("rustc")

5. .arg("--version")

6. .output().unwrap_or_else(|e| {

7. panic!("failed to execute process: {}", e)

8. });

10. if output.status.success() {

11. let s = String::from_utf8_lossy(&output.stdout);

13. print!("rustc succeeded and stdout was:\n{}", s);

14. } else {

15. let s = String::from_utf8_lossy(&output.stderr);

17. print!("rustc failed and stderr was:\n{}", s);

18. }

19. }

Nous vous encourageons à essayer l'exemple précédent en lançant rustc avec un flag incorrect.

18-5-1 - Les pipes

La structure Process représente un processus en cours d'exécution et expose la gestion de stdin, stdout et stderr pour interagir avec le processus sous-jacent via les pipes.

1. use std::error::Error;

2. use std::io::prelude::*;

3. use std::process::{Command, Stdio};

5. static PANGRAM: &'static str =

6. "the quick brown fox jumped over the lazy dog\n";

8. fn main() {

9. // On crée un processus dans lequel la commande `wc` va s'exécuter.

10. let process = match Command::new("wc")

11. .stdin(Stdio::piped())

12. .stdout(Stdio::piped())

13. .spawn() {

14. Err(why) => panic!("couldn't spawn wc: {}", why.description()),

15. Ok(process) => process,

16. };

18. // On écrit quelque chose dans l'entrée standard de `wc`.

19. // `stdin` est de type `Option<ChildStdin>`, mais puisque nous savons que

20. // cette instance en possède une, nous pouvons directement l'`unwrap()`.

21. match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {

22. Err(why) => panic!("couldn't write to wc stdin: {}",

23. why.description()),

24. Ok(_) => println!("sent pangram to wc"),

25. }

27. // Puisque `stdin` ne survit pas après l'appel du dessus, elle va être libérée et

28. // et le pipe fermé.

29. //

30. // C'est très important sinon `wc` ne pourrait pas commencer à traiter l'entrée

31. // que nous avons soumis.

33. // La champ `stdout` est également de type `Option<ChildStdout>` et doit donc être `unwrap()`.

34. let mut s = String::new();

35. match process.stdout.unwrap().read_to_string(&mut s) {

36. Err(why) => panic!("couldn't read wc stdout: {}",

37. why.description()),

38. Ok(_) => print!("wc responded with:\n{}", s),

39. }

40. }

18-5-2 - La méthode wait

Vous souhaiteriez peut-être attendre qu'un processus, dont un objet process::Child est responsable, se termine. Pour cela vous devez appeler la méthode Child::wait qui renverra un objet process::ExitStatus.

1. // Dans le fichier wait.rs

2. use std::process::Command;

4. fn main() {

5. let mut child = Command::new("sleep").arg("5").spawn().unwrap();

6. let _result = child.wait().unwrap();

8. println!("reached end of main");

9. }

1. $ rustc wait.rs && ./wait

2. reached end of main

3. # `wait` s'est exécuté pendant 5 secondes.

4. # Une fois la commande `sleep 5` terminée notre programme `wait` a pris fin.

18-6 - Opérations sur le système de fichiers

Le module std::io::fs contient de nombreuses fonctions traitant avec le système de fichiers.

1. // Dans le fichier fs.rs

2. use std::fs;

3. use std::fs::{File, OpenOptions};

4. use std::io;

5. use std::io::prelude::*;

6. use std::os::unix;

7. use std::path::Path;

9. // Une simple implémentation de la commande `cat path`.

10. fn cat(path: &Path) -> io::Result<String> {

11. let mut f = try!(File::open(path));

12. let mut s = String::new();

13. match f.read_to_string(&mut s) {

14. Ok(_) => Ok(s),

15. Err(e) => Err(e),

16. }

17. }

19. // Une simple implémentation de la commande `echo s > path`.

20. fn echo(s: &str, path: &Path) -> io::Result<()> {

21. let mut f = try!(File::create(path));

23. f.write_all(s.as_bytes())

24. }

26. // Une simple implémentation de la commande `touch path` (ignore les fichiers existants).

27. fn touch(path: &Path) -> io::Result<()> {

28. match OpenOptions::new().create(true).write(true).open(path) {

29. Ok(_) => Ok(()),

30. Err(e) => Err(e),

31. }

32. }

34. fn main() {

35. println!("`mkdir a`");

36. // Crée un répertoire et renvoie un objet `io::Result<()>`.

37. match fs::create_dir("a") {

38. Err(why) => println!("! {:?}", why.kind()),

39. Ok(_) => {},

40. }

42. println!("`echo hello > a/b.txt`");

43. // Le match précédent peut être simplifié en utilisant la méthode `unwrap_or_else()`.

44. echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {

45. println!("! {:?}", why.kind());

46. });

48. println!("`mkdir -p a/c/d`");

49. // Crée un répertoire récursivement, renvoie un objet `ìo::Result<()>`.

50. fs::create_dir_all("a/c/d").unwrap_or_else(|why| {

51. println!("! {:?}", why.kind());

52. });

54. println!("`touch a/c/e.txt`");

55. touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {

56. println!("! {:?}", why.kind());

57. });

59. println!("`ln -s ../b.txt a/c/b.txt`");

60. // Crée un lien symbolique, renvoie un objet `io::Result<()>`.

61. if cfg!(target_family = "unix") {

62. unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {

63. println!("! {:?}", why.kind());

64. });

65. }

67. println!("`cat a/c/b.txt`");

68. match cat(&Path::new("a/c/b.txt")) {

69. Err(why) => println!("! {:?}", why.kind()),

70. Ok(s) => println!("> {}", s),

71. }

73. println!("`ls a`");

74. // Lit le contenu d'un répertoire, renvoie un objet `io::Result<Vec<Path>>`.

75. match fs::read_dir("a") {

76. Err(why) => println!("! {:?}", why.kind()),

77. Ok(paths) => for path in paths {

78. println!("> {:?}", path.unwrap().path());

79. },

80. }

82. println!("`rm a/c/e.txt`");

83. // Supprime un fichier, renvoie un objet `io::Result<()>`.

84. fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {

85. println!("! {:?}", why.kind());

86. });

88. println!("`rmdir a/c/d`");

89. // Supprime un répertoire vide, renvoie un objet `io::Result<()>`.

90. fs::remove_dir("a/c/d").unwrap_or_else(|why| {

91. println!("! {:?}", why.kind());

92. });

93. }

Voici le résultat attendu :

1. $ rustc fs.rs && ./fs

2. `mkdir a`

3. `echo hello > a/b.txt`

4. `mkdir -p a/c/d`

5. `touch a/c/e.txt`

6. `ln -s ../b.txt a/c/b.txt`

7. `cat a/c/b.txt`

8. > hello

9. `ls a`

10. > "a/b.txt"

11. > "a/c"

12. `rm a/c/e.txt`

13. `rmdir a/c/d`

Et l'état final du répertoire a est :

1. $ tree a

2. a

3. |-- b.txt

4. `-- c

5. `-- b.txt -> ../b.txt

7. 1 directory, 2 files

Voir aussi

La macro cfg!.

18-7 - Les arguments du programme

Les arguments passés en ligne de commande peuvent être récupérés en utilisant std::env::args qui renvoie un itérateur fournissant une String pour chaque argument :

1. use std::env;

3. fn main() {

4. let args: Vec<String> = env::args().collect();

6. // Le premier argument est le chemin à partir duquel le programme

7. // a été appelé.

8. println!("My path is {}.", args[0]);

10. // Le reste des arguments sont ceux passés en ligne de commande au programme.

11. // On appelle le programme comme ceci:

12. //  $ ./args arg1 arg2

13. println!("I got {:?} arguments: {:?}.", args.len() - 1, &args[1..]);

14. }

$ ./args 1 2 3
My path is ./args.
I got 3 arguments: ["1", "2", "3"].

18-7-1 - Récupération des arguments

Le pattern matching peut être utilisé pour traiter de simples arguments :

1. use std::env;

3. fn increase(number: i32) {

4. println!("{}", number + 1);

5. }

7. fn decrease(number: i32) {

8. println!("{}", number - 1);

9. }

11. fn help() {

12. println!("usage:

13. match_args <string>

14. Check whether given string is the answer.

15. match_args {{increase|decrease}} <integer>

16. Increase or decrease given integer by one.");

17. }

19. fn main() {

20. let args: Vec<String> = env::args().collect();

22. match args.len() {

23. // Pas d'arguments passés.

24. 1 => {

25. println!("My name is 'match_args'. Try passing some arguments!");

26. },

27. // Un seul argument passé.

28. 2 => {

29. match args[1].parse() {

30. Ok(42) => println!("This is the answer!"),

31. _ => println!("This is not the answer."),

32. }

33. },

34. // Une commande et un argument passé.

35. 3 => {

36. let cmd = &args[1];

37. let num = &args[2];

38. // On traite le nombre.

39. let number: i32 = match num.parse() {

40. Ok(n) => {

41. n

42. },

43. Err(_) => {

44. println!("error: second argument not an integer");

45. help();

46. return;

47. },

48. };

49. // On traite la commande.

50. match &cmd[..] {

51. "increase" => increase(number),

52. "decrease" => decrease(number),

53. _ => {

54. println!("error: invalid command");

55. help();

56. },

57. }

58. },

59. // On couvre tous les autres cas...

60. _ => {

61. // ... en affichant l'aide.

62. help();

63. }

64. }

65. }

1. $ ./match_args Rust

2. This is not the answer.

3. $ ./match_args 42

4. This is the answer!

5. $ ./match_args do something

6. error: second argument not an integer

7. usage:

8. match_args <string>

9. Check whether given string is the answer.

10. match_args {increase|decrease} <integer>

11. Increase or decrease given integer by one.

12. $ ./match_args do 42

13. error: invalid command

14. usage:

15. match_args <string>

16. Check whether given string is the answer.

17. match_args {increase|decrease} <integer>

18. Increase or decrease given integer by one.

19. $ ./match_args increase 42

20. 43

18-8 - FFI

Rust fournit une Interface pour Fonction Externe (« Foreign Function Interface », dans la langue de Shakespear) pour les bibliothèques écrites en C. Les fonctions externes peuvent être déclarées dans un bloc extern annoté de l'attribut #[link] contenant le nom de la bibliothèque externe.

1. // Dans le fichier ffi.rs

2. use std::fmt;

4. // Ce bloc externe lie la bibliothèque libm.

5. #[link(name = "m")]

6. extern {

7. // Ceci est une fonction externe

8. // qui calcule la racine carrée d'un nombre complexe à précision simple.

9. fn csqrtf(z: Complex) -> Complex;

10. }

12. fn main() {

13. // z = -1 + 0i

14. let z = Complex { re: -1., im: 0. };

16. // Appeler une fonction externe est une opération dite "à risque".

17. let z_sqrt = unsafe {

18. csqrtf(z)

19. };

21. println!("the square root of {:?} is {:?}", z, z_sqrt);

22. }

24. // Implémentation minimale d'un nombre complex à précision simple.

25. #[repr(C)]

26. #[derive(Clone, Copy)]

27. struct Complex {

28. re: f32,

29. im: f32,

30. }

32. impl fmt::Debug for Complex {

33. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

34. if self.im < 0. {

35. write!(f, "{}-{}i", self.re, -self.im)

36. } else {

37. write!(f, "{}+{}i", self.re, self.im)

38. }

39. }

40. }

$ rustc ffi.rs && ./ffi
the square root of -1+0i is 0+1i

Puisque l'appel de fonctions externes est considéré comme « à risque », il est courant d'écrire des wrappers sécurisés.

1. // Dans le fichier safe.rs

2. use std::fmt;

4. #[link(name = "m")]

5. extern {

6. fn ccosf(z: Complex) -> Complex;

7. }

9. // Wrapper sécurisé.

10. fn cos(z: Complex) -> Complex {

11. unsafe { ccosf(z) }

12. }

14. fn main() {

15. // z = 0 + 1i

16. let z = Complex { re: 0., im: 1. };

18. println!("cos({:?}) = {:?}", z, cos(z));

19. }

21. // Implémentation minimale d'un nombre complexe à précision simple.

22. #[repr(C)]

23. #[derive(Clone, Copy)]

24. struct Complex {

25. re: f32,

26. im: f32,

27. }

29. impl fmt::Debug for Complex {

30. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

31. if self.im < 0. {

32. write!(f, "{}-{}i", self.re, -self.im)

33. } else {

34. write!(f, "{}+{}i", self.re, self.im)

35. }

36. }

37. }

19 - Divers

Certaines sections ne sont pas tout à fait liées à la manière dont vous programmez mais vous introduisent et fournissent des outils facilitant les choses pour tout le monde. Ces sections abordent :

- La documentation : Générez la documentation d'une bibliothèque pour les utilisateurs via rustdoc ;

- Les tests : Créez des suites de tests pour garantir l'intégrité de vos bibliothèques ;

- Les benchmarks : Créez des benchmarks de vos fonctionnalités pour garantir leur performance.

19-1 - Documentation

Les commentaires de documentation sont très utiles pour d'importants projets nécessitant une documentation. Lorsque vous lancez Rustdoc, ce sont ces commentaires qui seront compilés dans la documentation. Ils sont préfixés par la séquence /// et supportent Markdown.

1. // #![crate_name = "doc"]

3. /// Un être humain est représenté ici.

4. pub struct Person {

5. /// Une personne doit avoir un nom.

6. name: String,

7. }

9. impl Person {

10. /// Renvoie une personne avec le nom qu'on lui a donné.

11. ///

12. /// # Arguments

13. ///

14. /// * `name` - Une slice qui contient le nom de la personne.

15. ///

16. /// # Example

17. ///

18. /// ```

19. /// // Vous pouvez écrire du code rust entre les balises

20. /// // dans les commentaires.

21. /// // Si vous passez --test à Rustdoc, il testera même la source pour vous !

22. /// use doc::Person;

23. /// let person = Person::new("name");

24. /// ```

25. pub fn new(name: &str) -> Person {

26. Person { name: name.to_string() }

27. }

29. /// Affiche un salut amical !

30. ///

31. /// Affiche "Hello, [name]" à l'objet `Person` en question.

32. pub fn hello(&self) {

33. println!("Hello, {}!", self.name);

34. }

35. }

37. fn main() {

38. let john = Person::new("John");

40. john.hello();

41. }

Note : si vous souhaitez compiler ce programme vous-même, n'oubliez pas de décommenter la ligne // #![crate_name = "doc"].

Pour lancer les tests, vous devez tout d'abord compiler le code en mode bibliothèque puis renseigner la position de la bibliothèque à rustdoc pour qu'il puisse la lier à chaque programme de la documentation :

rustc doc.rs --crate-type lib
rustdoc --test --extern doc="libdoc.rs"

Lorsque vous exécutez la commande cargo test sur une crate, Cargo générera et exécutera automatiquement les commandes respectives de rustc et rustdoc.

19-2 - Tests

Les fonctions peuvent être testées en utilisant ces attributs:

- #[test] désigne une fonction comme test unitaire. La fonction ne doit prendre aucun paramètre et ne rien renvoyer ;

- #[should_panic] désigne une fonction comme un test voué à l'échec.

1. // Dans le fichier unit_test.rs

2. // Compile `main` à condition que la compilation des tests ne soit pas activée.

3. #[cfg(not(test))]

4. fn main() {

5. println!("If you see this, the tests were not compiled nor ran!");

6. }

8. // Compile le module `test` seulement si la compilation des tests est activée.

9. #[cfg(test)]

10. mod test {

11. // Un test unitaire `distance_test` est nécessaire.

12. fn distance(a: (f32, f32), b: (f32, f32)) -> f32 {

13. (

14. (b.0 - a.0).powi(2) +

15. (b.1 - a.1).powi(2)

16. ).sqrt()

17. }

19. #[test]

20. fn distance_test() {

21. assert!(distance((0f32, 0f32), (1f32, 1f32)) == (2f32).sqrt());

22. }

24. #[test]

25. #[should_panic]

26. fn failing_test() {

27. assert!(1i32 == 2i32);

28. }

29. }

Les tests peuvent être exécutés avec la commande cargo test ou rustc --test.

1. $ rustc --test unit_test.rs

2. $ ./unit_test

4. running 2 tests

5. test test::distance_test ... ok

6. test test::failing_test ... ok

8. test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured

Si --test n'a pas été inclut alors il devrait se passer ceci :

$ rustc unit_test.rs
$ ./unit_test
If you see this, the tests were not compiled nor ran!

Voir aussi

Les attributs, la compilation conditionnelle et mod.

20 - Opérations à risque

Pour reprendre ce que la documentation officielle dit : « Il faudrait essayer de minimiser la quantité de code à risque dans la base du code. » Avec ceci en tête, commençons ! Les blocs unsafe en Rust sont utilisés pour contourner les protections mises en place par le compilateur ; plus précisément, il y a quatre principaux cas d'utilisation que nous pouvons retrouver dans les blocs unsafe :

1. Le déréférencement des pointeurs bruts ;

2. L'appel d'une fonction à partir de la FFI (cette partie est couverte à d'autres endroits dans le livre) ;

3. La modification des types par le biais de std::mem::transmute ;

4. Inliner de l'assembleur.

Les pointeurs bruts

Les pointeurs bruts * et références &T fonctionnent de la même manière mais les références sont toujours sécurisées parce qu'elles garantissent de pointer sur une ressource valide grâce au vérificateur d'emprunts. Le déréférencement d'un pointeur brut ne peut se faire que par le biais d'un bloc unsafe.

1. // Dans le fichier pointer.rs

2. fn main() {

3. let raw_p: *const u32 = &10;

5. unsafe {

6. assert!(*raw_p == 10);

7. }

8. }

Transmuter

Permet une simple conversion d'un type à l'autre, cependant les deux types doivent disposer de la même taille en mémoire et le même alignement :

1. // Dans le fichier transmute.rs

2. fn main() {

3. let u: &[u8] = &[49, 50, 51];

5. unsafe {

6. assert!(u == std::mem::transmute::<&str, &[u8]>("123"));

7. }

8. }

21 - Pour aller plus loin

Pour en savoir plus à propos des exemples présentés, n'oubliez pas d'installer Rust sur votre machine et de visiter la documentation officielle. Pour les plus curieux, vous pouvez également consulter le code source du site.

Vous pouvez partager vos avis et suggestions sur le forum.

23 - Remerciements

Developpez.com remercie l'équipe de Rust pour la mise à disposition de ce cours pratique. Nos remerciements également à Songbird pour la traduction française, et à Jacques Jean pour la relecture orthographique.

(1)Consultez cette section pour plus de détails.