المسارات
/
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 أو اشتقاقها، وهي تُخبر المترجم بأنه يمكن تدمير النوع بأمان بمجرد انتهاء الحاجة إليه.

#[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.
}

في هذه الحالة، نحاول تمرير القيمة نفسها (بنية Point في المتغير p) إلى نداءي الدالة. بمجرد أن نمرّر المتغير إلى الدالة 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
}

لتحويل لقطة عائدة إلى متغير عادي، يمكنك استخدام عامل إلغاء اللقطة *، وهو عكس العامل @. ولا يمكن إلغاء لقطة إلا للأنواع التي تنفّذ سمة Copy.

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 
}

التعليمات

هناك مدرسة محلية قريبة منك لديها جمعية طلابية نشطة للغاية. تُدار الجمعية الطلابية بواسطة رئيس، وتُجرى الانتخابات مرة كل عامين لاختيار رئيس جديد. في انتخابات هذا العام، تقرر أن هناك حاجة إلى نظام رقمي جديد لإحصاء الأصوات.

تحتاج المدرسة إلى مساعدتك في بناء هذا النظام الجديد.

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 لتتعلّم وتتقن Cairo عبر 25 مفهومًا68 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.