轨道
/
Cairo
Cairo
/
练习
/
选举日
选举日

选举日

学习练习

简介

结构体

结构体是一种自定义数据类型,它让你能够把多个相关的值组合在一起并为之命名。

要定义结构体,使用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
}

目前,除字典(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 年就会举行一次选举,选出新一任主席。 在今年的选举中,大家决定需要一个全新的数字化计票系统。

学校需要你的帮助来构建这个新系统。

1. 创建投票计数器

新投票系统最先需要的东西之一就是投票计数器。 这个计数器用来记录某位候选人获得的票数。

创建一个new_vote_counter函数,它接受某位候选人的初始票数,并返回一个VoteCounter结构体的快照,该快照用给定的初始票数初始化。

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 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,全部免费。