Tracks
/
Go
Go
/
Übungen
/
Wahltag
Wahltag

Wahltag

Lernübung

Einführung

Wie viele andere Sprachen hat Go Pointer. Wenn du neu bei Pointern bist, können sie etwas geheimnisvoll wirken, aber sobald du dich an sie gewöhnt hast, sind sie ganz unkompliziert. Sie sind ein entscheidender Teil von Go, also nimm dir etwas Zeit, sie wirklich zu verstehen.

Bevor wir in die Details eintauchen, lohnt es sich zu verstehen, wofür Pointer verwendet werden. Pointer sind eine Möglichkeit, Speicher mit anderen Teilen unseres Programms zu teilen, was aus zwei wichtigen Gründen nützlich ist:

  1. Wenn wir große Datenmengen haben, ist es sehr ineffizient, Kopien zu erstellen, um sie zwischen Funktionen zu übergeben. Indem wir stattdessen die Speicheradresse übergeben, an der die Daten gespeichert sind, können wir den Ressourcenverbrauch unserer Programme drastisch reduzieren.
  2. Indem wir Pointer zwischen Funktionen übergeben, können wir direkt auf die einzige Kopie der Daten zugreifen und sie ändern. Das bedeutet, dass alle Änderungen, die eine Funktion vornimmt, für andere Teile des Programms sofort sichtbar sind, wenn die Funktion endet.

Variablen und Speicher

Nehmen wir an, wir haben eine gewöhnliche Variable a vom Typ Ganzzahl:

var a int

Wenn wir eine Variable deklarieren, muss Go einen Platz im Speicher finden, um ihren Wert zu speichern. Das ist weitgehend von uns abstrahiert. Wenn wir den Wert abrufen müssen, der an dieser Speicherstelle gespeichert ist, können wir ihn einfach über den Variablennamen ansprechen.

Wenn wir zum Beispiel a + 2 schreiben, rufen wir im Grunde den Wert ab, der im Speicher der Variable a gespeichert ist, und addieren 2 dazu.

Ähnlich verhält es sich, wenn wir den Wert im Speicher von a ändern möchten: Wir können den Variablennamen verwenden, um eine Zuweisung vorzunehmen:

a = 3

Die Speicherstelle, die mit a verknüpft ist, speichert jetzt den Wert 3.

Pointer

Variablen ermöglichen es uns, auf Werte im Speicher zu verweisen. Manchmal ist es jedoch nützlich, die Speicheradresse zu kennen, auf die die Variable zeigt. Pointer enthalten die Speicheradressen dieser Werte. Du deklarierst eine Variable mit einem Pointer-Typ, indem du dem zugrunde liegenden Typ ein Sternchen voranstellst:

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

Hier deklarieren wir eine Variable p vom Typ „Pointer auf int“ (*int). Das bedeutet, dass p die Speicheradresse einer Ganzzahl enthält. Der Nullwert von Pointern ist nil, weil ein nil-Pointer keine Speicheradresse enthält.

So bekommst du einen Pointer auf eine Variable

Um die Speicheradresse des Werts einer Variable zu finden, können wir den Operator & verwenden. Wenn wir zum Beispiel die Speicheradresse der Variable a ermitteln und im Pointer p speichern möchten, können wir Folgendes tun:

var a int
a = 2

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

Über einen Pointer auf den Wert zugreifen (Dereferenzierung)

Wenn wir einen Pointer haben, möchten wir vielleicht den Wert kennen, der an der Speicheradresse gespeichert ist, die der Pointer darstellt. Das können wir mit dem Operator * tun:

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

Die Operation *p ruft den Wert ab, der an der Speicheradresse liegt, die in p gespeichert ist. Diese Operation wird oft als „Dereferenzierung“ bezeichnet.

Wir können den Dereferenzierungsoperator auch verwenden, um der Speicheradresse, auf die der Pointer verweist, einen neuen Wert zuzuweisen:

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!

Eine Zuweisung an *pa ändert den Wert, der an der Speicheradresse gespeichert ist, die pa enthält. Da pa die Speicheradresse von a enthält, ändern wir durch die Zuweisung an *pa effektiv den Wert von a!

Ein Hinweis zur Vorsicht: Prüfe immer, ob ein Pointer nicht nil ist, bevor du ihn dereferenzierst. Das Dereferenzieren eines nil-Pointers lässt das Programm zur Laufzeit abstürzen!

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

Pointer auf Structs

Bisher haben wir nur Pointer auf primitive Werte gesehen. Wir können auch Pointer für Structs erstellen:

type Person struct {
    Name string
    Age  int
}

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

var p *Person
p = &peter

Wir hätten auch eine neue Person erstellen und sofort einen Pointer darauf speichern können:

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

Wenn wir einen Pointer auf ein Struct haben, müssen wir den Pointer nicht dereferenzieren, bevor wir auf eines der Felder zugreifen:

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

Slices und Maps sind bereits Pointer

Slices und Maps sind besondere Typen, weil sie bereits Pointer in ihrer Implementierung enthalten. Das bedeutet, dass wir für diese Typen meist keine Pointer erstellen müssen, um die Speicheradresse ihrer Werte zu teilen. Stell dir vor, wir haben eine Funktion, die den Wert eines Schlüssels in einer Map erhöht:

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

Wenn wir eine Map erstellen und diese Funktion aufrufen, bleiben die Änderungen, die die Funktion an der Map vorgenommen hat, nach dem Ende der Funktion erhalten. Das ist ein ähnliches Verhalten, wie wir es mit einem Pointer erhalten würden. Beachte jedoch, dass wir in diesem Beispiel keine Referenzierung/Dereferenzierung oder irgendeine Pointer-Syntax verwenden:

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!

Dasselbe gilt, wenn du ein vorhandenes Element in einem Slice änderst.

Allerdings sind Aktionen, die ein neues Slice zurückgeben, wie append, ein Sonderfall und ändern das Slice außerhalb der Funktion möglicherweise nicht. Das liegt an der internen Funktionsweise von Slices, aber wir werden dies in dieser Übung nicht im Detail behandeln, da es ein fortgeschritteneres Thema ist. Wenn du wirklich neugierig bist, kannst du mehr darüber im Go-Blog: Mechanics of 'append' lesen.

Anleitung

Eine Schule in deiner Nähe hat eine sehr aktive Schülervereinigung. Die Schülervereinigung wird von einem Präsidenten geleitet, und alle 2 Jahre finden Wahlen statt, um einen neuen Präsidenten zu wählen.

Bei der diesjährigen Wahl wurde entschieden, dass ein neues digitales System zum Auszählen der Stimmen benötigt wird. Die Schule braucht deine Hilfe beim Aufbau dieses neuen Systems.

1. Erstelle einen Stimmenzähler

Eines der ersten Dinge, die das neue Wahlsystem braucht, ist ein Stimmenzähler. Dieser Zähler ist eine Möglichkeit, die Stimmen eines bestimmten Kandidaten im Blick zu behalten.

Erstelle eine Funktion NewVoteCounter, die die Anzahl der anfänglichen Stimmen für einen Kandidaten entgegennimmt und einen Zeiger auf einen int zurückgibt, der mit der angegebenen Anzahl anfänglicher Stimmen initialisiert ist.

var initialVotes int
initialVotes = 2

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

2. Ermittle die Anzahl der Stimmen aus einem Zähler

Jetzt hast du eine Möglichkeit, neue Zähler zu erstellen! Aber jetzt stellst du fest, dass das neue System auch eine Möglichkeit braucht, die Anzahl der Stimmen aus einem Zähler zu ermitteln.

Erstelle eine Funktion VoteCount, die einen Zähler (*int) als Argument entgegennimmt und die Anzahl der Stimmen im Zähler zurückgibt. Wenn der Zähler nil ist, solltest du davon ausgehen, dass der Zähler keine Stimmen hat:

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

VoteCount(voteCounter)
// => 3

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

3. Erhöhe die Stimmen eines Zählers

Endlich ist es Zeit, die Stimmen auszuzählen! Jetzt brauchst du eine Möglichkeit, die Stimmen in einem Zähler zu erhöhen.

Erstelle eine Funktion IncrementVoteCount, die einen Zähler (*int) als Argument und eine Anzahl von Stimmen entgegennimmt und den Zähler um diese Anzahl von Stimmen erhöht. Du kannst davon ausgehen, dass der übergebene Zeiger nie nil ist.

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

IncrementVoteCount(voteCounter, 2)

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

4. Erstelle die Wahlergebnisse

Da nun alle Stimmen ausgezählt sind, ist es Zeit, die Bekanntgabe der Ergebnisse für die ganze Schule vorzubereiten. Dabei fällt dir auf, dass es nicht ausreicht, nur Zähler für die Stimmen zu haben. Es muss eine Möglichkeit geben, die Anzahl der Stimmen einem bestimmten Kandidaten zuzuordnen.

Erstelle eine Funktion NewElectionResult, die den Namen eines Kandidaten und seine Anzahl an Stimmen entgegennimmt und ein neues Wahlergebnis zurückgibt.

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

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

Das Struct für das Wahlergebnis ist bereits für dich erstellt und ist wie folgt definiert:

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

5. Verkünde die Ergebnisse

Es ist Zeit, der Schule den neuen Präsidenten bekanntzugeben! Der Präsident wird auf den kleinen digitalen Anzeigetafeln bekanntgegeben, die die Schule hat. Die Nachricht sollte den Namen des neuen Präsidenten und die Anzahl seiner Stimmen im folgenden Format anzeigen: <candidate_name> (<votes>). Ein Beispiel für eine solche Nachricht ist: "Peter (51)".

Erstelle eine Funktion DisplayResult, die ein *ElectionResult als Argument entgegennimmt und einen String mit der anzuzeigenden Nachricht zurückgibt.

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

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

6. Neuauszählung der Stimmen

Um sicherzustellen, dass die endgültigen Ergebnisse korrekt waren, wurden die Stimmen neu ausgezählt. Bei der Neuauszählung stellte sich heraus, dass die Anzahl der Stimmen für einige Kandidaten um eins abwich.

Erstelle eine Funktion DecrementVotesOfCandidate, die die endgültigen Ergebnisse und den Namen eines Kandidaten entgegennimmt, für den du seine Stimmenzahl verringern sollst. Die endgültigen Ergebnisse werden in Form einer map[string]int angegeben, wobei die Schlüssel die Namen der Kandidaten und die Werte ihre Gesamtstimmen sind.

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

DecrementVotesOfCandidate(finalResults, "Mary")

finalResults["Mary"]
// => 9
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
Go Exercism

Bereit, mit Wahltag zu starten?

Melde dich bei Exercism an, um Go mit 34 Konzepte165 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.