Structs sind benutzerdefinierte Datentypen, mit denen du mehrere zusammengehörige Werte benennen und bündeln kannst.
Um ein Struct zu definieren, verwendest du das Schlüsselwort struct, gefolgt von einem Namen und geschweiften Klammern, die die Felder mit ihren Namen und Typen enthalten:
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
Um eine Instanz eines struct zu erstellen, gibst du den Namen des struct an und verwendest geschweifte Klammern mit Schlüssel-Wert-Paaren für die Felder.
let user = User {
active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};
Mit der Punktnotation greifst du auf die Felder einer Instanz eines veränderbaren Structs zu oder änderst sie.
let mut user = User {
active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};
user.email = "anotheremail@example.com";
Werte in Cairo sind unveränderbar. Diese Unveränderbarkeit stellt sicher, dass ein Wert konstant und unverändert bleibt, auch wenn viele Variablen auf denselben Wert verweisen. Der Eigentümer einer Variablen ist der Code, der diese Variable lesen kann (und schreiben, wenn sie veränderbar ist).
Regeln der Ownership in Cairo:
Ein Gültigkeitsbereich ist der Bereich innerhalb eines Programms, in dem eine Variable gültig ist.
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
Wenn eine Variable den Gültigkeitsbereich verlässt, wird sie zerstört. Das stellt sicher, dass die zugehörige Ressource korrekt freigegeben wird.
Dazu implementierst du das Drop-Trait oder leitest es ab. Damit signalisierst du dem Compiler, dass der Typ sicher zerstört werden kann, sobald er nicht mehr benötigt wird.
#[derive(Drop)]
struct A {}
fn main() {
A {}; // this value can be destroyed after going out of scope
}
Momentan lässt sich die Drop-Implementierung für alle Typen ableiten, außer für Wörterbücher (Felt252Dict) und Typen, die Wörterbücher enthalten.
Ein Wert kann außerdem verschoben werden. Das passiert, wenn du einen Wert an eine andere Funktion übergibst: Die ursprüngliche Variable, die auf den Wert verwiesen hat, wird dabei zerstört (und damit nutzlos), und es entsteht eine neue Variable, die denselben Wert enthält.
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
}
Einen Wert zurückzugeben ist gleichbedeutend damit, ihn zu verschieben.
Das folgende Beispiel zeigt eine Funktion, die einen Wert zurückgibt:
#[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
}
Weil eine Variable nach dem Verschieben zerstört wird, verbietet uns der Compiler, sie danach weiterzuverwenden.
#[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.
}
In diesem Fall versuchen wir, denselben Wert (das Point-Struct in der Variablen p) an beide Funktionsaufrufe zu übergeben.
Sobald wir die Variable beim ersten Mal an die Funktion foo übergeben haben, ist sie nicht mehr verwendbar.
Eine Möglichkeit, das zu lösen, ist, das Copy-Trait für unser Struct abzuleiten. Damit ändert sich, was passiert, wenn wir eine Copy-Variable an eine Funktion übergeben: Der Wert wird nicht mehr verschoben, sondern es entsteht eine neue Variable, die auf denselben Wert verweist.
Das ist ein kostenloser Vorgang, denn Variablen sind eine Abstraktion von Cairo und Werte sind immer unveränderbar.
Um das Copy-Trait für deinen Typ zu implementieren, fügst du der Typdefinition die Annotation #[derive(Copy)] hinzu.
#[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 hat zwei Mechanismen, um einen Wert zu übergeben, ohne ihn zu zerstören oder zu verschieben: references und snapshots.
Snapshots liefern eine unveränderbare „Sicht“ auf einen Wert zu einem bestimmten Zeitpunkt. So behältst du die Ownership einer Variablen, während du auf ihren Zustand zugreifst. Jede Änderung an der Variablen erzeugt eine neue Speicherzelle, sodass die Sicht des Snapshots unverändert bleibt.
Um einen Snapshot zu erstellen, stellst du der Variablen einfach ein @ voran.
// 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
}
Um einen Snapshot wieder in eine reguläre Variable umzuwandeln, kannst du den Desnap-Operator * verwenden. Er ist das Gegenstück zum @-Operator.
Nur Typen, die das Copy-Trait implementieren, lassen sich desnappen.
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);
}
Mit Referenzen kannst du veränderbare Werte an Funktionen übergeben und sie implizit zurückgeben lassen, wobei die Ownership im aufrufenden Kontext bleibt.
Das gelingt mit dem ref-Modifizierer, sofern die Variable mit dem Schlüsselwort mut als veränderbar deklariert ist.
Wir können das vorherige Point-Beispiel so anpassen, dass es veränderbare Referenzen verwendet, statt sich auf das Copy-Trait zu verlassen. Es würde trotzdem funktionieren.
#[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
}
Eine Schule in deiner Nähe hat eine sehr aktive Schülervertretung. Die Schülervertretung wird von einem Präsidenten geleitet, und alle 2 Jahre finden Wahlen statt, um einen neuen Präsidenten zu wählen. Bei der diesjährigen Wahl wurde entschieden, dass ein neues digitales System zum Auszählen der Stimmen gebraucht wird.
Die Schule braucht deine Hilfe beim Aufbau dieses neuen Systems.
Eines der ersten Dinge, die das neue Wahlsystem braucht, ist ein Stimmenzähler. Mit diesem Zähler behältst du den Überblick über die Stimmen, die ein bestimmter Kandidat hat.
Erstelle eine Funktion new_vote_counter, die die Anzahl der anfänglichen Stimmen für einen Kandidaten entgegennimmt und eine Momentaufnahme einer VoteCounter-Struktur zurückgibt, die mit der angegebenen Anzahl an Anfangsstimmen initialisiert wurde.
let initial_votes: u32 = 2;
let counter: @VoteCounter = new_vote_counter(initial_votes);
// @VoteCounter {
// value: 2,
// }
Jetzt hast du eine Möglichkeit, neue Zähler zu erstellen! Aber jetzt fällt dir auf, dass das neue System auch eine Möglichkeit braucht, um die Anzahl der Stimmen aus einem Zähler zu ermitteln.
Erstelle eine Funktion vote_count, die einen Zähler (@VoteCounter) als Argument entgegennimmt und die Anzahl der Stimmen im Zähler zurückgibt.
let initial_votes: u32 = 3;
let vote_counter: @VoteCounter = new_vote_counter(initial_votes);
let count = vote_count(vote_counter);
// => 3
Endlich ist es Zeit, die Stimmen auszuzählen! Jetzt brauchst du eine Möglichkeit, die Stimmen in einem Zähler zu erhöhen.
Erstelle eine Funktion increment_vote_count, die einen Zähler (@VoteCounter) als Argument und eine Anzahl von Stimmen entgegennimmt und den Zähler um diese Anzahl von Stimmen erhöht.
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
Da nun alle Stimmen ausgezählt sind, ist es Zeit, die Bekanntgabe des Ergebnisses für die ganze Schule vorzubereiten. Dabei fällt dir auf, dass Zähler für die Stimmen allein nicht ausreichen. Es muss eine Möglichkeit geben, die Anzahl der Stimmen einem bestimmten Kandidaten zuzuordnen.
Erstelle eine Funktion new_election_result, die den Namen eines Kandidaten und seine Anzahl an Stimmen entgegennimmt und ein neues Wahlergebnis zurückgibt.
let result: ElectionResult = new_election_result("Peter", 3);
// ElectionResult {
// name: "Peter",
// votes: @VoteCounter {
// value: 3,
// },
// }
Die Struktur für das Wahlergebnis ist bereits für dich erstellt und ist so definiert:
struct ElectionResult {
name: ByteArray,
counter: @VoteCounter,
}
Es ist Zeit, der Schule den neuen Präsidenten bekanntzugeben! Der Präsident wird auf den kleinen digitalen Anzeigetafeln bekanntgegeben, die die Schule hat.
Die Nachricht sollte den Namen des neuen Präsidenten und die Anzahl der Stimmen im folgenden Format anzeigen: <candidate_name> (<votes>).
Ein Beispiel für eine solche Nachricht ist: "Peter (51)".
Erstelle eine Funktion display_result, die ein @ElectionResult als Argument entgegennimmt und ein ByteArray mit der anzuzeigenden Nachricht zurückgibt.
let result = ElectionResult {
name: "John",
votes: @VoteCounter {
value: 32,
},
};
let msg = display_result(@result);
// => "John (32)"
Um sicherzustellen, dass die endgültigen Ergebnisse korrekt waren, wurden die Stimmen neu ausgezählt. Bei der Nachzählung stellte sich heraus, dass die Anzahl der Stimmen einiger Kandidaten um eins daneben lag.
Erstelle eine Funktion decrement_votes_of_candidate, die die endgültigen Ergebnisse und den Namen eines Kandidaten entgegennimmt, für den du die Stimmenanzahl verringern sollst.
Die endgültigen Ergebnisse werden in Form eines Felt252Dict<u32> angegeben, wobei die Schlüssel die Namen der Kandidaten und die Werte ihre Gesamtstimmen sind.
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
Melde dich bei Exercism an, um Cairo mit 25 Konzepte68 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.