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 的所有權規則:
作用域是程式中變數有效的範圍。
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 年會舉辦一次選舉,選出新的會長。 今年的選舉決定需要一套新的數位計票系統。
學校需要你幫忙打造這套新系統。
新的投票系統首先需要的東西之一,就是計票器。 計票器可以用來追蹤某位候選人目前的票數。
建立一個函式 new_vote_counter,它接受某位候選人的初始票數,並回傳一個以該初始票數初始化的 VoteCounter struct 快照。
let initial_votes: u32 = 2;
let counter: @VoteCounter = new_vote_counter(initial_votes);
// @VoteCounter {
// value: 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
終於可以開始計票了! 現在你需要能增加計票器票數的方法。
建立一個函式 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
所有票都計完後,該準備向全校公布結果了。 你發現,只有計票器是不夠的。 需要有辦法把票數和特定的候選人關聯起來。
建立一個函式 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,
}
該向學校公布新的會長了! 新會長會在學校的數位公布欄上公布。
訊息應該顯示新會長的名字和得票數,格式如下:<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)"
為了確認最終結果是否準確,票數重新計算了一次。 重新計票時發現,有些候選人的票數差了一票。
建立一個函式 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