選舉日

選舉日

學習練習

簡介

Struct

Struct 是一種自訂資料型態,讓你能把多個相關的值命名並包裝在一起。

要定義一個 struct,請使用struct關鍵字,後面接上名稱,以及一對大括號,裡面列出各個欄位及其名稱與型別:

#[derive(Drop)]
struct User {
    active: bool,
    username: ByteArray,
    email: ByteArray,
    sign_in_count: u64,
}

要建立struct的實例,請指定struct名稱,並用大括號搭配鍵值對來設定各個欄位。

let user = User {
   active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};

使用點記法來存取或修改可變struct實例的欄位。

let mut user = User {
    active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};
user.email = "anotheremail@example.com";

所有權

Cairo 中的值是不可變的,這樣的不可變性確保即使有許多變數參照同一個值,這個值本身依然保持不變、不會被更動。 變數的_擁有者_是可以讀取該變數(若為可變,還能寫入)的程式碼。

Cairo 的所有權規則:

  1. Cairo 中的每個變數都有一個擁有者。
  2. 同一時間只能有一個擁有者。
  3. 當擁有者離開作用域時,變數就會被銷毀。

作用域是程式中變數有效的範圍。

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

當變數離開作用域時就會被銷毀,這確保了相關的資源能被正確釋放。 這是透過實作或衍生Drop trait 來達成,它會向編譯器表示:這個型別在不再有用之後,可以被安全地銷毀。

#[derive(Drop)]
struct A {}

fn main() {
    A {}; // this value can be destroyed after going out of scope
}

目前所有型別都可以衍生Drop的實作,但字典(Felt252Dict)以及包含字典的型別除外。

值也可以被_移動_,這發生在把值傳給另一個函式時:原本參照該值的變數會被銷毀(因此變得無用),並建立一個新的變數來持有同一個值。

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
}

回傳值就等同於移動它們。

下面的範例展示了一個會回傳某個值的函式:

#[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
}

由於變數在被移動之後就會遭到銷毀,編譯器不允許我們之後再度使用它。

#[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.
}

在這個例子中,我們試著把同一個值(p變數裡的Point struct)傳給兩次函式呼叫。 一旦我們第一次把變數傳給foo函式,這個變數就不能再使用了。

其中一種解法是為我們的 struct 衍生Copy trait,這會改變把Copy變數傳入函式時的行為:它不再移動該值,而是建立一個參照同一個值的新變數。 這是一個零成本的運算,因為變數只是 Cairo 的一層抽象,而值永遠是不可變的。

要為你的型別實作Copy trait,請在型別定義加上#[derive(Copy)]標註。

#[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 有兩種機制可以在不銷毀也不移動值的情況下傳遞值,分別是references和snapshots。

快照提供值在某個時間點上不可變的「檢視」,讓你在存取變數狀態的同時仍保有它的所有權。 對變數的任何修改都會建立新的記憶體單元,而快照的檢視維持不變。

要建立快照,只要在變數實例前面加上@即可。

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

要把快照轉回一般的變數,你可以使用 desnap 運算子*,它的作用與@運算子正好相反。 只有實作Copy trait 的型別才能被 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);
}

參照讓你可以把可變的值傳入函式,並讓它們被隱式回傳,從而在呼叫端保有所有權。 只要變數是用mut關鍵字宣告為可變,就能用ref修飾詞做到這一點。

我們可以把先前的Point範例改成使用可變參照,而不依賴Copy trait,這樣依然能運作。

#[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 
}

說明

你附近有一所學校,他們的學生會非常活躍。 學生會由會長管理,每 2 年會舉辦一次選舉,選出新的會長。 今年的選舉決定需要一套新的數位計票系統。

學校需要你幫忙打造這套新系統。

1. 建立計票器

新的投票系統首先需要的東西之一,就是計票器。 計票器可以用來追蹤某位候選人目前的票數。

建立一個函式 new_vote_counter,它接受某位候選人的初始票數,並回傳一個以該初始票數初始化的 VoteCounter struct 快照。

let initial_votes: u32 = 2;
let counter: @VoteCounter = new_vote_counter(initial_votes);
// @VoteCounter {
//     value: 2,
// }

2. 從計票器取得票數

現在你已經有辦法建立新的計票器了! 但你發現,新系統還需要能從計票器取得票數的方法。

建立一個函式 vote_count,它會接受一個計票器(@VoteCounter)作為引數,並回傳該計票器中的票數。

let initial_votes: u32 = 3;
let vote_counter: @VoteCounter = new_vote_counter(initial_votes);

let count = vote_count(vote_counter);
// => 3

3. 增加計票器的票數

終於可以開始計票了! 現在你需要能增加計票器票數的方法。

建立一個函式 increment_vote_count,它會接受一個計票器(@VoteCounter)和一個票數作為引數,並將該計票器的票數增加那麼多。

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

4. 建立選舉結果

所有票都計完後,該準備向全校公布結果了。 你發現,只有計票器是不夠的。 需要有辦法把票數和特定的候選人關聯起來。

建立一個函式 new_election_result,它會接收候選人的名字和票數,並回傳一個新的選舉結果。

let result: ElectionResult = new_election_result("Peter", 3);
// ElectionResult {
//     name: "Peter",
//     votes: @VoteCounter {
//         value: 3,
//     },
// }

選舉結果的 struct 已經幫你建立好了,定義如下:

struct ElectionResult {
    name: ByteArray,
    counter: @VoteCounter,
}

5. 公布結果

該向學校公布新的會長了! 新會長會在學校的數位公布欄上公布。

訊息應該顯示新會長的名字和得票數,格式如下:<candidate_name> (<votes>)。 以下是這種訊息的範例:"Peter (51)"。

建立一個函式 display_result,它會接收一個 @ElectionResult 作為引數,並回傳含有要顯示訊息內容的 ByteArray。

let result = ElectionResult {
    name: "John",
    votes: @VoteCounter {
        value: 32,
    },
};

let msg = display_result(@result);
// => "John (32)"

6. 重新計票

為了確認最終結果是否準確,票數重新計算了一次。 重新計票時發現,有些候選人的票數差了一票。

建立一個函式 decrement_votes_of_candidate,它會接收最終結果,以及要減少票數的候選人名字。 最終結果以 Felt252Dict<u32> 的形式提供,其中鍵是候選人的名字,值是他們的總票數。

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
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Cairo Exercism

準備好開始 選舉日 了嗎?

註冊 Exercism,透過 25 個概念68 個練習 和真人引導來學習並精通 Cairo,全部免費。