Διαδρομές
/
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" (*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

Δείκτες σε struct

Μέχρι τώρα έχουμε δει μόνο δείκτες σε πρωτόγονες τιμές. Μπορούμε επίσης να δημιουργήσουμε δείκτες για struct:

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}

Όταν έχουμε έναν δείκτη σε struct, δεν χρειάζεται να κάνουμε αποαναφορά του δείκτη πριν αποκτήσουμε πρόσβαση σε ένα από τα πεδία του:

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

Τα slice και τα map είναι ήδη δείκτες

Τα slice και τα map είναι ειδικοί τύποι, επειδή έχουν ήδη δείκτες στην υλοποίησή τους. Αυτό σημαίνει ότι τις περισσότερες φορές δεν χρειάζεται να δημιουργήσουμε δείκτες για αυτούς τους τύπους προκειμένου να μοιραστούμε τη διεύθυνση μνήμης των τιμών τους. Φαντάσου ότι έχουμε μια συνάρτηση που αυξάνει την τιμή ενός κλειδιού σε ένα map:

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

Αν δημιουργήσουμε ένα map και καλέσουμε αυτή τη συνάρτηση, οι αλλαγές που έκανε η συνάρτηση στο map παραμένουν μετά το τέλος της συνάρτησης. Αυτή είναι παρόμοια συμπεριφορά με αυτή που θα είχαμε αν χρησιμοποιούσαμε δείκτη, αλλά πρόσεξε ότι σε αυτό το παράδειγμα δεν χρησιμοποιούμε καθόλου αναφορά ή αποαναφορά ούτε κανένα από τα στοιχεία σύνταξης των δεικτών:

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!

Το ίδιο ισχύει όταν αλλάζουμε ένα υπάρχον στοιχείο σε ένα slice.

Ωστόσο, οι ενέργειες που επιστρέφουν ένα νέο slice, όπως η append, αποτελούν ειδική περίπτωση και μπορεί να μην τροποποιήσουν το slice έξω από τη συνάρτηση. Αυτό οφείλεται στον τρόπο με τον οποίο λειτουργούν τα slice εσωτερικά, αλλά δεν θα το καλύψουμε αναλυτικά σε αυτή την άσκηση, καθώς είναι ένα πιο προχωρημένο θέμα. Αν έχεις πραγματικά περιέργεια, μπορείς να διαβάσεις περισσότερα για αυτό στο Go Blog: Mechanics of 'append'

Οδηγίες

Ένα τοπικό σχολείο κοντά σου έχει έναν πολύ δραστήριο σύλλογο μαθητών. Ο σύλλογος μαθητών διοικείται από έναν πρόεδρο και κάθε 2 χρόνια γίνονται εκλογές για την ανάδειξη νέου προέδρου.

Στις φετινές εκλογές, αποφασίστηκε ότι χρειαζόταν ένα νέο ψηφιακό σύστημα για την καταμέτρηση των ψήφων. Το σχολείο χρειάζεται τη βοήθειά σου για να φτιαχτεί αυτό το νέο σύστημα.

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

Το struct του αποτελέσματος εκλογών έχει ήδη δημιουργηθεί για σένα και ορίζεται ως εξής:

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 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.