Les structures sont des types de données personnalisés qui permettent de nommer et de regrouper plusieurs valeurs liées entre elles.
Pour définir une structure, utilise le mot-clé struct suivi d'un nom et d'accolades contenant les champs avec leurs noms et leurs types :
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
Pour créer une instance d'une structure, précise le nom de la structure et utilise des accolades avec des paires clé-valeur pour les champs.
let user = User {
active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};
Utilise la notation pointée pour accéder aux champs d'une instance de structure mutable, ou pour les modifier.
let mut user = User {
active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};
user.email = "anotheremail@example.com";
En Cairo, les valeurs sont immuables, et cette immuabilité garantit que même si de nombreuses variables font référence à la même valeur, la valeur elle-même reste constante et inchangée. Le propriétaire d'une variable est le code qui peut lire cette variable (et la modifier si elle est mutable).
Les règles de la propriété en Cairo :
Une portée est l'intervalle, dans un programme, dans lequel une variable est valide.
fn main() {
// `x` is not valid here, it has not been declared yet
let x: u32 = 3; // `x` is declared, it is valid from this point forward
// do stuff with `x`
} // this scope ended here, `x` goes out of scope and is no longer valid
Quand une variable sort de sa portée, elle est détruite, ce qui garantit que la ressource associée est correctement libérée.
Cela se fait en implémentant ou en dérivant le trait Drop, qui indique au compilateur que le type peut être détruit sans risque une fois qu'il n'est plus utile.
#[derive(Drop)]
struct A {}
fn main() {
A {}; // this value can be destroyed after going out of scope
}
Pour le moment, l'implémentation de Drop peut être dérivée pour tous les types, sauf pour les dictionnaires (Felt252Dict) et les types qui contiennent des dictionnaires.
Une valeur peut aussi être déplacée : cela se produit quand une valeur est passée à une autre fonction, ce qui détruit la variable d'origine qui y faisait référence (la rendant inutilisable) et crée une nouvelle variable pour contenir la même valeur.
fn foo(num: u32) {
// do something with p
}
fn main() {
let num: u32 = 10;
foo(num);
// ownership of `num` was moved into `foo` and is thus no longer valid in this scope
}
Renvoyer une valeur équivaut à la déplacer.
L'exemple ci-dessous montre une fonction qui renvoie une valeur :
#[derive(Drop)]
struct A {}
fn main() {
// main now owns a1
let a1 = gives_ownership();
// a2 comes into scope
let a2 = A {};
// a2 is moved into takes_and_gives_back, which also moves its return value into a3
let a3 = takes_and_gives_back(a2);
} // Here, a3 and a1 go out of scope and are dropped. a2 was moved, so nothing happens.
// Moves its return value into the function that calls it
fn gives_ownership() -> A {
let some_a = A {};
some_a
}
// This function takes an instance some_a of A and returns it
fn takes_and_gives_back(some_a: A) -> A {
some_a
}
Comme une variable est détruite après avoir été déplacée, le compilateur interdit de la réutiliser ensuite.
#[derive(Drop)]
struct Point {
x: u128,
y: u128,
}
fn foo(p: Point) {
// do something with p
}
fn main() {
let p = Point { x: 5, y: 10 };
foo(p);
foo(p); // compiler error: Variable was previously moved.
}
Dans ce cas, on essaie de passer la même valeur (la structure Point contenue dans la variable p) aux deux appels de fonction.
Une fois que la variable a été passée à la fonction foo la première fois, elle n'est plus utilisable.
Une façon de régler le problème consiste à dériver le trait Copy sur notre structure : cela change ce qui se passe quand on passe une variable Copy à une fonction, car la valeur n'est plus déplacée, mais une nouvelle variable qui fait référence à la même valeur est créée.
C'est une opération sans coût, car les variables sont une abstraction de Cairo et les valeurs sont toujours immuables.
Pour implémenter le trait Copy sur ton type, ajoute l'annotation #[derive(Copy)] à la définition du type.
#[derive(Copy, Drop)]
struct Point {
x: u128,
y: u128,
}
fn foo(p: Point) {
// do something with p
}
fn main() {
let p = Point { x: 5, y: 10 };
foo(p);
foo(p); // this now works
}
Cairo propose deux mécanismes pour passer une valeur sans la détruire ni la déplacer : les references et les snapshots.
Les snapshots offrent une « vue » immuable d'une valeur à un instant donné, ce qui permet de conserver la propriété d'une variable tout en accédant à son état. Toute modification de la variable crée une nouvelle cellule mémoire, sans changer la vue du snapshot.
Pour créer un snapshot, il suffit de faire précéder l'instance de la variable du symbole @.
// this function takes in a snapshot using `@` with the data-type
fn get_len(arr: @Array<u128>) -> u32 { //
arr.len()
}
fn foo(arr: Array<u128>) {
arr.pop_front();
}
fn main() {
let mut new_arr: Array<u128> = array![1, 2, 3, 4, 5, 6, 7, 8];
let snapshot = @new_arr; // using `@` to create a snapshot of a variable
let len = get_len(snapshot); // pass the snapshot as argument, retaining ownership
foo(new_arr); // `new_arr` is still valid up until here and no further
}
Pour reconvertir un snapshot en variable ordinaire, tu peux utiliser l'opérateur desnap *, qui est l'inverse de l'opérateur @.
Seuls les types qui implémentent le trait Copy peuvent subir un desnap.
fn square(n: @u32) -> u32 {
let n = *n; // we desnap the value
n * n
}
fn main() {
let num: u32 = 10;
let squared = square(@num); // we use `@` to create a snapshot
println!("{} squared = {}", num, squared);
}
Les références permettent de passer des valeurs mutables à des fonctions et de se les faire renvoyer implicitement, tout en conservant la propriété dans le contexte appelant.
On y parvient en utilisant le modificateur ref, à condition que la variable soit déclarée mutable avec le mot-clé mut.
On peut adapter l'exemple précédent avec Point pour utiliser des références mutables plutôt que de compter sur le trait Copy, et cela fonctionnera quand même.
#[derive(Drop)]
struct Point {
x: u128,
y: u128,
}
fn foo(ref p: Point) {
// we can now update the passed values and the changes
// will be visible in the calling context
p.x += 1;
}
fn main() {
let mut p = Point { x: 5, y: 10 };
foo(ref p);
foo(ref p); // this now works even without the Copy trait
println!("p.x = {}", p.x); // p.x = 7
}
Une école près de chez toi a une association étudiante très active. L'association étudiante est dirigée par un président et, tous les 2 ans, des élections sont organisées pour élire un nouveau président. Pour l'élection de cette année, il a été décidé qu'un nouveau système numérique de comptage des votes était nécessaire.
L'école a besoin de ton aide pour construire ce nouveau système.
L'une des premières choses dont le nouveau système de vote a besoin est un compteur de votes. Ce compteur permet de suivre les votes obtenus par un candidat donné.
Crée une fonction new_vote_counter qui accepte le nombre de votes initiaux d'un candidat et renvoie un instantané d'une structure VoteCounter, initialisée avec le nombre de votes initiaux donné.
let initial_votes: u32 = 2;
let counter: @VoteCounter = new_vote_counter(initial_votes);
// @VoteCounter {
// value: 2,
// }
Tu as maintenant un moyen de créer de nouveaux compteurs ! Mais tu réalises maintenant que le nouveau système aura aussi besoin d'un moyen de récupérer le nombre de votes d'un compteur.
Crée une fonction vote_count qui prend un compteur (@VoteCounter) en argument et renvoie le nombre de votes du compteur.
let initial_votes: u32 = 3;
let vote_counter: @VoteCounter = new_vote_counter(initial_votes);
let count = vote_count(vote_counter);
// => 3
C'est enfin le moment de compter les votes ! Il te faut maintenant un moyen d'incrémenter les votes d'un compteur.
Crée une fonction increment_vote_count qui prend un compteur (@VoteCounter) et un nombre de votes en arguments, et qui incrémente le compteur de ce nombre de votes.
let votes: u32 = 3;
let vote_counter: @VoteCounter = new_vote_counter(initial_votes);
let vote_counter: VoteCounter = increment_vote_count(vote_counter, 2);
let count = vote_count(@vote_counter);
// => 5
Maintenant que tous les votes sont comptés, il est temps de préparer l'annonce des résultats pour toute l'école. Pour cela, tu remarques qu'avoir uniquement des compteurs de votes ne suffit pas. Il faut un moyen d'associer le nombre de votes à un candidat donné.
Crée une fonction new_election_result qui reçoit le nom d'un candidat et son nombre de votes, et renvoie un nouveau résultat d'élection.
let result: ElectionResult = new_election_result("Peter", 3);
// ElectionResult {
// name: "Peter",
// votes: @VoteCounter {
// value: 3,
// },
// }
La structure de résultat d'élection est déjà créée pour toi et elle est définie ainsi :
struct ElectionResult {
name: ByteArray,
counter: @VoteCounter,
}
Il est temps d'annoncer le nouveau président à toute l'école ! Le président sera annoncé sur les petits panneaux d'affichage numériques de l'école.
Le message doit afficher le nom du nouveau président et le nombre de votes obtenus, au format suivant : <candidate_name> (<votes>).
Voici un exemple d'un tel message : "Peter (51)".
Crée une fonction display_result qui reçoit un @ElectionResult en argument et renvoie un ByteArray contenant le message à afficher.
let result = ElectionResult {
name: "John",
votes: @VoteCounter {
value: 32,
},
};
let msg = display_result(@result);
// => "John (32)"
Pour s'assurer que les résultats finaux étaient exacts, les votes ont été recomptés. Lors du recomptage, on a constaté que le nombre de votes de certains candidats était erroné d'une unité.
Crée une fonction decrement_votes_of_candidate qui reçoit les résultats finaux et le nom d'un candidat dont il faut décrémenter le nombre de votes.
Les résultats finaux sont fournis sous la forme d'un Felt252Dict<u32>, où les clés sont les noms des candidats et les valeurs leur nombre total de votes.
let mut final_results: Felt252Dict<u32> = Default::default();
final_results.insert('Mary', 10);
final_results.insert('John', 51);
decrement_votes_of_candidate(ref final_results, 'Mary');
let decremented_votes = final_results.get('Mary');
// => 9
Inscris-toi sur Exercism pour apprendre et maîtriser Cairo avec 25 concepts68 exercices, et un vrai mentorat humain, le tout gratuitement.