선거일

선거일

학습 연습 문제

소개

구조체

구조체는 여러 관련된 값들을 하나로 묶고 이름을 붙일 수 있게 해 주는 사용자 정의 데이터 타입이에요.

구조체를 정의하려면 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 트레이트를 구현하거나 파생해서 이루어져요. Drop 트레이트는 더 이상 쓸모가 없어진 타입을 안전하게 소멸할 수 있다고 컴파일러에게 알려줘요.

#[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 트레이트를 파생하는 거예요. 그러면 Copy 변수를 함수에 전달할 때 일어나는 일이 달라져요. 값을 이동하는 대신 같은 값을 가리키는 새 변수를 만들어요. 변수는 Cairo의 추상화이고 값은 항상 불변이기 때문에, 이 작업에는 비용이 들지 않아요.

타입에 Copy 트레이트를 구현하려면 타입 정의에 #[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 트레이트를 구현한 타입만 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);
}

참조를 사용하면 가변 값을 함수에 전달하고 그 값을 암묵적으로 돌려받을 수 있어요. 소유권은 호출한 쪽에 그대로 유지돼요. 이는 ref 한정자를 사용해 할 수 있는데, 변수가 mut 키워드로 가변으로 선언되어 있어야 해요.

앞의 Point 예제를 Copy 트레이트에 의존하는 대신 가변 참조를 사용하도록 바꿔도 여전히 잘 동작해요.

#[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. 투표 집계기 만들기

새 투표 시스템에 가장 먼저 필요한 것 중 하나는 투표 집계기예요. 이 집계기는 특정 후보가 받은 투표 수를 계속 추적하는 방법이에요.

후보의 초기 투표 수를 받아서, 주어진 초기 투표 수로 초기화된 VoteCounter 구조체의 스냅샷을 반환하는 함수 new_vote_counter를 만들어요.

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

2. 집계기에서 투표 수 가져오기

이제 새로운 집계기를 만들 수 있게 됐어요! 그런데 이제 보니, 새 시스템에는 집계기에서 투표 수를 가져오는 방법도 필요해요.

집계기(@VoteCounter)를 인자로 받아서 집계기의 투표 수를 반환하는 함수 vote_count를 만들어요.

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

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

3. 집계기의 투표 수 늘리기

드디어 투표를 집계할 시간이에요! 이제 집계기의 투표 수를 늘리는 방법이 필요해요.

집계기(@VoteCounter)와 투표 수를 인자로 받아서, 그 투표 수만큼 집계기를 늘리는 함수 increment_vote_count를 만들어요.

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)".

@ElectionResult를 인자로 받아서, 표시할 메시지를 담은 ByteArray를 반환하는 함수 display_result를 만들어요.

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에 가입하고 Cairo 트랙을 개념 25개연습 문제 68개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.