This is an old revision of the document!


TP 01 - Ownership et Arguments du programme

Objectifs

Le but de ce TP est d'apprendre à utiliser

  • Le concept de Ownership
  • Le type Option
  • Utiliser des String
  • Arguments du programme

Resources

  • Enums en, fr
  • An I/O Project: Building a Command Line Program en, fr

Ownership

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.

Règles de Ownership

  1. Chaque valeur en Rust a un owner.
  2. Un valeur ne peut pas avoir qu'un seul owner à la fois.
  3. Quand le owner est out of scope, la valeur est supprimée.

Scope des variables

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

Mémoire et allocation

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.

Pour mieux comprendre, s'il vous plaît de lire cette section du chapitre 4!

Ownership et fonctions

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.

Valuers de retour et scope

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
}

References et Borrowing

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.

Mutable references

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");
}

Règles pour les références:

  1. À tout moment, vous pouvez avoir une référence mutable ou un nombre quelconque de références immuables.
  2. Les références doivent toujours être valides.

Slices

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.

String slices

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.

Autre slices

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.

Enums

La syntaxe pour definir un enum est la suivante:

enum IpAddrKind {
    V4,
    V6,
}

Pour comprendre mieux comment definir un enum, lisez cette section du chapitre 5!

Option enum

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.

Vous devez convertir une Option<T> en T avant de pouvoir effectuer des opérations T avec elle. Généralement, cela aide à détecter l'un des problèmes les plus courants avec null : supposer que quelque chose n'est pas null alors qu'il l'est réellement. Comment faire ca?

Match

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,
    }
}

Exercises

  1. Écrivez une fonction qui reçoit deux nombres comme arguments et les divise. Gérez le cas de la division par zéro à l'aide du type Option.
  2. Écrivez un programme qui reçoit deux nombres de la ligne de commande et les divise. Écrivez le résultat à l'écran. S'il y a une erreur, retournez -1 depuis main.
  3. Écrivez un programme qui reçoit en paramètres une commande et deux nombres. La commande est l'une des suivantes : add, sub, mul, div, rem. Écrivez le résultat à l'écran ou renvoyez une erreur.
  4. Écrivez un programme similaire au précédent, à l'exception qu'il prend la commande de la variable d'environnement CMD. Un exemple d'utilisation est CMD=sub ./my-program 5 3.
  5. Écrivez un programme qui prend comme premier argument une commande suivie d'une liste de nombres. Les commandes peuvent être add, sub, mul, div, avg (moyenne), sort, unique.
  6. Modifiez le programme afin qu'il n'exécute pas les commandes de la fonction principale, mais que chaque commande soit exécutée dans sa propre fonction. Ecrire des tests unitaires pour chacun d'eux. Les fonctions reçoivent comme arguments les entrées exactes de la ligne de commande (chaînes, pas de nombres).

Solutions

sde2/laboratoare/01_rust.1677952903.txt.gz · Last modified: 2023/03/04 20:01 by cristiana.andrei
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0