I. Préambule

Ce document est une traduction du gitbook Rust by Example, vous serez donc très régulièrement invités dans cette ressource à exécuter les exemples proposés sur le bac à sable dédié au langage, pour respecter l'aspect « pratique » du livre d'origine.

II. Premier programme « Hello world »

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

Hello World
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
// Ceci est un commentaire et sera ignoré par le compilateur.

// Ceci est la fonction principale
fn main() {
// Toutes les déclarations se trouvant dans le corps de la fonction 
seront exécutées lorsque le binaire est exécuté.
// Afficher du texte dans la console.
    println!("Hello World!");
}
Bouton pour exécuter l'exemple proposé

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

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

 
Sélectionnez
1.
$ rustc hello.rs

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

 
Sélectionnez
1.
2.
$ ./hello
Hello World!

II-A. Exercice

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 :

 
Sélectionnez
1.
2.
Hello World!
I'm a Rustacean!

Cet exercice est également disponible dans la section exercices de la rubrique Rust !

III. 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 monolignes.,
    • /* 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.*/.

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
fn main(){
    // En général, le marqueur '{}' sera automatiquement remplacé par 
    // n'importe quel argument. Il sera transformé en chaîne de caractères.
    println!("{} jours", 31);
    
    // Sans suffixe, 31 est de type i32. Vous pouvez changer le type de 31 avec
    // un suffixe. (e.g. 31i64)
    
    // Différents modèles peuvent être utilisés. 
    // Les marqueurs de position peuvent être utilisés.
    println!("{0}, voici {1}. {1}, voici {0}", "Alice", "Bob");
    
    // Les marqueurs peuvent également être
    // nommés
    println!("{sujet} {verbe} {objet}", 
    objet="le chien paresseux",
    sujet="Rapide, le renard",
    verbe="saute par-dessus");
    
    // Un formatage spécial peut être spécifié après un ':'.
    println!("{} personne sur {:b} sait lire le binaire, l'autre moitié non.", 1, 2);
    
    // Vous pouvez aligner vers la droite votre texte en spécifiant 
    // la largeur (en espace) entre le côté gauche de la console 
    // et votre chaîne. Cet exemple affichera: "     1", un "1" après 5 espaces.

    println!("{number:>width$}", number=1, width=6);
    
    // Vous pouvez également remplacer les white spaces par des '0'.
    // Affiche: "000001"
    
    println!("{number:>0width$}", number=1, width=6);
    
    // Le nombre d'arguments utilisés est vérifié par le compilateur.
    println!("Mon nom est {0}, {1} {0}", "Bond");
    // FIXME ^ Ajoutez l'argument manquant: "James".
    
    // On crée une structure nommée 'Structure' contenant un entier de type 'i32'.
    #[allow(dead_code)]
    struct Structure(i32);
    
    // Cependant, les types complexes tels que les structures demandent
    // une gestion de l'affichage plus complexe. Cela ne fonctionnera pas.
    println!("Cette structure '{}' ne sera pas affichée...", Structure(3));
    // FIXME ^ Commentez cette ligne pour voir l'erreur disparaître.
    
}
Le bouton pour exécuter l'exemple proposer

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.

IV-A. Activités

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::fmtDocumentation du module pour configurer le nombre de décimales à afficher).

Cet exercice est également disponible dans la section exercices de la rubrique Rust !

IV-B. Voir aussi

std::fmtDocumentation du module, [lien interne vers les macros], les structuresLes structures, [lien interne vers les traits].

IV-C. 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.

Exemple d'utilisation du trait Debug
CacherSélectionnez

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

Formatage de types standards et personnalisés
CacherSélectionnez
Le bouton pour exécuter l'exemple proposé

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.

IV-C-1. Voir aussi

attributesLien vers la référence anglaise de Rust, [lien interne vers l'attribut derive], std::fmt, [lien interne vers les structures].

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

 
CacherSélectionnez

fmt::Display pourrait être plus lisible que fmt::Debug, mais il présente un problème pour la bibliothèque standard. Comment les types ambigus 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é.

Exemple d'utilisation simultanée des traits Display et Debug.
CacherSélectionnez
Le bouton pour exécuter l'exemple proposé

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

std::fmt possède de nombreux [traits](lien interne vers les traits) et chacun doit posséder sa propre implémentation. Pour plus d'informations, nous vous invitons à consulter la documentation du moduleDocumentation du module fmt..

IV-D-1. 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 :

Résultat attendu.
Sélectionnez
1.
2.
Display: 3.3 + 7.2i
Debug: Complex { real: 3.3, imag: 7.2 }

Cet exercice est également disponible dans la section exercices de la rubrique Rust !

IV-D-2. Voir aussi

[lien interne vers l'attribut derive], std::fmt, [lien interne vers les macros], les structuresLes structures, [lien interne vers les traits], le mot-clé useLa déclaration use.

IV-D-2-a. Exemple d'utilisation : La structure List

Implémenter le trait fmt::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 demande 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 :

Utilisation de la macro try!.
CacherSélectionnez

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

Utilisation de l'opérateur '?'.
CacherSélectionnez

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

Implémentation du trait Display pour la structure List.
CacherSélectionnez
Image non disponible
IV-D-2-a-i. 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 :

Ensemble index->valeur
Sélectionnez
1.
[0: 1, 1: 2, 2: 3]

Cet exercice est également disponible dans la section exercices de la rubrique Rust !

IV-D-2-a-ii. Voir aussi

La boucle forLa boucle for et les intervalles, [lien interne vers le mot-clé ref], [lien interne vers l'enum Result], les structuresLes structures, [lien interne vers la macro try!], [lien interne vers la macro vec!].

IV-E. 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. {}).

 
CacherSélectionnez
Bouton pour exécuter l'exemple proposé

N'hésitez pas à consulter la liste complète des traitsListe exhaustive des traits et de leurs arguments. dédiés au formatage ainsi que leurs types d'argument dans la documentation du module std::fmtLien vers la page principale de la documentation du module..

IV-E-1. Activité

Implémentez le trait fmt::Display pour la structure Color dans l'exemple ci-dessous.

Résultat attendu:
Sélectionnez
1.
2.
3.
RGB (128, 255, 90) 0x80FF5A
RGB (0, 3, 254) 0x0003FE
RGB (0, 0, 0) 0x000000

Indices :

IV-E-2. Voir aussi

V. 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és. Les nombres peuvent être également 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 que les réels sont typés f64.

Exemple de typage explicite et implicite
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
fn main() {
    // Le type des variables peut être spécifié, annoté.
    let logical: bool = true;

    let a_float: f64 = 1.0;  // typage classique
    let an_integer   = 5i32; // typage par suffixe

    // Le type par défaut peut également être conservé.
    // typage implicite
    let default_float   = 3.0; // `f64`
    let default_integer = 7;   // `i32`

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

    // Erreur! Le type d'une variable ne peut être modifié en cours de route.
    mutable = true;
}
Bouton pour exécuter l'exemple proposé

V-A. Voir aussi

V-B. 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 () 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 ».

 
CacherSélectionnez
Image non disponible

V-C. Les tuples

Un tuple est une collection de valeurs de différents types. 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é.

Exemple d'utilisation des tuples:
CacherSélectionnez
Image non disponible

V-C-1. 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 :

Résultat attendu:
Sélectionnez
1.
2.
( 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 DisplayDisplay.

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

 
Sélectionnez
1.
2.
 println!("Matrix:\n{}", matrix);
println!("Transpose:\n{}", transpose(matrix));
Résultat attendu:
CacherSélectionnez

V-D. Les tableaux et les « slices »

Un tableau est une collection d'objets appartenant au même type T, contenu dans un bloc de 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 », « fragment ». 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].

 
CacherSélectionnez
Image non disponible

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

VI-A. Les structures

Il y a trois types de structures pouvant être créés 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 « unit structs » (que nous pourrions traduire par « structure unitaire »), ne possèdent aucun champ et sont donc très utiles pour la généricité.
Exemple d'utilisation des différents types de structures
CacherSélectionnez
Image non disponible

VI-A-1. Activité

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

    Vraiment coincé pour cette première activité ? Nous vous proposons une solution sur laquelle vous appuyer pour la suite :

    Template pour le deuxième tp:
    CacherSélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    41.
    42.
    43.
    44.
    45.
    46.
    47.
    48.
    49.
    50.
    51.
    52.
    53.
    54.
    55.
    56.
    57.
    58.
    59.
    60.
    61.
    62.
    63.
    64.
    65.
    66.
    67.
    68.
    69.
    70.
    71.
    72.
    73.
    74.
    75.
    76.
    77.
    78.
    79.
    80.
    81.
    82.
    83.
    84.
    85.
    86.
    87.
    88.
    89.
    90.
    91.
    92.
    93.
    94.
    95.
    96.
    97.
    98.
    99.
    100.
    101.
    // Une structure unitaire. 
    struct Nil;
    
    // Un tuple.
    struct Pair(i32, f32);
    
    // Une structure avec deux champs.
    struct Point {
        x: f32,
        y: f32,
    }
    
    // Les structures peuvent faire partie des champs d'une autre structure.
    #[allow(dead_code)]
    struct Rectangle {
        p1: Point,
        p2: Point,
    }
    
    /// Renvoie l'aire d'un rectangle.
    /// ## Exemple
    /// ```rust
    /// let point1: Point = Point{x: 1.0, y: 1.0};
    /// let point2: Point = Point{x: 5.0, y: 6.0};
    /// let rec = Rectangle{p1: point1, p2: point2};
    /// println!("rec area:{}", rec_area(rec)); // renvoie `20`.
    /// ```
    fn rec_area(rec: Rectangle) -> f32{
        let Rectangle{p1: first_point, p2: second_point} = rec;
        let Point{x: first_point_x, y: first_point_y} = first_point;
        let Point{x: second_point_x, y: second_point_y} = second_point;
        let rec_width: f32 = Math::max(first_point_x, second_point_x) - Math::min(first_point_x, second_point_x);
        let rec_height: f32 = Math::max(first_point_y, second_point_y) - Math::min(first_point_y, second_point_y);
        rec_width * rec_height
    }
    
    struct Math;
    
    impl Math{
        /// Renvoie la plus petite des deux valeurs.
        /// ## Exemple
        /// ```rust
        /// let foo = 1.0;
        /// let bar = 3.0;
        /// println!("smallest value:{}", Math::min(foo, bar)); // renvoie 1.0
        /// ```
        pub fn min(v1: f32, v2: f32) -> f32{
            if v1 > v2{
                return v2;
            }else { return v1; }
        }
        /// Renvoie la plus grande des deux valeurs.
        /// ## Exemple
        /// ```rust
        /// let foo = 1.0;
        /// let bar = 3.0;
        /// println!("greater value:{}", Math::max(foo, bar)); // renvoie 3.0
        /// ```
        pub fn max(v1: f32, v2: f32) -> f32{
            if v1 > v2{
                return v1;
            }else { return v2; }
        }
    }
    
    fn main() {
        // On instancie la structure `Point`.
        let point: Point = Point { x: 0.3, y: 0.4 };
    
        // On accède aux champs du point.
        println!("point coordinates: ({}, {})", point.x, point.y);
    
        // On décompose les champs de la structure pour les assigner 
        // à de nouvelles variables (i.e. my_x et my_y)
        let Point { x: my_x, y: my_y } = point;
    
        let _rectangle = Rectangle {
            // L'instanciation de la structure est également une expression.
            p1: Point { x: my_y, y: my_x },
            p2: point,
        };
    
        // On instancie la structure unitaire, vide.
        let _nil = Nil;
    
        // On instancie un tuple.
        let pair = Pair(1, 0.1);
    
        // On accède aux champs du tuple.
        println!("pair contains {:?} and {:?}", pair.0, pair.1);
    
        // On décompose un tuple.
        let Pair(integer, decimal) = pair;
    
        println!("pair contains {:?} and {:?}", integer, decimal);
        
        let point1: Point = Point{x: 1.0, y: 1.0};
        let point2: Point = Point{x: 5.0, y: 6.0};
        let rec = Rectangle{p1: point1, p2: point2};
        println!("rec area:{}", rec_area(rec));
    }
    
  2. Ajoutez une fonction square qui prend en paramètre une instance de la structure et un réel de type f32, et 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.

Vous ne parvenez pas à trouver la solution ?

Solution proposée:
CacherSélectionnez

VI-A-2. Voir aussi

VI-B. 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 (cf. les variantes de structuresLes structures si cela vous paraît flou).

 
CacherSélectionnez
Image non disponible

VI-B-1. Voir aussi

Les attributsLes attributs, le mot-clé matchLe pattern matching, le mot-clé fnLes fonctions, [lien interne vers les chaînes de caractères].

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

VI-B-2. Le mot-clé use

Exemple d'utilisation du mot-clé use:
CacherSélectionnez

VI-B-2-a. Voir aussi

VI-B-3. 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).

Démonstration des différentes syntaxes des énumérations:
CacherSélectionnez
Image non disponible

VI-B-3-a. Voir aussi

VI-B-4. 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 :

 
CacherSélectionnez
Image non disponible

VI-B-4-a. Voir aussi

[lien interne vers la structure Box], les méthodesLes méthodes.

VI-B-5. Les constantes

Rust possède deux types de constantes qui peuvent être déclarées dans n'importe quel « scope » 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.

Exemple d'utilisation des constantes et de la lifetime static:
CacherSélectionnez
Image non disponible

VI-B-5-a. Voir aussi

La RFCRequest For Comments (Appel à révision) des mot-clés const et staticLien vers le document de la RFC en question, [lien interne vers la lifetime ‘static].

VII. Les assignations

Rust assure l'immutabilité du type d'une variable grâce au typage statique. Lorsqu'une variable est déclarée, elle peut être annotée (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 liées à des variables en utilisant le mot-clé let.

 
CacherSélectionnez
Image non disponible

VII-A. Mutabilité

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

Mutabilité des variables
CacherSélectionnez
Image non disponible

Le compilateur vous renverra un diagnostic complet à propos des erreurs de mutabilité, si il y en a.

VII-B. Scope et shadowing

Les assignations possèdent un contexte (« scope ») 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 dans des accolades {}. Le shadowing(1) est permis.

Fonctionnement des contextes et du shadowing:
CacherSélectionnez
Image non disponible

VII-C. 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).

Utilisation de variables non-initialisées:
CacherSélectionnez
Image non disponible

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.

VIII. La coercition

Rust fournit le mot-clé as pour la conversion entre types primitifs. Notez toutefois que la conversion implicite n'est pas supportée par le langage.

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.

 
CacherSélectionnez
Image non disponible

VIII-A. 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. Si 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.

Exemple d'utilisation des suffixes:
CacherSélectionnez
Image non disponible

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 [lien interne vers le concept de borrowing] ;

  • 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 modulesLes modules et/ou les « crates »Les « crates ».

VIII-B. L'inférence des types

Le moteur dédié à l'inférence des types est assez 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 :

 
CacherSélectionnez
Image non disponible

Aucune annotation de type n'était nécessaire, le compilateur est heureux et le programmeur aussi !

VIII-C. 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. Les exceptions à cette règle sont les types primitifs : usize, f32, etc.

Exemple d'utilisation du mot-clé type:
CacherSélectionnez
Image non disponible

VIII-C-1. Voir aussi

IX. Les expressions

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

 
CacherSélectionnez

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

 
CacherSélectionnez

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 ().

 
CacherSélectionnez
Image non disponible

X. 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 !

X-A. 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.

Branchements conditionnels
CacherSélectionnez
Image non disponible

X-B. 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.

Utilisation du mot-clé loop
CacherSélectionnez
Bouton pour exécuter l'exemple

X-B-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.

Exemple d'utilisation des labels
CacherSélectionnez
Bouton pour exécuter l'exemple proposé

X-C. 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 FizzBuzzLes règles du jeu FizzBuzz en utilisant une boucle while :

FizzBuzz
CacherSélectionnez
Image non disponible

X-D. La boucle for et les intervalles

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.

Le jeu du FizzBuzz avec une boucle for
CacherSélectionnez
Image non disponible

X-D-1. Voir aussi

[lien interne vers la structure Iterator]

X-E. 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.

Exemple d'utilisation du pattern matching
CacherSélectionnez
Image non disponible

X-E-1. La déstructuration

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

X-E-1-a. Les tuples

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

Déstructuration de tuples
CacherSélectionnez
Image non disponible
X-E-1-a-i. Voir aussi

X-E-1-b. Les énumérations

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

Déstructuration d'une énumération
CacherSélectionnez
Image non disponible
X-E-1-b-i. Voir aussi

X-E-1-c. 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.
Déstructuration et déréférencement
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
 fn main() {
    // Assigne une référence de type `i32`. Le `&` signifie qu'une 
    // référence est assignée.
    // L'équivalent non raccourci de cette assignation pourrait ressembler à ceci:
    // ```rust
    // let _reference: i32 = 4;
    // let reference: &i32 = &_reference;
    // ```
    let reference: &i32 = &4;

    match reference {
        // Lorsque `reference` est comparé à `&val`, la comparaison ressemble à ceci:
        // `&i32`
        // `&val` <- `val` est plus ou moins une représentation de `reference`.
        // ^ Nous remarquons que si le `&` est omis, la valeur devrait être 
        // assignée à `val`.
        &val => println!("Got a value via destructuring: {:?}", val),
    }

    // Pour éviter d'utiliser la référence, vous pouvez déréférencer `reference` 
    // avant analyse (vous permettant d'opérer sur la valeur, si elle est mutable).
    match *reference {
        val => println!("On récupère la valeur déréférencée: {:?}", val),
    }

    // Que se passe-t-il si vous ne créez pas une référence ? `reference` 
    // était une référence parce que la "r-value" était une référence. Cette 
    // variable n'en est pas une parce que la valeur de droite n'en est pas une.
    let _not_a_reference = 3;

    // Rust fournit le mot-clé `ref` dans ce but. Il modifie l'assignation 
    // de manière à créer une référence pour l'élément; cette référence est assignée.
    let ref _is_a_reference = 3;

    // Bien entendu, en assignant deux valeurs sans références, ces dernières
    // peuvent être récupérées à l'aide du mot-clé `ref` et `ref mut`.
    let value = 5;
    let mut mut_value = 6;

    // On utilise le mot-clé `ref` pour créer une référence.
    match value {
        ref r => println!("On récupère une référence de la valeur : {:?}", r),
    }

    // `ref mut` s'utilise de la même manière.
    match mut_value {
        ref mut m => {
            // On obtient une référence. Nous allons déréférencer `m` avant 
            // de pouvoir opérer.
            *m += 10;
            println!("Nous incrémentons de 10. `mut_value`: {:?}", m);
        },
    }
}
Image non disponible

X-E-1-d. Les structures

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

Déstructuration d'une structure
CacherSélectionnez
Image non disponible
X-E-1-d-i. Voir aussi

Les structuresLes structures, [lien interne vers le mot-clé ref].

X-E-2. Les gardes

Lorsque vous usez du pattern matching, un « garde » (qui est ni plus ni moins une structure conditionnelle) peut être ajouté dans chaque branche du match.

Exemple de gardes
CacherSélectionnez
Bouton pour exécuter l'exemple proposé

X-E-2-a. Voir aussi

X-E-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 :

Exemple d'assignation dans un match
CacherSélectionnez
Image non disponible

X-E-3-a. Voir aussi

X-E-4. if let

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

Exemple d'utilisation de if let
CacherSélectionnez

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

Exemple d'utilisation de if let
CacherSélectionnez
Image non disponible

X-E-4-a. Voir aussi

Les énumérationsLes énumérations, [lien interne vers la structure Option] et la RFC de if let.

X-E-5. 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 :

Version "lourde"
CacherSélectionnez

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

Exemple d'utilisation de while let
CacherSélectionnez
Image non disponible

X-E-5-a. Voir aussi

Les énumérationsLes énumérations, [lien interne vers la structure Option] et la RFC de while let.

XI. 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. Également, il est 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 !

Exemple d'utilisation des fonctions
CacherSélectionnez
Bouton pour exécuter l'exemple proposé

XI-A. 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'à ces autres méthodes par le biais du mot-clé self. Les méthodes sont déclarées dans un bloc impl.

Exemple d'utilisation des méthodes
CacherSélectionnez

Oups ! Cet exemple est trop long pour être publié par le biais d'un lien, n'hésitez pas à le copier/coller sur https://play.rust-lang.org/Site officiel du compilateur Rust en ligne !

Définition : valeur absolue.

XI-B. Les closures

Les closures en Rust, également appelées « lambdas », sont des fonctions qui peuvent capturer l'environnement que les entoure. Par exemple, voici une closure qui capture la variable x :

 
CacherSélectionnez

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.

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.
Exemple d'utilisation des closures
CacherSélectionnez
Image non disponible

XI-B-1. Capture

Les closures sont naturellement « souples » 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.

Gestion des ressources dans les closures
CacherSélectionnez
Image non disponible

XI-B-1-a. Voir aussi

XI-B-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 à la manière à adopter, en fonction du contexte.

C'est pourquoi si un « déplacement » (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 :

Les types de captures utilisés par le compilateur
CacherSélectionnez
Image non disponible

XI-B-2-a. Voir aussi

XI-B-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ériqueLa généricité pour définir le « régime » de capture que la closure adoptera :

Paramètre générique
CacherSélectionnez

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> non délimité(2) 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.

Implémentation implicite du trait Fn
CacherSélectionnez
Image non disponible

XI-B-3-a. Voir aussi

XI-B-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.

Fonction passée en paramètre
CacherSélectionnez
Bouton pour exécuter l'exemple

XI-B-4-a. Voir aussi

XI-B-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 tagué 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.

Capture par valeur des variables contenues dans la fonction
CacherSélectionnez
Image non disponible

XI-B-5-a. Voir aussi

XI-B-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.

XI-B-6-a. 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édicatExemple de ce qu'est un prédicat, autrement false. Voici sa signature :

 
CacherSélectionnez
Exemple d'utilisation de la fonction any
CacherSélectionnez
Image non disponible
XI-B-6-a-i. Voir aussi

XI-B-6-b. Iterator::find

Iterator::find est une fonction qui renvoie le premier élément correspondant au prédicat.

Signature de la fonction Iterator::find
CacherSélectionnez
Exemple d'utilisation de la fonction Iterator::find
CacherSélectionnez
Image non disponible
XI-B-6-b-i. Voir aussi

XI-C. Les fonctions d'ordre supérieur

Rust supporte les fonctions d'ordre supérieur (HOFHigher Order Function). Ces fonctions prennent en paramètre une ou plusieurs fonctions et renvoie une autre fonction. Ce sont les HOF ainsi que les « itérateurs fainéants(3) » qui donnent cet « aspect » fonctionnel à Rust.

Somme de tous les carrés des nombres impairs en-dessous de 1000
CacherSélectionnez
Image non disponible

L'énumération OptionDocumentation officielle de la structure Option et le trait IteratorDocumentation officielle du trait Iterator implémentent leur lot d'HOFHigher Order Functions.

XII. 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'items(4).

XII-A. 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.

Exemple de hiérarchie de modules
CacherSélectionnez
Image non disponible

XII-B. 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, seulement 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).

Encapsulation
CacherSélectionnez
Image non disponible

XII-B-1. Voir aussi

XII-C. 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.

Exemple d'utilisation de la déclaration use
CacherSélectionnez
Image non disponible

XII-D. 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(5).

Exemple d'utilisation des mot-clés super et self
CacherSélectionnez
Image non disponible

XII-E. 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éLa visibilité en un ensemble de fichiers :

Squelette du projet
CacherSélectionnez
Dans le fichier split.rs
CacherSélectionnez
Dans le fichier my/mod.rs
CacherSélectionnez
Dans le fichier my/nested.rs
CacherSélectionnez
Dans le fichier my/inaccessible.rs
CacherSélectionnez
Vérifions que tout fonctionne comme
CacherSélectionnez

XIII. Les « crates »

Un crate est une unité de compilationhttps://www.techopedia.com/definition/23963/compilation-unit-programming, en Rust. Lorsque rustc some_file.rs est appelé, some_file.rs est considéré comme étant un «fichier paquet»(6). 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.

Un crate peut être compilé 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.

XIII-A. Créer une bibliothèque

Commençons pas créer une bibliothèque, dont nous nous servirons ensuite pour la rattacher à un autre crate(« paquet »).

Dans le fichier rary.rs
CacherSélectionnez
 
CacherSélectionnez

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 le paquet (en l'occurrence rary.rs). Ce comportement peut, bien entendu, être modifié en utilisant l'attribut [lien interne vers l'attribut crate_name].

XIII-B. La déclaration extern crate

Pour attacher un 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.

Dans le fichier executable.rs
CacherSélectionnez
 
CacherSélectionnez

XIV. Les attributs

Un attribut est une méta-donnée pouvant être appliquée à plusieurs sortes d'items(7). Ces méta-données peuvent être utilisées pour :

Quand les attributs sont appliqués à un crate tout entier, leur syntaxe est la suivante #![crate_attribute]. Lorsqu'ils sont appliqués à un module ou un autre item, 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)].

XIV-A. L'avertissement `dead_code`

La compilateur fournit la lint dead_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.

Exemple d'utilisation de l'attribut #[allow()]
CacherSélectionnez
Image non disponible

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.

XIV-B. Méta-données relatives aux crates

L'attribut crate_type peut être utilisé pour renseigner, au compilateur, le type du crate (exécutable ou bibliothèque(et quel type de bibliothèque)) et l'attribut crate_name est utilisé pour renseigner le nom du crate.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
// lib.rs
// Ce crate est une bibliothèque.
#![crate_type = "lib"]
// Cette bibliothèque est nommée "rary".
#![crate_name = "rary"]

pub fn public_function() {
    println!("called rary's `public_function()`");
}

fn private_function() {
    println!("called rary's `private_function()`");
}

pub fn indirect_access() {
    print!("called rary's `indirect_access()`, that\n> ");

    private_function();
}

Lorsque l'attribut crate_type est utilisé, vous n'avez, bien entendu, plus besoin de passer le flag --crate-type à rustc.

Le flag --crate-type peut être omis
CacherSélectionnez

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

Exemple d'utilisation de l'attribut cfg
CacherSélectionnez
Image non disponible

XIV-C-1. Voir aussi

XIV-C-2. 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.

Dans le fichier custom.rs
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
// custom.rs
#[cfg(some_condition)]
fn conditional_function() {
    println!("condition met!")
}

fn main() {
    conditional_function();
}

Sans le flag personnalisé :

 
CacherSélectionnez

Avec le flag personnalisé :

 
Sélectionnez
$ rustc --cfg some_condition custom.rs && ./custom
condition met!

XV. 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 des) paramètre(s) passé(s) à 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(s) 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) :

Exemple de fonction générique
CacherSélectionnez
Création de types concrets et génériques
CacherSélectionnez
Image non disponible

XV-A. Voir aussi

XV-B. 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, ...>().

Exemple d'utilisation des fonctions génériques
CacherSélectionnez
Image non disponible

XV-B-1. Voir aussi

XV-C. Implémentation générique

Tout comme les fonctions, les implémentations nécessitent quelques précisions pour être génériques.

Exemple d'implémentations génériques
CacherSélectionnez
Image non disponible

XV-C-1. Voir aussi

[lien vers la section Lifetime pour les fonctions], implLes méthodes et les structuresLes structures.

XV-D. 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 ayant pour fonction de « drop (10)» l'instance qui l'appelle ainsi qu'un paramètre passé.

Exemple d'utilisation de la généricité avec les traits
CacherSélectionnez
Image non disponible

XV-D-1. Voir aussi

La documentation du trait Drop, le chapitre sur les structuresLes structures et [lien vers le chapitre dédié aux traits].

XV-E. 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 de quel trait va-t-il hériter les services(11). 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.

Exemple d'utilisation des restrictions
CacherSélectionnez

Les ressources passées en paramètre sont toutes soumises à ces restrictions et doivent forcément remplir les conditions :

Structure qui n'implémente pas le trait requis
CacherSélectionnez

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 :

 
CacherSélectionnez
Image non disponible

Les conditions d'entrée pour les paramètres génériques peuvent également être spécifiées en utilisant le mot-clé whereLa condition where, les rendant plus explicites, plus lisibles.

XV-E-1. Voir aussi

Les traits dédiés à l'affichage et le formatage std::fmtAffichage formaté, les structuresLes structures et [lien interne vers les traits].

XV-F. 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é(12), 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.

Utilise des traits comme "filtres"
CacherSélectionnez
Image non disponible

XV-F-1. Voir aussi

La documentation du trait EqDocumentation officielle du trait Eq, la documentation du trait OrdDocumentation officielle du trait Ord et [lien interne vers les traits].

XV-G. 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 ,.

Restrictions multiples
CacherSélectionnez
Image non disponible

XV-G-1. Voir aussi

std::fmtAffichage formaté, [lien interne vers les traits].

XV-H. La condition where

Une restriction peut également être explicitée par la condition 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 :
 
CacherSélectionnez
  • lorsque d'autres types sont ajoutés aux restrictions :
Exemple d'utilisation de la condition where
CacherSélectionnez
Image non disponible

À propos de « ou adopter une autre approche », rien n'est précisé mais je tenais à présenter un cas de figure où la condition where n'est pas utilisée (et où l'approche est procédurale).

 
CacherSélectionnez

XV-H-1. Voir aussi

XV-I. Les items associés

Le concept « d'items associés » est un ensemble de règles appliquées sur différents types d'itemsLien vers la référence de Rust. C'est une extension des traits génériques, leur permettant de définir de nouveaux « items » 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.

XV-I-1. Voir aussi

L'introduction étant très abstraite (le terme « item » couvrant en réalité beaucoup de mécanismes présents dans le langage), je vous recommande vivement de consulter les explications fournies dans la RFC de cette proposition (il y a aussi de nombreux exemples pour illustrer le champ d'action du concept).

XV-I-2. 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 génériques de données supportés par le conteneur.

Dans l'exemple ci-dessous, le trait Contains permet l'utilisation des types génériques A et B. Le trait est alors implémenté pour le type (la structure) 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.

Spécification redondante des génériques
CacherSélectionnez
Image non disponible

XV-I-2-a. Voir aussi

Les structuresLes structures et [lien interne vers les traits].

XV-I-3. 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 :

Déclaration des types de sortie
CacherSélectionnez

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 :

Les différentes syntaxe (avec et sans les types associés)
CacherSélectionnez

Éditons l'exemple de la section précédente en utilisant les types associés :

Exemple d'utilisation des types associés
CacherSélectionnez
Image non disponible

XV-J. Les types de 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.

Exemple d'utilisation des paramètres fantômes
CacherSélectionnez