المسارات
/
Go
Go
/
التمارين
/
يوم الانتخابات
يوم الانتخابات

يوم الانتخابات

تمرين تعلّمي

مقدمة

كما في كثير من اللغات الأخرى، تحتوي لغة Go على المؤشرات. إذا كنت جديدًا على المؤشرات، فقد تبدو غامضة بعض الشيء، لكنها تصبح بسيطة جدًا بمجرد أن تعتاد عليها. إنها جزء أساسي من Go، لذا خصّص بعض الوقت لفهمها حقًا.

قبل الخوض في التفاصيل، يجدر بك فهم الغرض من استخدام المؤشرات. المؤشرات طريقة لمشاركة الذاكرة مع أجزاء أخرى من برنامجنا، وهذا مفيد لسببين رئيسيين:

  1. عندما تكون لدينا كميات كبيرة من البيانات، يصبح إنشاء نسخ لتمريرها بين الدوال غير فعّال إلى حد كبير. وبتمرير موقع الذاكرة الذي تُخزَّن فيه البيانات بدلًا من ذلك، يمكننا تقليص استهلاك برامجنا للموارد بشكل كبير.
  2. وبتمرير المؤشرات بين الدوال، يمكننا الوصول إلى النسخة الوحيدة من البيانات وتعديلها مباشرةً، ما يعني أن أي تغييرات تجريها إحدى الدوال تصبح مرئية فورًا لأجزاء أخرى من البرنامج عند انتهاء الدالة.

المتغيرات والذاكرة

لنفترض أن لدينا متغيرًا عاديًا من نوع عدد صحيح اسمه a:

var a int

عندما نُعلن عن متغير، يجب على Go أن تجد مكانًا في الذاكرة لتخزين قيمته. وهذا الأمر مجرّد عنّا إلى حد كبير: عندما نحتاج إلى جلب القيمة المخزّنة في ذلك الجزء من الذاكرة، يكفي أن نشير إليها باسم المتغير.

على سبيل المثال، عندما نكتب a + 2، فإننا في الواقع نجلب القيمة المخزّنة في الذاكرة المرتبطة بالمتغير a ونضيف إليها 2.

وبالمثل، عندما نحتاج إلى تغيير القيمة في الجزء الخاص بالمتغير a من الذاكرة، يمكننا استخدام اسم المتغير لإجراء إسناد:

a = 3

سيخزّن الجزء من الذاكرة المرتبط بالمتغير a الآن القيمة 3.

المؤشرات

بينما تتيح لنا المتغيرات الإشارة إلى القيم في الذاكرة، فإنه من المفيد أحيانًا معرفة عنوان الذاكرة الذي يشير إليه المتغير. المؤشرات تحمل عناوين الذاكرة لتلك القيم. يمكنك الإعلان عن متغير من نوع مؤشر بوضع نجمة قبل النوع الأساسي:

var p *int // 'p' contains the memory address of an integer

هنا نُعلن عن متغير p من نوع «مؤشر إلى عدد صحيح» (*int). هذا يعني أن p سيحمل عنوان الذاكرة الخاص بعدد صحيح. القيمة الصفرية للمؤشرات هي nil، لأن المؤشر nil لا يحمل أي عنوان ذاكرة.

الحصول على مؤشر إلى متغير

لمعرفة عنوان الذاكرة الخاص بقيمة متغير، يمكننا استخدام عامل &. على سبيل المثال، إذا أردنا معرفة عنوان الذاكرة الخاص بالمتغير a وتخزينه في المؤشر p، يمكننا فعل ما يلي:

var a int
a = 2

var p *int
p = &a // the variable 'p' contains the memory address of 'a'

الوصول إلى القيمة عبر مؤشر (إلغاء الإشارة)

عندما يكون لدينا مؤشر، قد نرغب في معرفة القيمة المخزّنة في عنوان الذاكرة الذي يمثله المؤشر. يمكننا فعل ذلك باستخدام عامل *:

var a int
a = 2

var p *int
p = &a // the variable 'p' contains the memory address of 'a'

var b int
b = *p // b == 2

تجلب العملية *p القيمة المخزّنة في عنوان الذاكرة الذي يحمله p. غالبًا ما تُسمّى هذه العملية «إلغاء الإشارة».

يمكننا أيضًا استخدام عامل إلغاء الإشارة لإسناد قيمة جديدة إلى عنوان الذاكرة الذي يشير إليه المؤشر:

var a int        // declare int variable 'a'
a = 2            // assign 'a' the value of 2

var pa *int
pa = &a          // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2    // increment by 2 the value at memory address 'pa'

fmt.Println(a)   // Output: 4
                 // 'a' will have the new value that was changed via the pointer!

إسناد قيمة إلى *pa سيغيّر القيمة المخزّنة في عنوان الذاكرة الذي يحمله pa. وبما أن pa يحمل عنوان الذاكرة الخاص بالمتغير a، فإننا بإسناد قيمة إلى *pa نغيّر فعليًا قيمة a!

لكن خذ حذرك: تحقّق دائمًا من أن المؤشر ليس nil قبل إلغاء الإشارة إليه. فإلغاء الإشارة إلى مؤشر nil سيجعل البرنامج ينهار وقت التشغيل!

var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference

مؤشرات إلى البنى

حتى الآن رأينا مؤشرات إلى قيم أولية فقط. يمكننا أيضًا إنشاء مؤشرات إلى البنى:

type Person struct {
    Name string
    Age  int
}

var peter Person
peter = Person{Name: "Peter", Age: 22}

var p *Person
p = &peter

كان يمكننا أيضًا إنشاء بنية Person جديدة وتخزين مؤشر إليها على الفور:

var p *Person
p = &Person{Name: "Peter", Age: 22}

عندما يكون لدينا مؤشر إلى بنية، لا نحتاج إلى إلغاء الإشارة قبل الوصول إلى أحد الحقول:

var p *Person
p = &Person{Name: "Peter", Age: 22}

fmt.Println(p.Name) // Output: "Peter"
                    // Go automatically dereferences 'p' to allow
                    // access to the 'Name' field

الشرائح والخرائط مؤشرات بالفعل

الشرائح والخرائط أنواع خاصة لأنها تحتوي بالفعل على مؤشرات في تنفيذها الداخلي. وهذا يعني أننا في أغلب الأحيان لا نحتاج إلى إنشاء مؤشرات لهذه الأنواع لمشاركة عنوان الذاكرة الخاص بقيمها. تخيّل أن لدينا دالة تزيد قيمة مفتاح في خريطة:

func incrementPeterAge(m map[string]int) {
	m["Peter"] += 1
}

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

ages := map[string]int{
  "Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!

ينطبق الأمر نفسه عند تغيير عنصر موجود في شريحة.

لكن الإجراءات التي تُرجع شريحة جديدة مثل append حالة خاصة، وقد لا تعدّل الشريحة خارج الدالة. يعود ذلك إلى كيفية عمل الشرائح داخليًا، لكننا لن نتناول هذا بالتفصيل في هذا التمرين، فهو موضوع أكثر تقدمًا. وإذا كنت فضوليًا حقًا، يمكنك قراءة المزيد عن هذا في مدونة Go: آليات 'append'

التعليمات

توجد في مدرسة محلية قريبة منك جمعية طلابية نشطة للغاية. تُدار الجمعية الطلابية بواسطة رئيس، وكل سنتين تُجرى انتخابات لاختيار رئيس جديد.

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

1. إنشاء عدّاد أصوات

من أول الأشياء التي يحتاجها نظام التصويت الجديد عدّاد أصوات. هذا العدّاد وسيلة لتتبّع الأصوات التي حصل عليها مرشح معيّن.

أنشئ دالة NewVoteCounter تقبل عدد الأصوات الأولية لمرشح وتُرجع مؤشرًا يشير إلى int، مُهيَّأ بعدد الأصوات الأولية المعطى.

var initialVotes int
initialVotes = 2

var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true

2. الحصول على عدد الأصوات من عدّاد

أصبح لديك الآن وسيلة لإنشاء عدّادات جديدة! لكنك تدرك الآن أن النظام الجديد سيحتاج أيضًا إلى وسيلة للحصول على عدد الأصوات من عدّاد.

أنشئ دالة VoteCount تأخذ عدّادًا (*int) كوسيط وتُرجع عدد الأصوات في العدّاد. إذا كان العدّاد nil، فافترض أن العدّاد لا يحتوي على أصوات:

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

VoteCount(voteCounter)
// => 3

var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0

3. زيادة أصوات عدّاد

حان الوقت أخيرًا لعدّ الأصوات! تحتاج الآن إلى وسيلة لزيادة الأصوات في عدّاد.

أنشئ دالة IncrementVoteCount تأخذ عدّادًا (*int) كوسيط وعددًا من الأصوات، وتزيد العدّاد بمقدار ذلك العدد من الأصوات. يمكنك افتراض أن المؤشر الممرَّر لن يكون nil أبدًا.

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

IncrementVoteCount(voteCounter, 2)

votes == 5          // true
*voteCounter == 5   // true

4. إنشاء نتائج الانتخابات

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

أنشئ دالة NewElectionResult تستقبل اسم مرشح وعدد أصواته وتُرجع نتيجة انتخابية جديدة.

var result *ElectionResult
result = NewElectionResult("Peter", 3)

result.Name == "Peter"  // true
result.Votes == 3       // true

البنية ElectionResult للنتيجة الانتخابية أُنشئت لك بالفعل، وهي مُعرَّفة كما يلي:

type ElectionResult struct {
    // Name of the candidate
    Name    string
    // Votes of votes the candidate had
    Votes   int
}

5. إعلان النتائج

حان الوقت لإعلان الرئيس الجديد للمدرسة! سيُعلن الرئيس في لوحات الرسائل الرقمية الصغيرة الموجودة في المدرسة. يجب أن تُظهر الرسالة اسم الرئيس الجديد وعدد الأصوات التي حصل عليها، بالصيغة التالية: <candidate_name> (<votes>). وهذا مثال على هذه الرسالة: "Peter (51)".

أنشئ دالة DisplayResult تستقبل *ElectionResult كوسيط وتُرجع سلسلة نصية تحتوي على الرسالة التي ستُعرض.

var result *ElectionResult
result = &ElectionResult{
    Name: "John",
    Votes: 32,
}

DisplayResult(result)
// => John (32)

6. إعادة عدّ الأصوات

للتأكد من دقة النتائج النهائية، أُعيد عدّ الأصوات. وخلال إعادة العدّ، وُجد أن عدد أصوات بعض المرشحين كان يختلف بمقدار واحد.

أنشئ دالة DecrementVotesOfCandidate تستقبل النتائج النهائية واسم المرشح الذي يجب إنقاص عدد أصواته. تُعطى النتائج النهائية على شكل map[string]int، حيث المفاتيح هي أسماء المرشحين والقيم هي إجمالي أصواتهم.

var finalResults = map[string]int{
    "Mary":  10,
    "John":  51,
}

DecrementVotesOfCandidate(finalResults, "Mary")

finalResults["Mary"]
// => 9
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Go Exercism

مستعد لبدء يوم الانتخابات؟

سجّل في Exercism لتتعلّم وتتقن Go عبر 34 مفهومًا165 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.