Installation et compilation avec Rust
Installation et compilation avec Rust
P our réaliser et tester tous les programmes présents lors de cette étude, il est bien entendu évident que vous devez installer
la structure, les bibliothèque et les outils du langage Rust. Pour cela, je vous invite à tapez la commande suivante :
> curl --proto '=https' --tlsv1.2 -sSf [Link] | sh
Cela prend un certain temps, mais reste relativement rapide. Vous pouvez dès lors savoir si l’installation s’est déroulée
correctement et connaître la version installée grâce à la commande suivante :
> rustc –version
rustc 1.53.0 (53cb7b09b 2021-06-17)
Si après coup, vous souhaitez désinstaller complètement Rust, utilisez alors la commande suivante
> rustup self uninstall
Cet outil est également utile pour mettre à jour vers une nouvelle version qui vient de paraître, voici alors la commande :
> rustup update
Lors des différentes études proposées sur ce langage, vous verrez que nous aurons souvent besoin de tenir compte de
librairies tierces (qui s’appellent des caisses). La mise en place de ces librairies ainsi que leurs installations automatique se
fait directement dans le fichier de projet qui s’appelle systématiquement « [Link] » et dans la rubrique [dependencies].
Vous placez le nom de la bibliothèque suivi du numéro de version. La documentation de Rust vous fourni alors le numéro
exact à placer. Dans certains cas, vous devez proposer plusieurs réglages pour une même librairie, vous devez alors mettre
en place une dépendance spécifique en rajoutant le nom de la librairie à la suite de dépendencies, voici un exemple :
[Link]
[package]
name = "test-rust"
version = "0.1.0"
authors = ["manu"]
edition = "2018"
Cross-compilation
L es outils de Rust sont relativement puissants et complets. Ils permettent notamment de réaliser des programmes pour
plusieurs cibles différents et donc de faire de la compilation croisée sur le même poste de travail.
Il est nécessaire au préalable d’installer les compilateurs pour chacune des cibles souhaitées et ceci toujours grâce au
même outil rustup. Dans la documentation de Rust, vous avez une très grande liste de cibles. Pour compiler un programme
pour qu’il fonction sur une Raspberry, vous devez prendre le compilateur armv7-unknown-linux-gnueabihf :
> rustup target add armv7-unknown-gnueabihf
Pour que cela fonctionne parfaitement, il ne faut pas oublier d’installer également le cross-compilateur C pour votre cible
choisie, ce qui se fait dans l’environnement Linux avec la commande suivante.
> sudo apt install crossbuild-essential-armhf
Les commandes précédentes, bien entendu, sont à faire qu’une seule fois sur votre ordinateur hôte. Par contre, par défaut,
lorsque vous exécutez un projet, la cible reste votre ordinateur hôte. Si vous souhaitez changer de cible, vous devez rajouter
dans votre projet un répertoire « .cargo » à l’intérieur duquel vous placez le fichier « [Link] » avec le script suivant :
[Link]
[build]
target = "armv7-unknown-linux-gnueabihf"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
Lorsque dès lors vous construisez votre projet, le système tient compte de ce fichier, et la compilation correspond par
exemple à un exécutable de type Raspberry. Il suffit alors de le déployer avec les commandes classiques de copie
sécurisée :
> scp test-rust pi@[Link]:/home/pi/rust
Page 1/15
Cours Introduction au langage Rust
P our conclure, je vous prose de donner des captures d’écran à la suite d’une compilation croisée sur une cible de type
Raspberry, pour des processeurs de type ARMv7 :
Page 2/15
Cours Introduction au langage Rust
µProcesseur
0
ensemble de fils de connexion, et c’est cette information qui est numérique. En fait, le 1
système contrôle la présence ou pas d’un courant sur chacun des fils de connexion.
Mémoire
1
La valeur même de l’intensité du courant n’a pas d’importance. Ce qui importe c’est de 1
savoir uniquement si le courant passe ou pas. Nous avons coutume d’utiliser la
1
numération binaire pour représenter cet état de fait. La présence d’un courant est
0
symbolisée par le chiffre binaire 1 et l’absence du courant par le chiffre binaire 0.
données
BUS
Chaque fil représente un bit
De
Connexion vers d’autres éléments
(Clavier, Ecran, Disque dur, etc… )
Organisation interne :
S i la communication entre les différents composants s’effectue en binaire, cela veut également dire qu’à l’intérieur de tous
les composants, tout est structuré de façon binaire. Le microprocesseur s’occupe de réaliser tout un ensemble de
traitements élémentaires qui sera déterminé par un programme, et cet ensemble d’instructions élémentaires devra être écrit et
stocké dans la mémoire centrale de l’ordinateur. Ce programme est en fait une suite d’instructions écrites en binaire dont
chacun des codes correspond à une action spécifique du microprocesseur, comme par exemple, l’opération +. Toutefois, pour
que cette simple opération puisse être réalisée, il est nécessaire d’avoir au moins deux opérandes. Donc, cela veut dire que des
données doivent être stockées dans un autre endroit de la mémoire centrale de l’ordinateur. Ces données sont également
exprimées de façon binaire.
L e premier langage informatique est donc le langage binaire. Seulement, il est très fastidieux d’écrire directement en binaire,
surtout qu’il existe un très grand risque de se tromper. Un 1 peut très vite se transformer en 0, ce qui peut avoir des
conséquences désastreuses. Pour éviter ces erreurs, l’idée a été d’utiliser le codage hexadécimal qui est en fait une autre façon
de représenter le binaire (L’hexadécimal est une concaténation de quatre chiffres binaires). Finalement, que l’on parle du
binaire ou de l’hexadécimal, il s’agit de la même chose, c'est-à-dire du langage machine.
Ce langage machine pose un autre problème ; le code correspondant à une instruction d’un processeur ne fonctionne pas
nécessairement pour un autre processeur. En effet, chaque processeur possède son propre codage binaire. Du coup, il n’est
pas possible de déployer des applications sur des plate-formes où les systèmes d’exploitations sont différents (surtout si les
machines ne possèdent pas les mêmes microprocesseurs).
L’idée a été donc de proposer des langages de plus haut niveau qui soient d’une part plus proche de la pensée humaine, et
qui soient également non tributaire du processeur utilisé.
L e langage Rust est un langage qui s’approche fortement de la pensée humaine et donc plus facilement compréhensible
par le programmeur. C’est à la fois un langage structuré mais aussi Orienté Objet. Malgré tout, le seul langage
compréhensible par le microprocesseur reste le langage machine. Il faut donc qu’il existe un système qui analyse notre code en
langage évolué (fichier source avec l’extension rs) et qui le transforme en un code écrit en langage machine.
Page 3/15
Cours Introduction au langage Rust
Il existe deux solutions pour cela. Soit nous effectuons une traduction définitive avec la production d’un fichier binaire qui
sera nécessaire pour exécuter le programme. Ce système s’appelle un compilateur et joue donc le rôle de traducteur. C’est
ce qu’utilise le langage C++ ou le langage Rust qui nous intéresse ici.
Soit nous effectuons une traduction « à la volée » sans génération de fichier supplémentaire, ce système s’appelle un
interpréteur. C’est ce qu’utilise le langage python. L’avantage, c’est que nous n’avons pas besoin de générer un fichier
binaire, l’inconvénient toutefois c’est que la traduction sera effectuée à chaque fois que nous lancerons le programme défini
dans le code source. Le temps d’exécution est alors beaucoup plus conséquent de l’ordre de 70 fois moins performant.
Lorsque nous utilisons un interpréteur, le code source est appelé « script ».
Compilation
Nom du [Link] Programme exécuté
Remarque : Pour tous les langages de programmation compilés ou interprétés, le code source est toujours un texte. Donc, pour écrire votre
programme, un simple éditeur de texte suffit. Toutefois, il sera nécessaire de posséder un compilateur ou un interpréteur pour réaliser la
traduction vers le code binaire (code objet).
Mémorisation d’une donnée dans un langage de haut niveau
U n programme dans un langage de haut niveau correspond à une suite d’instructions. Le programme à besoin d’utiliser un
certain nombre de données pour aboutir à sa finalité. Il est alors nécessaire de stocker la valeur des calculs
intermédiaires.
Programme principal
L ors de la description de ce langage Rust, je supposerais que vous connaissez les autres langages tel le C++ ou le Python
que vous avez sur mon site. J’aborderais donc les sujets beaucoup plus rapidement avec cette fois-ci beaucoup plus
d’exemples de codes.
Je rappelle toutefois que la plus petite unité indépendante d’un programme Rust est l’instruction. En langage naturel, cela
correspond à une phrase. L’instruction est constituée d’identificateurs de symboles, ainsi que des mots réservés.
Une application quelle qu’elle soit diot toujours posséder une fonction principale nommée main(). Une fonction se reconnaît
par le fait d’avoir des parenthèses pour prendre en compte des paramètres éventuels et pour la définition de cette fonction,
elle commence par le mot réservé « fn » et le bloc de cette fonction est délimité par des accolades « {} ». Chaque instruction
élémentaire est ponctuée par un point-virgule « ; ».
Page 4/15
Cours Introduction au langage Rust
Programme de bienvenue
fn main() {
println!("Bienvenue !");
}
Ce simple programme classique affiche le texte « Bienvenue ! » à l’écran grâce à la macro intégrée println!(), qui affiche le
texte souhaité et va à la ligne suivante. Si vous désirez afficher un texte sans aller à la ligne, choisissez la macro print!().
U n langage informatique manipule de l’information, comme le terme l’indique. Celle-ci est stockée dans les cases mémoire
de l’ordinateur sous forme de bits. Cependant, il est rare que l’on est besoin de manipuler ces bits en tant que tels. En
général, on souhaite plutôt utiliser des entités plus sophistiquées, comme des entiers, des réels, des booléens, etc. Chacune de
ces entités va elle-même être codée sur un certain nombre de bits dans la mémoire.
Un langage évolué, comme le Rust, permet d’utiliser des données de haut niveau, comme des nombres entiers par exemple,
en se chargeant lui-même de la « basse besogne » consistant à convertir les bits de mémoire en ce type de donnée, ou
inversement (lors de la compilation).
Une donnée possède un type qui indique deux choses importantes : l’ensemble des valeurs dont elle fait partie, l’ensemble des
propriétés qui la caractérise, comme les nombres entiers par exemple qui sont capables de réaliser des divisions entières
(euclidiennes), ce que ne peut faire les nombres réels qui vont proposer le résultat avec des décimales après la virgule.
U ne donnée est une brique élémentaire dans un programme que l’on caractérise par son type, d’une part, et par sa valeur
actuelle d’autre part. Nous venons de voir ce qu’est un type de données simples; la valeur actuelle de la donnée (par
exemple 12 pour un entier) est sujette à modification en général, sous la contrainte qu’elle reste dans l’ensemble de valeurs du
type. Par contre, le type de la donnée reste toujours le même; en conséquence, les propriétés d’une donnée, qui sont celles de
son type, sont constantes.
La déclaration d’une variable en Rust est assez particulière par rapport à beaucoup d’autres langages de programmation
puisque vous n’êtes pas obligé de spécifier le type de la variable, à l’image de Python. C’est la valeur initiale (constante
littérale) qui détermine le type. D’ailleurs, pour qu’une variable soit définie, elle doit être impérativement initialisée (justement
pour connaître et évaluer son type, pendant la déclaration ou plus tard, le tout c’est qu’elle soit définie avant la fin du
programme). La déclaration d’une variable se réalise à l’aide du mot réservé « let ».
Déclaration de variables immuables (constantes)
fn main() {
let x; // déclaration d'une variable x non encore définie
let y=5.; // déclaration d'une variable réelle y initialisée avec la valeur 5.0
let lettre='A'; // déclaration d'une variable caractère initialisée avec le caractère 'A'
let test= true; // déclaration d'une variable booléenne
x=12; // définition de la variable x qui alors considérée comme entière
println!("x={}, y={}, lettre={}, test={}", x, y, lettre, test);
}
Résultat
x=12, y=5, lettre=A, test=true
La macro println!() utilise une chaîne de caractères qui sert de motif pour obtenir un résultat paramétré
suivant le nombre d’arguments utilisés par cette macro, grâce à l’utilisation des « {} » qui détermine
Page 5/15
Cours Introduction au langage Rust
l’emplacement de la valeur associée à l’argument donné dans l’ordre spécifié dans la liste proposée. Ici, nous
avons quatre {} qui seront respectivement associés à x, y, lettre et test.
Il est possible de déclarer plusieurs variables sur une même ligne. En reprenant le scénario précédent :
Déclaration de variables immuables (constantes)
fn main() {
let (x, y, lettre, test) = (12, 5.0, 'A', true);
println!("x={}, y={}, lettre={}, test={}", x, y, lettre, test);
}
Résultat
x=12, y=5, lettre=A, test=true
Il est impératif qu’une variable soit définie en proposant une valeur avant la fin du programme. La plupart du temps, la
meilleure stratégie est de l’initialisée au moment de la déclaration. L’exemple suivant provoque l’erreur du compilateur :
Déclaration de variables immuables (constantes)
fn main() {
let x;
println!("x={}", x);
}
Résultat
error[E0282]: type annotations needed
--> src/[Link]:9
|
2| let x;
| ^ consider giving `x` a type
Le compilateur est dans l’embarras puisqu’il ne sait pas à priori quel est le type de la variable utilisée. Il est possible de
spécifier son type en rajoutant le symbole « : » suivi du type désiré dans la liste que nous avons fourni, comme suit :
Déclaration de variables immuables (constantes)
fn main() {
let x : i32;
println!("x={}", x);
}
Résultat
error[E0381]: borrow of possibly-uninitialized variable: `x`
--> src/[Link]:22
|
3| println!("x={}", x);
| ^ use of possibly-uninitialized `x`
Malgré la connaissance du type, le compilateur n’aime pas avoir des variables non initialisées, ce qui est logique
puisque cela ne correspond à rien. Il ne faut surtout pas rester avec des valeurs aléatoires.
Nous remarquons que le compilateur de Rust est très performant puisque après coup, il est capable de reconnaître le type
d’une variable et empêche le programmeur de faire n’importe quoi, alors que le compilateur du C++ est beaucoup plus
permissif, ce qui dans certaines situations peut poser des problèmes (mauvaise gestion de la mémoire).
L e compilateur nous aide dans notre développement afin de minimiser toutes les erreurs que nous pourrions faire en phase
de conception. Les déclarations des variables que nous venons de réaliser ne peuvent pas être modifiées après la phase
déclarative. Ce sont en définitives plutôt des constantes. Elles n’évoluent pas au cours du temps. Si vous désirez qu’elles
soient « variables », vous devez le spécifier explicitement grâce au mot réservé « mut ».
Je vous invite à faire l’expérience suivante afin de bien suivre le comportement du compilateur. Après la déclaration de la
variable x, je propose de la faire évoluer en rajoutant la valeur 5 :
Déclaration de variables immuables (constantes)
fn main() {
let x=12;
println!("x={}", x);
x += 5;
println!("x={}", x);
}
Résultat
error[E0384]: cannot assign twice to immutable variable `x`
--> src/[Link]:5
Page 6/15
Cours Introduction au langage Rust
|
2| let x=12;
| -
| |
| first assignment to `x`
| help: make this binding mutable: `mut x`
3| println!("x={}", x);
4| x += 5;
| ^^^^^^ cannot assign twice to immutable variable
Résultat
x=12
x=17
Pour la déclaration des variables, nous pourrions spécifier systématiquement le mot clé mut. Il est
normalement utile lorsque nous prévoyons de faire évoluer cette variable. Là aussi, le compilateur nous aide
dans notre prise de décision. Il nous propose un warning dans le cas d’une variable mutable qui n’a
finalement pas évolué, comme dans l’exemple suivant :
Déclaration de variables mutables
fn main() {
let mut x=12;
println!("x={}", x);
}
Résultat
warning: variable does not need to be mutable
--> src/[Link]:9
|
2| let mut x=12;
| ----^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
Une fonctionnalité permise par Rust et de déclarer une variable plusieurs fois (avec le même nom) à l’image
de ce que fait le langage Python (là aussi les variables primitives sont immuables) :
Déclaration de variables immuables
fn main() {
let x=12;
let x = x+5;
println!("x={}", x);
}
Résultat
x=17
Nous pouvons aussi changer de type, seulement la première variable ne sert à rien. Je vous invite à éviter ce genre de code
qui prête à confusion, surtout qu’il est même possible de passer d’une variable mutable à une variable immuable et vice-
versa. Par ailleurs, il faut bien comprendre que même si le nom est identique, il s’agit en réalité d’une autre variable avec un
autre emplacement mémoire. Je le répète, évitez ce genre de codage.
Déclaration de variables immuables
fn main() {
let x=12;
let x = 'A';
println!("x={}", x);
}
Résultat
x=A
Page 7/15
Cours Introduction au langage Rust
Pour conclure, je pense qu’il est préférable de bien gérer les variables qui sont mutables et celles qui ne le sont pas :
Déclaration de variables
fn main() {
let (mut entier, lettre) = (12, 'A');
entier += 5;
println!("entier={}, lettre={}", entier, lettre);
}
Résultat
entier=17, lettre=A
Constantes littérales
L ors de la déclarations des variables, il suffit la plupart du temps de les initialisées avec des valeurs correspondant aux
types que nous désirons exploiter. Ces valeurs sont appelées des constantes littérales, puisque nous exprimons
directement et définitivement pour les variables immuables le contenu de ces variables.
L’expression est assez libre lorsque nous manipulons des valeurs entières. Nous pouvons proposer des nombres classiques
(en base dix), mais aussi une valeur en hexadécimale, avec le préfixe « 0x », en binaire avec le préfixe « 0b », sous forme
de caractère avec le préfixe « b », comme nous le pratiquons généralement avec le langage C++. Par ailleurs, lorsque la
constante littérale est assez longue, il est possible d’utiliser le caractère « _ » comme séparateur (des milliers ou quartets,
suivant le cas) afin que la lecture soit beaucoup plus aisée. Dans le cas des caractères, il est possible d’utiliser « \ » pour
permettre l’utilisation des caractères de contrôle. Par contre, de façon surprenant « \b » n’existe pas dans ce langage.
Constantes littérales
fn main() {
let nombre = 98_222; // constante sur 32 bits (utilisation d'un séparateur des milliers)
let x : u8 = 65; // écriture d'un octet avec une écriture classique en base 10
let y : u8 = 0x41; // écriture d'un octet avec une écriture en hexadécimale
let z : u8 = 0b0100_0001; // écriture d'un octet avec une écriture en binaire (séparateur de quartets)
let a : u8 = b'A'; // écriture d'un octet avec une écriture sous forme de caractère
let A= 'A'; // lettre en format 32 bits
let ligne = '\n'; // utilisation d'un caractère de contrôle pour aller à la ligne
Résultat
nombre=98222, x=65, y=65, z=65, a=65
A=A, ligne=
Suite du texte
L e littéral chaîne de caractères sert de modèle pour les macros println!() et print!() : chacun des blocs {,,,} dans ce modèle
est remplacé par la valeur d’un des arguments après la chaîne de formatage. La chaîne qui sert de modèle doit être une
constante pour que Rust puisse la confronter aux différents types des arguments dès la compilation. Enfin, tous les arguments
doivent être utilisés, sous peine d’erreur de compilation, ce qui paraît logique.
Dans le modèle, les blocs {…} sont des paramètres de format qui se base sur la syntaxe {lequel : format}. Ces deux
éléments sont facultatifs et la paire d’accolades vide { } est souvent utilisée.
L’élément lequel permet de désigner lequel des arguments dans le modèle doit être inséré à cette place. Vous pouvez
désigner le paramètre par un indice (le premier commence à 0) ou par son nom. Lorsque le rang n’est pas indiqué avec
lequel, les arguments sont pris dans l’ordre logique de gauche à droite.
La valeur format (comment) détermine le format à appliquer : remplissage, précision numérique, base numérique, etc. Si
cette valeur est fournie, le signe deux-points doit la précéder.
Chaînes paramétrées
fn main() {
let (nom, prenom) = ("RÉMY", "Emmanuel");
println!("Bonjour {} {}", nom, prenom);
println!("Bonjour {1} {0}", nom, prenom);
println!("Bonjour {p} {n}", n="BORÉALE", p="Aurore");
}
Résultat
Bonjour RÉMY Emmanuel
Bonjour Emmanuel RÉMY
Bonjour Aurore BORÉALE
Page 8/15
Cours Introduction au langage Rust
L orsque que le type d’un argument est une chaîne de caractères, la valeur du format est constituée de plusieurs parties,
toutes facultatives : un caractère de remplissage, un alignement, une longueur maximale, une largeur de champ minimale,
en respectant le schéma suivant {:remplissage alignement [Link]}
Une longueur maximale : le paramètre est tronqué s’il est plus long. Si rien n’est mentionné, tout le texte est utilisé.
Une largeur de champ minimale : après troncature éventuelle, si le paramètre est moins long que cette valeur, Rust ajoute
des espaces par défaut pour obtenir la bonne longueur. Rien n’est fait en l’absence d’une valeur.
Un alignement : Si le paramètre doit subir un remplissage pour une longueur minimale, cette valeur d’alignement permet de
choisir d’aligner à gauche, à droite ou de centre la chaîne, grâce aux trois symboles respectifs : « < ^ > ».
Un caractère de remplissage : à utiliser à la place de l’espace proposé par défaut. Si vous spécifiez ce caractère, vous
devez également précisez l’alignement.
Formatage de chaînes de caractères
fn main() {
let message = "Bienvenue !";
println!("{}", message);
println!("{:.4}", message);
println!("{:>15}", message);
println!("{:#<15}", message);
println!("{:=^15}", message);
println!("{:=^15.4}", message);
println!("{:^15}", message);
println!("{:^15.4}", message);
println!("{:.^15.4}", message);
}
Résultat
Bienvenue !
Bien
Bienvenue !
Bienvenue !####
==Bienvenue !==
=====Bien======
Bienvenue !
Bien
.....Bien......
L orsque le type d’un argument est numérique, comme usize ou f64, le membre format du paramètre peut contenir les
éléments suivants, qui sont tous facultatifs :
Un remplissage et un alignement : fonctionnant comme le type chaîne de caractères
Un caractère + : qui demande à toujours afficher le signe de la valeur, même si positive.
Un caractère # : qui demande d’ajouter un préfixe de base numérique comme 0x ou 0b.
Un caractère 0 : qui demande d’ajouter des zéros pour atteindre la largeur minimale demandée, au lieu d’utiliser le
remplissage normal.
Une largeur minimale : si la valeur n’est pas suffisante après formatage, Rust ajoute des espaces à gauche par défaut.
Une indication de précision: pour les valeurs à virgule flottante, qui correspond au nombre de chiffres après la virgule (le
point décimal). Rust arrondit ou ajoute des zéros pour atteindre le nombre demandé. S’il n’y a pas de précision, Rust utilise
le moins de chiffres possible pour représenter la valeur complète. Ce paramètre est ignoré dans le cas des valeurs entières.
Une notation : pour les types entiers, le préfixe est un b pour le binaire, un o pour l’octal ou bien un x ou un X pour
l’hexadécimal. Si vous avez spécifié le caractère #, la base numérique est affiché au format Rust, 0b, 0o , 0x ou 0X. Pour les
types à virgule flottante, la base est e ou E pour la notation scientifique avec coefficient normalisé, la lettre correspondant à
l’exposant. Si rien n’est mentionné pour la base, Rust utilise bien sûr la base décimale.
Contrôle des formats numériques
fn main() {
let (entier, reel) = (65, 4./3.);
println!("{}", entier);
println!("{:+}", entier);
println!("{:12}", entier);
println!("{:#12x}", entier);
println!("{:#12b}", entier);
println!("{:012}", entier);
println!("{:.^12}", entier);
println!("{:#012b}", entier);
println!("{0} {0:X} {0:b}", entier);
println!("{0} {0:#X} {0:#b}", entier);
println!("\n{}", reel);
println!("{:.2}", reel);
println!("{:12.2}", reel);
}
Page 9/15
Cours Introduction au langage Rust
Résultat
65
+65
65
0x41
0b1000001
000000000065
.....65.....
0b0001000001
65 41 1000001
65 0x41 0b1000001
1.3333333333333333
1.33
1.33
Opérateurs
L es opérateurs de Rust sont similaires à ceux utilisés par le langage C++, sauf pour l’incrémentation « ++ » et la
décrémentation « -- » qui a été purement et simplement supprimé. Utilisez à la place « += 1 » ou « -=1 » pour obtenir le
même résultat. Le seul opérateur ternaire du C++ « ?: » n’est également pas reconduit dans le langage Rust.
Chaînes paramétrées
fn main() {
let (x, y) = (7, 2);
let bonnenote = 12.5 >= 10.;
println!("{}/{} = {}, reste={}", x, y, x/y, x%y);
println!("Bonne note = {}", bonnenote);
}
Résultat
7/2 = 3, reste=1
Bonne note = true
L e langage C++ est très permissif et permet des conversions automatiques qui permettent par exemple de mélanger des
entiers et des réels lors de simple opérations basiques. Ce mélange n’est pas toléré par Rust. Toutes les opérations
doivent utiliser des variables ou des constantes de même type, sinon une erreur de compilation apparaît.
Vous devez donc prévoir des variables qui soient de type similaire si vous désirez effectuer des opérations entre elles. Il est
quand même possible de réaliser une conversion explicite à l’aide de l’opérateur as pour que les opérandes soient de la
même nature.
Opérations sur des types différents
fn main() {
let (x, y) = (7, 2.);
let z = x/y;
println!("x={}, y={}, z={}", x, y, z);
}
Résultat
error[E0277]: cannot divide `{integer}` by `{float}`
--> src/[Link]:14
|
3| let z = x / y;
| ^ no implementation for `{integer} / {float}`
|
= help: the trait `Div<{float}>` is not implemented for `{integer}`
Résultat
x=7, y=2, z=3.5
3.5 = 3 + 0.5
Page 10/15
Cours Introduction au langage Rust
Résultat
Bonjour Aurore BORÉALE (19 ans)
Bonjour Aurore BORÉALE (19 ans)
Vous serez peut-être surpris de découvrir qu’il existe un type de tuple souvent utilisé alors qu’il s’agit d’un tuple zéro qui
s’écrit tout simplement « () ». Il est souvent désigné sous le terme type unité parce qu’il ne contient qu’une valeur qui s’écrit
elle aussi (). Rust se sert de ce type unité lorsque nous n’avons aucune valeur à transmettre, mais que le contexte
demande la présence d’un type, comme c’est le cas lorsque nous définissons une fonction qui ne renvoie aucune valeur.
L e langage Rust propose plusieurs types qui correspondent non à des valeurs directes, mais à des adresses mémoires.
C’est utile lorsque vous devez désigner une valeur dans une variable à l’aide d’une autre variable qui pointe sur la
première. Dans ce cadre là vous êtes obligé de prendre les types pointeurs.
Nous connaissons déjà bien ce concept avec le langage C++ qui utilise aussi la notion de référence. Rust n’utilise qu’un seul
de ces concepts, le pointeur que Rust appelle finalement référence. Grâce à l’inférence de type, l’écriture du code Rust
ressemblera très souvent à celui du C++ avec plus de protection au niveau de la gestion de la mémoire. Il est beaucoup plus
facile d’utiliser correctement les pointeurs en Rust qu’en C++ (plus de problème de fuites mémoires).
La déclaration &String, qui se prononce « ref String », constitue une référence à une valeur de type chaîne de caractères.
De même, &i32 est une référence à une valeur de type entier, etc.
Les références représentent le type pointeur de base de Rust. L’expression &x est une référence à x, c’est-à-dire qu’elle
contient l’adresse de x. Dans la terminologie Rust, l’expression &x emprunte (dans le vrai sens du terme avec un contrôle
permanent) une référence de x. Inversement, l’expression *r (déréférencement, comme dans le C++) correspond à la
valeur qui se trouve à l’adresse contenue dans r. Les références Rust sont par défaut immuables.
Une caractéristique importante des pointeurs dans Rust est le suivi permanent de la possession et de la durée de vie des valeurs. Les
pointeurs errants, les doubles libérations et les invalidations de pointeurs sont donc bloqués dès la compilation.
Pointeurs : référence et déréférencement
fn main() {
let a = -23;
let b = &a; // b fait référence à a
println!("a = {}", b); // inférence de type, déréférencement automatique
let c = b-25; // inférence de type, déréférencement automatique
println!("c = {}", c);
let mut x = 12;
x += 5;
let y = &mut x; // y fait référence à x en permettant le changement de valeur
*y += 5; // utilisation de l'opérateur de déréférencement
println!("x = {}", x);
}
Résultat
a = -23
c = -48
x = 22
Les tableaux
C ontrairement aux tuples, un tableau est une collection d’éléments de même type. Un tableau Rust possède une taille
déterminée lors de la compilation et non modifiable ensuite. Vous ne pouvez donc pas ajouter d’éléments ou réduire un
tableau. Il n’existe pas en Rust de syntaxe pour les tableaux non initialisés. En effet, comme pour tous les autres types, Rust
fait tout son possible pour ne laisser aucune valeur non initialisée dans le code.
Page 11/15
Cours Introduction au langage Rust
La syntaxe la plus simple pour créer un tableau consiste à délimiter l’ensemble des valeurs par des crochets droits « [ ] ». Si
vous n’avez pas de valeurs précises, et comme vous êtes obligé d’initialiser un tableau, vous pouvez préciser le nombre de
cases que vous souhaiter gérer avec une valeur par défaut pour toutes ces cases (pas de valeur aléatoire). La syntaxe est
alors la suivante : « [valeur ; nombre] ».
Par défaut, comme pour les autres types, un tableau est immuable. Ce qui fait que vous ne pouvez pas modifier les valeurs
proposées dans le tableau. Si vous souhaitez modifier son contenu, il faudra là aussi utiliser le mot réservé mut.
Pour modifier une valeur du tableau, vous devez utiliser l’opérateur d’indexation (les crochets droits « [ ] ») en spécifiant la
case concernée sachant que la première case commence par l’indice 0. L’utilisation est la même qu’avec le langage C++.
Un tableau Rust est un objet et à ce titre, ils possèdent des méthodes associées, comme len() qui renvoi le nombre de
cases du tableau, ou sort() par exemple qui permet de mettre les valeurs dans l’ordre croissant, etc.
Si le tableau doit changer de taille pendant l’exécution, ce qui est souvent le cas, vous utiliserez non pas un tableau mais un vecteur
que nous connaîtrons par la suite.
Tableaux
fn main() {
let entiers = [12, 24, -8, 5, -3, 19];
println!("Entiers = {:?} ({})", entiers, [Link]());
let mut reels = [3.2; 5];
reels[3] = -1.2;
reels[0] = 8.9;
println!("Réels = {:?} ({})", reels, [Link]());
let mut prenoms = ["Bruno", "Alice", "Alain", "Aurore", "Marcel", "Jean"];
[Link]();
println!("Prénoms = {:?}", prenoms);
[Link]();
println!("Prénoms = {:?}", prenoms);
}
Résultat
Entiers = [12, 24, -8, 5, -3, 19] (6)
Réels = [8.9, 3.2, 3.2, -1.2, 3.2] (5)
Prénoms = ["Alain", "Alice", "Aurore", "Bruno", "Jean", "Marcel"]
Prénoms = ["Marcel", "Jean", "Bruno", "Aurore", "Alice", "Alain"]
Vous remarquez qu’il est possible d’afficher un tableau dans son ensemble grâce au formateur « {:?} » au sein de la macro
println!(). Ce formateur nous permet d’afficher des structures plus complexes que les types de base.
Les vecteurs
U n vecteur est un tableau redimensionnable, dont la taille n’est pas connue à la compilation. En interne, les vecteurs sont
composés d’un pointeur sur le tableau sous-jacent, d’une taille et d’une capacité. Lorsque la capacité est atteinte, le
vecteur est agrandi automatiquement (sans opération manuelle à faire côté développeur). Vous pouvez ajouter des éléments
les supprimer et pouvez annexer un vecteur à la fin d’un autre.
La création d’un vecteur est possible de plusieurs manières. Vous disposez tout d’abord d’une macro qui s’écrit « vect ! »
grâce à laquelle la syntaxe de création d’un vecteur ressemble à celle d’un tableau. Comme pour un tableau, vous pouvez
également créer un vecteur en demandant le nombre d’éléments après la valeur initiale.
La différence fondamentale avec le tableau, c’est que le vecteur propose une capacité variable. Vous pouvez ajouter des
éléments à la fin du tableau et les enlever par la suite en utilisant respectivement les méthodes push() et pop(). Vous
pouvez également insérer des éléments avec la méthode insert() et en supprimer d’autres avec la méthode remove(), etc.
Les méthodes que nous avons déjà utilisées pour les tableaux sont identiques pour les vecteurs. Ainsi, suivant le besoin,
nous pouvons prendre les méthodes len(), sort(), reverse(), etc.
Finalement, la déclaration d’un vecteur est très similaire à la déclaration d’un tableau, vous devez juste rajouter le préfixe
« vect ! ». Cette macro représente en réalité la classe Vec qui possède des méthodes statiques new() et from(). La
première méthode permet de créer un vecteur vide, la deuxième permet, comme avec la macro, de créer un vecteur avec las
valeurs proposées en argument. J’avoue que la syntaxe des macro me paraît plus séduisante et rapide.
Il est à noter que puisqu’un vecteur est un tableau dynamique. La logique veut que vous déclariez systématiquement vos variables
(tableaux) comme mutable.
Vecteurs
fn main() {
let mut entiers = vec![12, 24, -8, 5, -3, 19];
println!("Entiers = {:?} ({})", entiers, [Link]());
[Link](-5);
[Link]();
[Link]();
println!("Entiers = {:?} ({})", entiers, [Link]());
let mut reels = vec![3.2; 5];
reels[0] = -5.9;
reels[4] = 2.5;
[Link](2);
Page 12/15
Cours Introduction au langage Rust
Résultat
Entiers = [12, 24, -8, 5, -3, 19] (6)
Entiers = [-5, 19, -3, 5, -8, 24] (6)
Réels = [-5.9, 3.2, 3.2, 2.5]
Réels = [4.0, -2.3, 8.9]
Prénoms = ["Alain", "Alice", "Aurore", "Bruno"]
vide = [-8.5], rempli = [5, 2, -9, 33, 12]
U ne tranche est très utile pour représenter un sous-ensemble d’un tableau ou d’un vecteur et correspond à la syntaxe
« debut..fin ». Associé à un tableau ou un vecteur, la syntaxe est alors la suivante « [debut..fin] », non suivie de la
longueur. Une tranche de collection ne peut être stockée directement dans une variable, ni transmise en argument d’un appel
de fonction puisque sa longueur n’est pas prédéfinie. Les tranches de collection sont toujours transmises par référence.
La tranche elle-même est un type à part entière qui représente la classe Range. Elle sert à proposer une délimitation avec
une borne inférieure et une borne supérieure de n’importe quel type (puisqu’il s’agit d’un générique, mais nous manipulons le
plus souvent des entiers), en prenant toutes les valeurs comprises entre ces deux bornes, comme par exemple 2..5, sachant
que la borne supérieure et non incluse. Les bornes inférieures et supérieures sont facultatives.
Si vous désirez que la borne supérieure soit inclusive, il existe une autre classe qui se nomme RangeInclusive qui s’exprime
avec la syntaxe suivante « debut...=fin ». Vous remarquez la présence supplémentaire du symbole « = ». En reprenant
l’exemple précédent, si vous désirez que la borne 5 soit inclusive, voici la tranche à soumettre 2..=5.
Comme une tranche est une classe, il est bien entendu possible d’utiliser des méthodes adaptées. Il en existe une très
intéressante, qui s’appelle collect() qui, comme son nom l’indique, permet de générer une collection de valeurs consécutives
délimités par les bornes. Vous devez toutefois préciser le type, car cette méthode permet de construire plusieurs sortes de
collection. Pour que cette méthode soit prise en compte, vous devez entourer votre tranche de parenthèses.
Tranches
fn main() {
let tranche = 2..5;
println!("tranche = {:?}", tranche);
let tableau = [15, 23, -4, 12, 36, 44, 7, 15];
println!("tableau = {:?}", tableau);
let entiers = &tableau[tranche];
println!("tableau={:?}, entiers={:?}", tableau, entiers);
println!("tableau[{:?}] = {:?}", 1..3, &tableau[1..3]);
println!("tableau[{:?}] = {:?}", 1..=3, &tableau[1..=3]);
println!("tableau[{:?}] = {:?}", ..3, &tableau[..3]);
println!("tableau[{:?}] = {:?}", 3.., &tableau[3..]);
println!("tableau[{:?}] = {:?}", .., &tableau[..]);
let n = [Link]()-4;
println!("tableau[{:?}] = {:?}", ..n, &tableau[..n]);
let lettres : Vec<char> = ('a'..'m').collect();
println!("lettres={:?}", lettres);
}
Résultat
tranche = 2..5
tableau = [15, 23, -4, 12, 36, 44, 7, 15]
tableau=[15, 23, -4, 12, 36, 44, 7, 15], entiers=[-4, 12, 36]
tableau[1..3] = [23, -4]
tableau[1..=3] = [23, -4, 12]
tableau[..3] = [15, 23, -4]
tableau[3..] = [12, 36, 44, 7, 15]
tableau[..] = [15, 23, -4, 12, 36, 44, 7, 15]
tableau[..4] = [15, 23, -4, 12]
lettres=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l']
L es développeurs C++ savent qu’il existe deux types de chaînes de caractères. Tout d’abord, nous avons les littéraux
chaînes qui correspondent au type pointeur const char *. Par ailleurs, la librairie standard définit la classe std::string pour
créer des chaînes pendant l’exécution. L’approche est quasiment la même dans Rust. Avec les types &str et la classe String.
Page 13/15
Cours Introduction au langage Rust
Un littéral chaîne est une suite de caractères délimitée par des guillemets. Le caractère spécial est l’anti-barre ou barre
oblique inverse, comme pour un littéral de type char. Si un signe guillemet fait partie de la chaîne, il doit être précédée par
une anti-barre, alors que dans un littéral caractère c’est l’apostrophe qui doit être préfixé.
Si vous exprimez une chaîne sur plusieurs lignes dans le code, la représentation en sera de même lorsque vous utilisez les
macro print!() ou println!(). Les sauts de ligne font partis de la chaîne.
Si votre chaîne est relativement grande et que vous désirez la mettre sur plusieurs lignes dans votre code source sans que
les sauts de ligne soient intégrer dans la chaîne, placer en fin de chaque ligne l’anti-barre « \ ».
Il est parfois gênant de devoir doubler tous les signes anti-barre dans une chaîne, notamment dans les expressions
régulières. Vous pouvez dans ce cas utiliser la chaîne brute de Rust correspondant à la lettre minuscule « r ». Si cette lettre
est présente avant le guillemet ouvrant, tous les caractères dans la chaîne sont considérés de façon littérale.
Normalement, vous ne pouvez donc pas insérer de guillemets dans une chaîne brute, puisque ce guillemet va servir de
délimiteur de chaîne. Cependant, il existe une solution. Vous pouvez en effet englober votre chaîne par des signes dièse
« # » répétés juste après la lettre « r » pour identifier chaque caractère brut. Vous pouvez utiliser autant de signes dièse que
nécessaire pour créer un couple de délimiteurs pour chaque chaîne brute.
Lorsque vous faites commencer le littéral chaîne par le préfixe « b », vous obtenez une chaîne octet qui est une série de
valeurs de type « u8 », c’est-à-dire des octets numériques et non du texte au format Unicode. Les chaînes octets profitent
des mêmes possibilités que les autres littéraux chaînes : vous pouvez les répartir sur plusieurs lignes, insérer des caractères
d’échappement et utiliser des anti-barres de fin de ligne.
Une chaîne octet brute commence donc par le double préfixe « br ». Une chaîne octet ne peut pas contenir de valeur de
caractères Unicode, sauf les valeurs des caractères de la première moitié du code ASCII de base, ainsi que les séquences
hexadécimales.
Les chaînes octets sont différentes des chaînes str puisqu’elles représentent plutôt des tableaux d’octets. Dans ce cadre
là, les chaînes octets n’ont aucune des méthodes du type chaîne str. Ce qui apparente les chaînes octets aux chaînes
normales est la syntaxe pour y stocker une valeur.
Chaînes de type &str
fn main() {
let phrase = "\"Aie!\" dit le garçon.\n";
print!("{}", phrase);
let plusieurs_lignes = "Bonjour à
tout le monde";
println!("{}", plusieurs_lignes);
let une_seule_ligne = "Bonjour à \
tout le monde";
println!("{}", une_seule_ligne);
let regex = r"\d+(\.\d+)*";
let phrase_brute = r##""Aie!" dit le garçon."##;
println!("{}", phrase_brute);
let octets = "Une phrase";
println!("{:?} ({})", octets, [Link]());
let octets = b"Une phrase";
println!("{:?} ({})", octets, [Link]());
let phrase_brute = br##""Aie!" dit la fille."##;
println!("{:?}", phrase_brute);
}
Résultat
"Aie!" dit le garçon.
Bonjour à
tout le monde
Bonjour à tout le monde
"Aie!" dit le garçon.
"Une phrase" (10)
[85, 110, 101, 32, 112, 104, 114, 97, 115, 101] (10)
[34, 65, 105, 101, 33, 34, 32, 100, 105, 116, 32, 108, 97, 32, 102, 105, 108, 108, 101, 46]
U ne chaîne Rust est une suite de caractères Unicode au format UTF-8. Cela n’a rien à voir avec les tableaux de caractères
habituels. Le format UTF-8 est un codage à largeur variable. Seuls les caractères ASCII de base sont stockés sur un seul
octet. Tous les autres caractères sont stockés sur plusieurs octets chacun (jusqu’à 4 octets).
Les littéraux chaînes que nous venons d’étudier sont des références « &str » qui désignent des textes au format UTF-8 pré-
réservés dans des zones mémoires en lecture seule avec le code exécutable.
Nous pouvons considérer &str comme une référence tableau &[u8] capable de stocker des valeurs UTF-8 bien formés.
Une chaîne de type String est incarnée par un tampon retaillable qui contient le texte au format UTF-8. Ce tampon est
implanté dans le tas, ce qui permet de modifier sa longueur selon les besoins. Le type String peut donc être vu comme un
type Vec<u8> capable de stocker des codes UTF-8 bien formés.
Ainsi, pour les deux types de chaînes, nous retrouvons la même différence qu’avec les tableaux et les vecteurs étudiés
précédemment. Dès que vous avez besoin de créer une chaîne ou de la modifier, vous devez passer par le classe String.
Page 14/15
Cours Introduction au langage Rust
Comme les vecteurs Vec, les chaînes String possède chacune un tampon implanté dans la zone du tas, tampon qui n’est
partagé avec aucun autre String. Lorsqu’une variable de type String sort de la portée d’exécution, le tampon est
automatiquement supprimé, sauf si la propriété de l’élément String a été transféré.
Nous retrouvons dans la classe String les mêmes méthodes que la classe Vec, les méthodes statiques new() et from(), les
méthodes push(), pop(), reverse(), etc.
La méthode to_string() sert à convertir une chaîne &str vers un String, de même que la méthode statique from().
Il existe également des méthodes plus personnelles, pour manipuler les majuscule et les minuscules - to_uppercase(),
to_lowercase(), pour supprimer les caractères blancs – trim(), la concaténation de plusieurs chaînes – concat() et join(),
la découpe d’une chaîne en plusieurs – split(), etc.
La macro format!() fonctionne sur le même principe que println!(), sauf qu’elle renvoie une nouvelle chaîne String au lieu
d’envoyer le texte pour affichage vers « stdout ». Elle n’ajoute pas non plus de saut de ligne à la fin de la chaîne.
Les opérateurs sont utilisables pour comparer les chaînes, pour concaténer des chaîne, etc. comme pour le langage C++. Il
est également possible de passer d’une chaîne vers une valeur numérique et vice-versa.
Chaînes de type String
fn main() {
// Création de chaînes vides
let s1 = String::new();
let s2 = String::from("");
let s3 = "".to_string();
// Création d'autres chaînes
let mut s4 = "Bonjour".to_string();
let s5 = String::from("à tout le monde !");
let s6 = format!("{} {}", s4, s5);
let majuscules = s6.to_uppercase();
let minuscules = s6.to_lowercase();
println!("{}", majuscules);
println!("{}", minuscules);
[Link]('.');
s4 += " Salut";
s4.push_str(" à tous");
println!("{:?}", s4);
// tranformation chaîne <=> valeur numérique
let chaine_nombre = 15.23.to_string();
let nombre_chaine : f64 = "15.23".parse().unwrap();
println!("{} {}", chaine_nombre, nombre_chaine);
}
Résultat
BONJOUR À TOUT LE MONDE !
bonjour à tout le monde !
"Bonjour. Salut à tous"
15.23 15.23
N ous avons souvent besoin d’une saisie clavier pour que notre programme soit interactif. La bibliothèque d’entrée sortie ne
fait pas partie du module principal, vous devez donc l’importer, comme pour le C++, grâce ici au mot réservé « use ».
Dans ce module, il existe la fonction stdin() qui retourne un objet qui représente l’entrée standard qui possède des méthodes
pour réaliser la saisie correctement, notamment la méthode read_line() qui prend toujours en argument une chaîne de
caractères mutable de type String. Vous pouvez rajouter une exception pour gérer une mauvaise saisie - méthode expect().
Page 15/15