This is an old revision of the document!
Le but de ce TP est d'apprendre à utiliser
Ownership est un ensemble de règles qui régissent la façon dont un programme Rust gère la mémoire. Tous les programmes doivent gérer la façon dont ils utilisent la mémoire d'un ordinateur pendant leur exécution.
Certains langages ont garbage collection qui recherche régulièrement la mémoire non utilisée pendant l'exécution du programme ; dans d'autres langages, le programmeur doit explicitement allouer et libérer la mémoire.
Rust utilise une troisième approche : la mémoire est gérée via un système de propriété avec un ensemble de règles que le compilateur vérifie. Si l'une des règles est violée, le programme ne compilera pas. Aucune des caractéristiques de propriété ne ralentira votre programme pendant son exécution.
Une scope est la plage d'un programme pour laquelle un élément est valide.
Voila ici un example pour comprendre le concept:
{ // s n'est pas valide ici, car il n'est pas encore déclaré let s = "hello"; // s est valide à partir de ce point // fait quelque chose avec s } // le scope est fini maintenant, donc s n'est pas valid maintenant
Quand on a besoin d'allouer de mémoire sur le heap (ex: variables qui sont mutable et n'ont pas une taille connue au runtime, donc la taille peut être modifié pendant l'exécution du programme) nous devons nous assurer que cette mémoire est retourné à l'allocateur au moment quand nous n'avons plus besoin de notre variable.
Pour ça, dans Rust la mémoire est automatiquement renvoyée une fois que la variable qui la possède sort de la scope. Lorsqu'une variable sort de la portée, Rust appelle une fonction spéciale pour nous: drop automatiquement à l'accolade fermante.
Les mécanismes de transmission d'une valeur à une fonction sont similaires à ceux de l'affectation d'une valeur à une variable. Passer une variable à une fonction déplacera ou copiera, tout comme le fait l'affectation.
Example (lisez les commentaires):
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.
Si nous essayions d'utiliser s après l'appel à take_ownership, Rust renverrait une erreur de compilation. Ces vérifications statiques nous protègent des erreurs.
Les valeurs de retour peuvent également transférer le ownership.
Le ownership d'une variable suit le même schéma à chaque fois : l'affectation d'une valeur à une autre variable la déplace. Lorsqu'une variable qui inclut des données sur le heap sort de la scope, la valeur sera nettoyée par drop à moins que la propriété des données n'ait été déplacée vers une autre variable.
Example (lisez les commentaires):
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 }
Une référence est comme un pointeur en ce sens qu'il s'agit d'une adresse que nous pouvons suivre pour accéder aux données stockées à cette adresse ; ces données appartiennent à une autre variable. Contrairement à un pointeur, une référence est garantie de pointer vers une valeur valide d'un type particulier pour la durée de vie de cette référence.
Pour indiquer la reference on utilise le symbole & avant le nom de variable ou, pour le cas de parametre d'un fonction, avant le type du parametre. Ces esperluettes représentent des références et vous permettent de vous référer à une valeur sans vous en approprier.
let x: u16 = 10; let y = &x;
Example d'un fonction qui a une reference vers un objet comme parametre au lieu de prendre ownership de cette valeur:
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 is a reference to a String s.len() } // Here, s goes out of scope. But because it does not have ownership of what // it refers to, it is not dropped.
La syntaxe &s1 nous permet de créer une référence qui fait référence à la valeur de s1 mais qui ne la possède pas. Comme il ne la possède pas, la valeur vers laquelle il pointe ne sera pas supprimée lorsque la référence cessera d'être utilisée.
De même, la signature de la fonction utilise & pour indiquer que le type du paramètre s est une référence.
Nous appelons borrowing l'action de créer une référence. Comme dans la vraie vie, si une personne possède quelque chose, vous pouvez lui emprunter. Lorsque vous avez terminé, vous devez le rendre. Vous ne le possédez pas.
Tout comme les variables sont immuables par défaut, les références le sont aussi. Nous ne sommes pas autorisés à modifier quelque chose auquel nous avons une référence.
Si on veut modifier la valeur d'un reference on doit dit explicitement ca au compilateur en utilisant le mot mut. Les références mutables ont une grande restriction : si vous avez une référence mutable à une valeur, vous ne pouvez pas avoir d'autres références à cette valeur.
Nous ne pouvons pas non plus avoir une référence mutable alors que nous en avons une immuable à la même valeur.
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
Les slices vous permettent de référencer une séquence contiguë d'éléments dans une collection plutôt que la collection entière. Une slice est une sorte de référence, elle n'a donc pas de ownership.
Une slice de String est une référence à une partie d'une String.
let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11];
Plutôt qu'une référence à la chaîne entière, hello est une référence à une partie de la chaîne, spécifiée dans le bit supplémentaire [0..5]. Nous créons des slices en utilisant une plage entre parenthèses en spécifiant [starting_index..ending_index], où starting_index est la première position dans la slice et ending_index est un de plus que la dernière position dans la slice.
En interne, la structure de données de la slice stocke la position de départ et la longueur de la tranche, qui correspond à l'index_fin moins l'index_départ.
Ainsi, dans le cas de let world = &s[6..11];, world serait une slice contenant un pointeur vers l'octet à l'index 6 de s avec une valeur de longueur de 5.
Tout comme nous pourrions vouloir faire référence à une partie d'une chaîne, nous pourrions vouloir faire référence à une partie d'un tableau. On ferait comme ça :
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]);
Cette tranche est de type &[i32]. Cela fonctionne de la même manière que les tranches de chaîne, en stockant une référence au premier élément et une longueur.
La syntaxe pour definir un enum est la suivante:
enum IpAddrKind { V4, V6, }
Option est une autre énumération définie par la bibliothèque standard. Le type Option encode le scénario très courant dans lequel une valeur peut être quelque chose ou rien.
Rust n'a pas la fonctionnalité null que beaucoup d'autres langages ont. Null est une valeur qui signifie qu'il n'y a aucune valeur ici. Dans les langages avec null, les variables peuvent toujours être dans l'un des deux états suivants : null ou non-null.
En tant que tel, Rust n'a pas de valeurs nulles, mais il a une énumération qui peut coder le concept d'une valeur présente ou absente. Cette énumération est Option<T>, et elle est définie par la bibliothèque standard comme suit :
enum Option<T> { None, Some(T), }
L'énumération Option<T> est si utile qu'elle est même incluse dans le prélude ; vous n'avez pas besoin de l'inclure explicitement dans la scope. Ses variantes sont également incluses dans le prélude : vous pouvez utiliser Some et None directement sans le préfixe Option ::. L'énumération Option<T> n'est toujours qu'une énumération normale, et Some(T) et None sont toujours des variantes du type Option<T>.
Pour l'instant, tout ce que vous devez savoir, c'est que <T> signifie que la variante Some de l'énumération Option peut contenir une donnée de n'importe quel type, et que chaque type concret utilisé à la place de T fait l'ensemble Option<T > tapez un autre type.
let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None;
Le type de some_number est Option<i32>. Le type de some_char est Option<char>, qui est un type différent.
Lorsque nous avons une valeur Some, nous savons qu'une valeur est présente et que la valeur est contenue dans Some. Lorsque nous avons une valeur None, cela signifie en quelque sorte la même chose que null : nous n'avons pas de valeur valide.
Rust a une construction de flux de contrôle extrêmement puissante appelée match qui vous permet de comparer une valeur à une série de modèles, puis d'exécuter du code en fonction du modèle qui correspond. Les modèles peuvent être constitués de valeurs littérales, de noms de variables, de caractères génériques et de bien d'autres choses.
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }