Introduction
Rust est un langage de programmation impératif fortement typé avec une sémantique d'utilisation sûre de la mémoire. Le compilateur Rust génère du code natif.
Le développement de Rust a été influencé par de nombreux autres langages, mais certains aspects de ce dernier sont originaux et spécifiques, notamment le modèle de possession des références à la mémoire, rendant le langage particulièrement adapté au développement de code système sûr et performant.
Ce modèle d'utilisation de la mémoire combine la puissance d'un langage possédant un mécanisme de ramasse-miettes avec la performance d'une gestion manuelle de la mémoire.
Histoire de Rust
- Exposé de Steve Klabnik
- Objectif de Rust: concevoir un langage pour la programmation système compilé, concurrent et sûr.
- Utilisé par Mozilla pour le moteur de rendu web Servo, dont une partie du code est partagé avec le navigateur web Firefox
- 2006-2016: v1.0. Développement ouvert (initié par Graydon Hoare)
- Many older languages better than the new ones. We keep forgetting already-learned lessons.
- Technologies from the past come to save the future from itself.
- Premier code "Rust" montré à la communauté (syntaxe obsolète):
fn main() {
log "Hello, world!";
}
fn max(int x, int y) -> int {
if (x > y) {
ret x;
} else {
ret y;
}
}
- Ancien mécanisme pour les objets:
obj counter(int i) {
fn incr() {
i += 1;
}
fn get() -> int {
ret i;
}
}
fn main() {
auto c = counter(10);
c.incr();
log c.get();
}
- Ancien mécanisme de généricité:
fn swap[T](tup(T,T) pair) -> tup(T,T) {
ret tup(pair._1, pair._0);
}
fn main() {
auto str_pair = tup("hi", "there");
auto int_pair = tup(10, 12);
str_pair = swap[str](str_pair);
int_pair = swap[int](int_pair);
}
- Dans la conception: importance de la sémantique par rapport à la syntaxe
- D'OCaml à Rust dans LLVM
Ressources utilisées pour ce cours
Toutes les ressources utilisées sont gratuites et libre d'accès, et leurs codes sources sont disponibles sur Github.
Manuels
- The Rust Programming Language: également appelé "The Rust Book", un livre d'introduction à Rust
- Rust by Example: une collection d'exemples exécutables pour illustrer différents concepts de Rust et de ses bibliothèques
- Cookin' with Rust: également appelé the "The Rust Cookbook", des exemples de code pour effectuer des tâches de programmation courantes, utilisant des crates courants (les crates sont des paquetages développés par des tiers)
- The Rust Reference: la référence du langage, décrivant chaque construction. Ce n'est pas un guide d'intruction mais il est utile pour comprendre précisément certains aspects du langage.
Outils
Ce cours a été fait avec:
- mdBook qui permet de créer des documentations
- The Rust Playground qui permet de compiler et d'exécuter du code Rust en ligne et qui possède également une API qui permet de compiler et d'exécuter les exemples du cours
Première étape: installation de Rust et "Hello, world!"
- Utiliser la page d'installation de Rust pour l'installer sur votre machine.
- Créer, compiler et exécuter le programme
hello.rs
suivant:
fn main() { println!("Hello, world"); }
Deuxième étape: Rustlings
- Récupérer et installer les Rustlings (des exercices cours pour apprendre à lire et à écrire du code en Rust)
git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
cd rustlings
cargo install --force --path .
- Continuer les Rustlings en vous servant des manuels ci-dessus
- À la fin de votre session de travaux pratiques, générez un patch (
git diff > p
)
Troisième étape: outil d'indexation (baf-core)
- Documentation pour créer un outil en ligne de commande
- Ajouter une fonction d'indexation d'un répertoire à baf-core (voir les spécifications discutées en cours):
baf-core index <repertoire>
Expressions
Expression: combinaison de valeurs (calcul arithmétique, booleen, etc.) retournant (exprimant) une valeur.
Exemple d'expressions arithmétiques, booléennes et litérales:
fn main() { // Integer addition println!("1 + 2 = {}", 1u32 + 2); // Integer subtraction println!("1 - 2 = {}", 1i32 - 2); // Short-circuiting boolean logic println!("true AND false is {}", true && false); println!("true OR false is {}", true || false); println!("NOT true is {}", !true); // Bitwise operations println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101); println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101); println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101); println!("1 << 5 is {}", 1u32 << 5); println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2); // Use underscores to improve readability! println!("One million is written as {}", 1_000_000u32); }
À la différence de langages comme C/C++, en Rust, les blocs peuvent "renvoyer" une expression, ce qui le rapproche d'un langage fonctionnel, comme dans l'exemple ci-dessous:
fn main() { let y = 8; let x = 6 + { let x = 7; if y > 5 { 5 * x } else { y + x } }; println!("{x} and {y}"); }
ou encore ici:
fn main() { let x = 5u32; let y = { let x_squared = x * x; let x_cube = x_squared * x; // This expression will be assigned to `y` x_cube + x_squared + x }; let z = { // The semicolon suppresses this expression and `()` is assigned to `z` 2 * x; }; println!("x is {:?}", x); println!("y is {:?}", y); println!("z is {:?}", z); }
La valeur ()
correspond à une valeur vide et est appelée unit
.
Variables et types de base
Lier une variable
En Rust, on ne déclare pas à proprement parler les variables, mais on
les associe à une valeur avec le mot clef let
. La ligne suivante
crée une variable a
qui va être liée à la valeur 3:
fn main() { let a = 3; println!("La valeur de a est {a}."); }
Le type de a
va être inféré par rapport à la valeur du litéral,
ici un entier signé sur 32 bits (i32
). On aurait pu également donner
le type de manière explicite à la variable de la manière suivante:
fn main() { let a:i64 = 3; println!("La valeur de a est {a}."); }
En Rust, une instruction let
n'est pas une expression (à la différence de C/C++ ou l'affectation renvoie une valeur).
Types de données scalaires
Rust a les types scalaires classiques entiers, flottants, booléens et caractères.
Entiers (integers)
Les entiers sont les suivants:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Les entiers litéraux peuvent être définis avec les syntaxes suivantes:
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
Un entier litéral sera par défaut en 32 bits (i32
).
Flottants
Rust propose des flottants suivant le standard IEEE-754: simple
précision f32
et double précision f64
. Un litéral de type flottant
comme 2.0
sera par défaut en double précision.
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
Booléens
Le type booléen (bool
) supporte les deux valeurs littérales true
et false
.
fn main() { let t = true; let f: bool = false; // with explicit type annotation }
Caractères
Les caractères en Rust sont sur quatre octets et contiennent des valeurs Unicode scalaires.
fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }
Opérations
fn main() { // addition let sum = 5 + 10; // subtraction let difference = 95.5 - 4.3; // multiplication let product = 4 * 30; // division let quotient = 56.7 / 32.2; let truncated = -5 / 3; // Results is -1 println!("Truncated = {truncated}"); // remainder let remainder = 43 % 5; }
Synthèse
fn print_type_of<T>(_: &T) { println!("{}", std::any::type_name::<T>()) } fn main() { // Variables can be type annotated. let logical: bool = true; let a_float: f64 = 1.0; // Regular annotation let an_integer = 5i32; // Suffix annotation // Or a default will be used. let default_float = 3.0; // `f64` let default_integer = 7; // `i32` let a = 'a'; let alpha = 'α'; let inf = '∞'; let z: char = 'ℤ'; let heart_eyed_cat = '😻'; // A type can also be inferred from context let mut inferred_type = 12; // Type i64 is inferred from another line inferred_type = 4294967296i64; print_type_of(&inferred_type); }
Immutabilité et Ombrage
Immutabilité par défaut
Une variable est immutable par défaut, c'est-à-dire qu'une fois liée à une valeur, cette dernière ne peut pas être changée:
fn main() { let x = 5; println!("The value of x is: {x}"); //x = 6; // Ce code est invalide //println!("The value of x is: {x}"); }
Ainsi, pour qu'il soit valide, ce code doit être réécrit ainsi (noter le
mot-clef mut
):
fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; // Ce code est valide println!("The value of x is: {x}"); }
Ombrage
L'ombrage (shadowing) consiste à lier une nouvelle variable avec le même nom afin de modifier sa valeur:
fn main() { let x = 5; println!("The value of x is: {x}"); let x = x + 1; println!("The value of x is: {x}"); }
La portée (scope) d'une variable liée est limitée à son bloc de définition et aux blocs enfants, à partir de l'endroit où elle est déclarée:
fn main() { let x = 5; // x_1 let x = x + 1; // x_2 = x_1 + 1 { let x = x * 2; // x_3 = x_2 * 2 println!("The value of x in the inner scope is: {x}"); // x_3 } println!("The value of x is: {x}"); // x_2 }
On peut changer le type d'une variable:
fn main() { let spaces = " "; let spaces = spaces.len(); println!("{} spaces", spaces); }
On ne peut pas changer le type d'une variable mutable:
fn main() { let mut spaces = " "; // spaces = spaces.len(); // faux ! println!("{} spaces", spaces); }
Constantes
Une constante est déclarée avec le mot-clef const
et doit être typée. La
valeur d'une constante doit être calculable au moment de la compilation du code.
La portée d'une constante est identique à celle d'une variable.
const SECONDS_IN_ONE_HOUR: u32 = 60 * 60; const THREE_HOURS_IN_SECONDS: u32 = SECONDS_IN_ONE_HOUR * 3; fn main() { println!("There are {} seconds in 3 hours.", THREE_HOURS_IN_SECONDS); }
Fonctions
Une fonction en Rust est définie avec le mot clef fn
, doit être nommée, peut
prendre un certain nombre de paramètres qui doivent être typés, et peut
éventuellement retourner une valeur qui doit également être typée.
fn number_of_seconds_in_hours(hours: i32) -> i32 { hours * 60 * 60 } fn main() { println!("Seconds in 3 hours: {}", number_of_seconds_in_hours(3)); }
Une fonction non typée renvoie "unit" ()
.
Instructions et expressions
Le corps d'une fonction en Rust peut être composé d'instructions et d'expressions:
- une instruction effectue une action et ne retourne pas de valeur
- une expression est évaluée et retourne une valeur
Ainsi, let x = 6
est une instruction et ne renvoie pas de valeur,
alors que x + 3
est une expression et renvoie une valeur (le résultat de l'addition de x avec 3).
Une fonction renvoie la valeur de la dernière expression évaluée sur son flot de contrôle:
fn truc_idiot(x: i32) -> i32 { let y = 3; x + y } fn truc_idiot_bis(x: i32) { let y = 3; x + y; // return () } fn truc_idiot_ter(x: i32) { let y = 3; x + y; () // return () too } fn main() { println!("{}", truc_idiot(1)); truc_idiot_bis(2); truc_idiot_ter(3); }
Flot de contrôle
if
La construction syntaxique if
est une expression permettant d'exécuter une
branche de code selon une certaine condition:
fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else if number % 2 == 0 { println!("number is divisible by 2"); } else { println!("number is not divisible by 4, 3, or 2"); } }
La construction if
étant une expression, elle peut-être notamment être
utilisée dans une instruction let
:
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
Boucles
Plusieurs constructions de boucles peuvent être utilisées en Rust.
loop
L'expression loop
permet de créer des boucles infinie:
fn main() { loop { println!("again!"); } }
Une boucle peut être interrompue avec le mot-clef break
qui permet de renvoyer
une valeur éventuelle:
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }
Une boucle peut-être identifiée par un label avec la syntaxe 'label
pour
déterminer sur quelle boucle le break
doit agir (ce dernier peut
éventuellement également retourner une
valeur):
fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}"); }
while
La construction while
permet d'exécuter du code tant qu'une condition est
vraie:
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
for
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
Tuples
A tuple is a collection of values of different types. Tuples are constructed
using parentheses ()
, and each tuple itself is a value with type signature
(T1, T2, ...)
, where T1
, T2
are the types of its members. Functions can
use tuples to return multiple values, as tuples can hold any number of values.
// Tuples can be used as function arguments and as return values. fn reverse(pair: (i32, bool)) -> (bool, i32) { // `let` can be used to bind the members of a tuple to variables. let (int_param, bool_param) = pair; (bool_param, int_param) } // The following struct is for the activity. #[derive(Debug)] struct Matrix(f32, f32, f32, f32); fn main() { // A tuple with a bunch of different types. let long_tuple = (1u8, 2u16, 3u32, 4u64, -1i8, -2i16, -3i32, -4i64, 0.1f32, 0.2f64, 'a', true); // Values can be extracted from the tuple using tuple indexing. println!("Long tuple first value: {}", long_tuple.0); println!("Long tuple second value: {}", long_tuple.1); // Tuples can be tuple members. let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16); // Tuples are printable. println!("tuple of tuples: {:?}", tuple_of_tuples); // But long Tuples (more than 12 elements) cannot be printed. //let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); //println!("Too long tuple: {:?}", too_long_tuple); // TODO ^ Uncomment the above 2 lines to see the compiler error let pair = (1, true); println!("Pair is {:?}", pair); println!("Uhe reversed pair is {:?}", reverse(pair)); // To create one element tuples, the comma is required to tell them apart // from a literal surrounded by parentheses. println!("One element tuple: {:?}", (5u32,)); println!("Just an integer: {:?}", (5u32)); // Tuples can be destructured to create bindings. let tuple = (1, "hello", 4.5, true); let (a, b, c, d) = tuple; println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d); let matrix = Matrix(1.1, 1.2, 2.1, 2.2); println!("{:?}", matrix); }
Tableaux
fn main() { let a = [1, 2, 3, 4, 5]; }
#![allow(unused)] fn main() { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }
#![allow(unused)] fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; }
#![allow(unused)] fn main() { let a = [3; 5]; }
Possession
Mécanisme de possession (ownership)
En Rust, la mémoire est gérée avec un mécanisme de possession (ownership) qui est vérifié au moment de la compilation. Les règles de l'ownership sont les suivantes:
- chaque valeur en Rust a un propriétaire (owner)
- il ne peut y avoir qu'un seul propriétaire de cette valeur à un instant t
- quand le propriétaire est hors de portée, la valeur est détruite
Pour illustrer ce concept, nous utiliserons le type String
qui est stocké sur
le tas (heap). Nous avons déjà utilisé des chaînes de caractères mais ces
dernières, de type &str
, ont un contenu immutable:
fn print_type_of<T>(_: &T) { println!("{}", std::any::type_name::<T>()) } fn main() { let mut x = "hello"; x = "world"; print_type_of(&x); println!("{}", x); }
La fonction from
de String
permet de créer une chaîne de caractère dont on
va pouvoir modifier le contenu:
fn print_type_of<T>(_: &T) { println!("{}", std::any::type_name::<T>()) } fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String println!("{}", s); // This will print `hello, world!` print_type_of(&s); }
Examinons la relation entre une variable s1
et la mémoire dans le cas de la
déclaration suivante:
#![allow(unused)] fn main() { let s1 = String::from("hello"); }
Dans le cas d'un type String
, la mémoire utilisée est de deux types: une
structure, stockée sur la pile, contenant ta taille maximale de la chaîne de
caractère, le nombre de caractères actuels, ainsi qu'un pointeur vers une zone
mémoire sur le tas (heap) contenant la chaîne elle-même:
Si l'on écrit le code suivant, regardons ce qui se passe au niveau de la mémoire:
let s1 = String::from("hello"); let s2 = s1;
En mémoire, la structure contenant les informations sur la chaîne est dupliquée, à la différence de la chaîne elle-même se trouvant sur le tas:
Si l'on fait un parallèle avec d'autres langages comme C++, on parlera de shallow copy: une partie de la mémoire est dupliquée, à la différence d'une deep copy.
En Rust, la mémoire avec laquelle la variable est liée est
automatiquement détruire lorsque la variable devient hors de portée: Rust
appelle la méthode drop
qui rend (au système d'exploitation) la mémoire allouée sur le
tas. Sur le schéma ci-dessus, la question de pose de savoir qui de s1
et s2
doit posséder la zone mémoire et doit donc être responsable de sa destruction,
afin d'éviter les double free.
Pour résoudre ce problème, Rust considère qu'après la ligne:
#![allow(unused)] fn main() { let s2 = s1; }
la variable s1
n'est plus valable. En effet, pour toutes les données possédant
une méthode drop
, une affectation correspond à un déplacement (move)
plutôt qu'à une copie (copy): l'emplacement mémoire de départ est donc
invalidé.
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 n'est plus valide println!("{}", s1); println!("{}", s2); }
Le schéma de la mémoire après l'affectation de s1
à s2
est donc le suivant:
Rust, par défaut, ne fera jamais de copie profonde des données stockées sur le tas. Cette copie doit être faite de manière explicite avec clone():
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("{}", s1); println!("{}", s2); }
Le code ci-dessus correspondant à l'organisation de la mémoire suivante:
Implémentation des traits Copy
et Drop
Revenons au code exemple que nous avions vu précédemment:
fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
Ici, il n'est pas nécessaire d'utiliser clone()
car pour les types de données
de base (comme les entiers), la taille est connue au moment de la compilation.
Ces types de données donnent donc lieu à une allocation statique effectuée sur
la pile, les notions de copie profonde et de shallow copy ont ici le même
comportement: les données, dans le cas d'une affectation sont bien copiées.
Ces types de données implémentent le
trait Copy
, régissant
le comportement de l'affectation. Les données de type String
implémentent
quant à elles le trait Drop
: ces données impliquent une affectation de type
déplacement plutôt qu'une copie: elles seront donc régies par le mécanisme de
possession. Rust ne permet pas qu'un type implémentant Drop
implémente le
type Copy
.
Parmi les types implémentant le trait Copy, on trouve:
- Tous les types entiers, tel que
u32
- Le type booléen
bool
- Tous les types flottants, comme
f64
- Le type caractère
char
- Les tuples, seulement si tous leurs membres implémentent
Copy
. Par exemple,(i32, i32)
implémenteCopy
, mais pas(i32, String)
Possession et fonctions
Le passage d'un paramètre à une fonction utilise le même mécanisme que
l'affectation. Dans l'exemple ci-dessous, le fait de passer s
en paramètre à
la fonction takes_ownership
transfère la propriété de s
au paramètre
some_string
de la fonction. Lorsque l'on sort de la fonction, comme
some_string
est hors de portée, la mémoire associée est rendue. Pour une
variable entière le comportement est différent puisque le passage au paramètre
some_integer
de la fonction makes_copy
se fait en effectuant une copie.
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. But because s's value was moved, nothing // special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{}", some_string); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{}", some_integer); } // Here, some_integer goes out of scope. Nothing special happens.
Il est tout à fait possible de transférer la possession à une fonction et de la récupérer ensuite. Il suffit de retourner les données:
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns one fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
Nous avons donc trois possibilités: une fonction peut prendre la possession de données, la donner, ou la prendre puis la rendre. Notons un dernier mécanisme: une fonction peut tout à fait retourner un tuple, permettant ainsi de retourner plusieurs valeurs.
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
Références et emprunts
Emprunt d'une possession via une référence
Les mécanismes de transfert de possession présentés précédemment sont fastidieux à utiliser. Rust utilise un mécanisme d'emprunt (borrowing) simplifiant l'utilisation de données en mémoire, par l'intermédiaire d'un mécanisme de référence à une zone mémoire.
Examinons par exemple ce code qui retourne la longueur d'une chaîne stockée dans
un type String
:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
La notation &s1
indique que l'on emprunte la zone mémoire par l'intermédiaire
d'une référence. Le type de s
dans la fonction calculate_length
est
&String
qui indique une référence vers une zone mémoire de type String
.
L'organisation de cette zone mémoire est la suivante:
Lorsque l'exécution de la fonction se termine, s
est détruite, ainsi que sa
mémoire associée, mais pas s1
qui avait été empruntée.
Mutabilité des références
Par défaut, une référence est immutable, même si la zone mémoire est mutable. Ainsi, le code en commentaire ci-dessous est incorrect:
fn main() { let mut s = String::from("hello"); // change(&s); // invalide s.push_str("!"); println!("{}", s); } // Cette fonction est invalide // fn change(some_string: &String) { // some_string.push_str(", world"); //}
Pour permettre à s
d'être modifiée lors de l'emprunt, il faut explicitement
définir la référence comme étant mutable avec le mot-clef mut
, dans la
définition du type du paramètre et lors du passage du paramètre:
fn main() { let mut s = String::from("hello"); change(&mut s); s.push_str("!"); println!("{}", s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
Il n'est pas possible de créer une référence mutable à partir d'une variable qui
ne l'est pas: si s
n'était pas mutable, il ne serait pas possible de créer une
référence &mut s
.
Références multiples
Il est possible de créer plusieurs références en même temps vers une zone mémoire, donc de l'emprunter simultanément, avec néanmoins une restriction importante: il ne doit pas y avoir de référence mutable en même temps qu'une autre référence mutable ou immutable. Une référence mutable verrouille donc l'emprunt. Ce mécanisme de verrou est vérifié au moment de la compilation.
Ainsi, le code ci-dessous est correct, mais la partie en commentaire ne l'est pas:
fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{}, {}", r1, r2); //let r3 = &mut s; // BIG PROBLEM //println!("{}, {}, and {}", r1, r2, r3); }
Cet exemple nous permet d'introduire la notion de durée de vie (liveness) d'une variable, légèrement différente de la notion de portée. Considérons le code suivant:
fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{}, {}", r1, r2); let r3 = &mut s; // no problem println!("{}", r3); //let r3 = &mut s; // BIG PROBLEM //println!("{}", r3); //println!("{}, {}", r1, r2); }
Ainsi, bien que la portée de r1 et r2 soit celle du bloc de la fonction
main()
, la durée de vie des variables r1
et r2
s'arrête après le
println!
: comme ces variable ne sont plus utilisées par la suite, Rust
considère qu'elle ne sont plus vivantes.
Dangling Reference
Rust ne permet pas de créer des références vers de la mémoire qui peut potentiellement être détruite avant que la référence ne le soit. Ainsi, le code ci-dessous est faux:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
alors que celui-ci est correct:
fn main() { let s1 = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
Synthèse
- À tout instant, nous pouvons avoir soit une référence mutable, soit un nombre arbitraire de références immutables
- les références doivent toujours être valides
Tranches
Les tranches (slices) sont des références spéciales permettant d'emprunter une séquence d'éléments d'une collection plutôt que la collection complète. Une collection est une suite d'éléments: une chaîne de caractères, un vecteur, un tableau, etc.
Tranche de chaîne de caractère
Prenons l'exemple d'une fonction devant retourner le premier mot d'une chaîne de caractères constituée de mots séparés par des espaces:
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 s.clear(); // this empties the String, making it equal to "" // word still has the value 5 here, but there's no more string that // we could meaningfully use the value 5 with. word is now totally invalid! println!("End index of first word: {}", word); }
Bien que correcte, cette implémentation n'est pas pertinente. En effet, même si
word
contient toujours 5 après l'appel à s.clear()
, cet index n'a plus de
sens.
Afin d'implémenter cette fonction de manière plus pertinente, il est plus judicieux d'utiliser un mécanisme de tranche:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; println!("{}:{}", hello, world); }
La représentation en mémoire est la suivante:
Une tranche de chaîne est dénotée en Rust par &src
, ce qui nous permet de
réécrire la fonction précédente en:
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 // s.clear(); // Incorrect ! println!("first word: {}", word); }
L'appel s.clear()
a été mis en commentaire car il est incorrect. En effet,
nous avons créé une référence immutable à s
lors de l'appel à first_word
,
transmise sous la forme d'une tranche à word
. Nous ne pouvons donc pas changer
s
par l'intermédiaire d'une référence mutable.
Les littéraux de type chaîne de caractère sont des tranches de type &str
:
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let hs = "hello world"; let my_string = String::from(hs); // `first_word` works on slices of `String`s, whether partial or whole let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // `first_word` also works on references to `String`s, which are equivalent // to whole slices of `String`s let word = first_word(&my_string); let my_string_literal = "hello world"; // `first_word` works on slices of string literals, whether partial or whole let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
Autres tranches
fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }
Réalisations en Rust
Quelques exemples de réalisations en Rust.