结构体是一种自定义数据类型,它让你能够把多个相关的值组合在一起并为之命名。
要定义结构体,使用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
}
目前,除字典(Felt252Dict)以及包含字典的类型之外,所有类型都可以派生Drop实现。
值也可以被_移动_。当把一个值传给另一个函数时就会发生移动:原来引用该值的变量被销毁(因此这个变量不再可用),同时创建一个新变量来持有同一个值。
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结构体)传给两个函数调用。
一旦我们第一次把该变量传给foo函数,这个变量就不能再用了。
解决这个问题的一种方法是给结构体派生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
}
要把快照转回普通变量,可以使用解快照运算符*,它的作用与@运算符相反。
只有实现了Copy trait 的类型才能被解快照。
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结构体的快照,该快照用给定的初始票数初始化。
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 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