TP 01 - Ownership et Arguments du programme

Assignment

Vous devez accepter le assignment d'ici et travailler avec ce repository: Lab1

Objectifs

Le but de ce TP est d'apprendre à utiliser

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

Resources

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 6!

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

Lorsque l'expression de match s'exécute, elle compare la valeur résultante au modèle de chaque bras, dans l'ordre. Si un modèle correspond à la valeur, le code associé à ce modèle est exécuté. Si ce modèle ne correspond pas à la valeur, l'exécution continue jusqu'au bras suivant.

Le code associé à chaque bras est une expression, et la valeur résultante de l'expression dans le bras correspondant est la valeur renvoyée pour l'ensemble de l'expression de correspondance.

Une autre caractéristique utile des bras de match est qu'ils peuvent se lier aux parties des valeurs qui correspondent au modèle. C'est ainsi que nous pouvons extraire les valeurs des variantes enum.

Dans la section précédente, nous voulions extraire la valeur T interne du cas Some lors de l'utilisation de Option<T> ; nous pouvons également gérer Option<T> en utilisant match, comme nous l'avons fait avec l'énumération Coin ! Au lieu de comparer des pièces, nous comparerons les variantes de Option<T>, mais le fonctionnement de l'expression de correspondance reste le même.

    fn get_option(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i),
        }
    }
 
    let five = Some(5);
    let six = get_option(five);
    let none = get_option(None);

Pour comprendre mieux comment match fonctionne, lisez cette section du chapitre 6!

Vecteurs

Les vecteurs vous permettent de stocker plusieurs valeurs dans une seule structure de données qui place toutes les valeurs les unes à côté des autres en mémoire. Les vecteurs ne peuvent stocker que des valeurs du même type.

Pour créer un nouveau vecteur vide, nous appelons la fonction Vec::new.

let v: Vec<i32> = Vec::new();

Lorsque nous créons un vecteur pour contenir un type spécifique, nous pouvons spécifier le type entre crochets.

Push

Pour créer un vecteur puis y ajouter des éléments, nous pouvons utiliser la méthode push.

    let mut v = Vec::new();
 
    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);

Comme pour toute variable, si nous voulons pouvoir changer sa valeur, nous devons la rendre modifiable en utilisant le mot-clé mut.

Lire les elements

Il existe deux façons de référencer une valeur stockée dans un vecteur : via l'indexation ou en utilisant la méthode get.

    let v = vec![1, 2, 3, 4, 5];
 
    let third: &i32 = &v[2];
    println!("The third element is {third}");
 
    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }

Rust fournit commodément le vec! macro, qui créera un nouveau vecteur contenant les valeurs que vous lui donnez.

Itérer un vecteur

Pour accéder à tour de rôle à chaque élément d'un vecteur, nous parcourrions tous les éléments plutôt que d'utiliser des indices pour accéder à un à la fois. L'example montre comment utiliser une boucle for pour obtenir des références immuables à chaque élément dans un vecteur de valeurs i32 et les imprimer.

 let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }

Nous pouvons également itérer sur des références mutables à chaque élément dans un vecteur mutable afin d'apporter des modifications à tous les éléments.

    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }

Strings

Rust n'a qu'un seul type de chaîne dans le langage de base, qui est la tranche de chaîne str qui est généralement vue sous sa forme empruntée(borrowed) &str.

Le type String, qui est fourni par la bibliothèque standard de Rust plutôt que codé dans le langage principal, est un type de chaîne encodé UTF-8 évolutif, mutable et propriétaire(owned).

Création d'une nouvelle String
    let mut s = String::new();

Cette ligne crée une nouvelle chaîne vide appelée s, dans laquelle nous pouvons ensuite charger des données.

Nous pouvons utiliser la fonction String :: from ou la fonction to_string() pour créer une chaîne à partir d'un littéral de chaîne:

    let s = String::from("initial contents");
let data = "initial contents";
 
    let s = data.to_string();
 
    // the method also works on a literal directly:
    let s = "initial contents".to_string();
Ajout à une chaîne

Nous pouvons développer une chaîne en utilisant la méthode push_str pour ajouter une tranche de chaîne.

    let mut s = String::from("foo");
    s.push_str("bar");

La méthode push prend un seul caractère comme paramètre et l'ajoute à la chaîne.

    let mut s = String::from("lo");
    s.push('l');
Méthodes d'itération sur les chaînes

La meilleure façon d'opérer sur des morceaux de chaînes est d'être explicite quant à savoir si vous voulez des caractères ou des octets. Pour les valeurs scalaires Unicode individuelles, utilisez la méthode chars.

for c in "Зд".chars() {
    println!("{c}");
}

Accepter les arguments de la ligne de commande

Pour permettre à notre programme de lire les valeurs des arguments de ligne de commande que nous lui transmettons, nous aurons besoin de la fonction std::env::args fournie dans la bibliothèque standard de Rust. Cette fonction renvoie un itérateur des arguments de ligne de commande transmis à notre programme.

Pour l'instant, vous n'avez besoin de connaître que deux détails sur les itérateurs : les itérateurs produisent une série de valeurs, et nous pouvons appeler la méthode collect sur un itérateur pour le transformer en une collection, telle qu'un vecteur, qui contient tous les éléments produits par l'itérateur.

use std::env;
 
fn main() {
    let args: Vec<String> = env::args().collect();
    println!(args);
}

Sur la première ligne de main, nous appelons env::args, et nous utilisons immédiatement collect pour transformer l'itérateur en un vecteur contenant toutes les valeurs produites par l'itérateur.

La première valeur retournée par env::args est le nom de notre exécutable!

Enregistrement des valeurs d'argument dans les variables

Le programme est actuellement en mesure d'accéder aux valeurs spécifiées en tant qu'arguments de ligne de commande. Nous devons maintenant enregistrer les valeurs des deux arguments dans des variables afin de pouvoir utiliser les valeurs dans le reste du programme.

use std::env;
 
fn main() {
    let args: Vec<String> = env::args().collect();
 
    let query = &args[1];
    let file_path = &args[2];
 
    println!("Searching for {}", query);
    println!("In file {}", file_path);
}

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. Hint: Result et std::process::exit
  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=mul cargo run 5 3. Hint: Utilisez env::var() pour prendre la valeur de CMD.
  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. Les fonctions reçoivent comme arguments les entrées exactes de la ligne de commande (chaînes, pas de nombres).
sde2/laboratoare/01_rust.txt · Last modified: 2023/03/07 07:57 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