구조체는 여러 관련된 값들을 하나로 묶고 이름을 붙일 수 있게 해 주는 사용자 정의 데이터 타입이에요.
구조체를 정의하려면 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 트레이트를 구현하거나 파생해서 이루어져요. 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년마다 새로운 회장을 뽑는 선거가 열려요. 올해 선거에서는 투표를 집계할 새로운 디지털 시스템이 필요하다는 결정이 내려졌어요.
학교는 이 새로운 시스템을 만드는 데 여러분의 도움이 필요해요.
새 투표 시스템에 가장 먼저 필요한 것 중 하나는 투표 집계기예요. 이 집계기는 특정 후보가 받은 투표 수를 계속 추적하는 방법이에요.
후보의 초기 투표 수를 받아서, 주어진 초기 투표 수로 초기화된 VoteCounter 구조체의 스냅샷을 반환하는 함수 new_vote_counter를 만들어요.
let initial_votes: u32 = 2;
let counter: @VoteCounter = new_vote_counter(initial_votes);
// @VoteCounter {
// value: 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
드디어 투표를 집계할 시간이에요! 이제 집계기의 투표 수를 늘리는 방법이 필요해요.
집계기(@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
이제 모든 투표를 집계했으니, 학교 전체에 결과를 발표할 준비를 할 시간이에요. 그런데 그러려면 투표 집계기만으로는 부족하다는 걸 알게 돼요. 투표 수를 특정 후보와 연결하는 방법이 필요해요.
후보의 이름과 그 후보의 투표 수를 받아서 새로운 선거 결과를 반환하는 함수 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)".
@ElectionResult를 인자로 받아서, 표시할 메시지를 담은 ByteArray를 반환하는 함수 display_result를 만들어요.
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