Zasady spełniania interfejsów
Część sekcji Programowanie obiektowe ścieżki GO w Coddy. Lekcja 43 z 107.
Chociaż Go używa typowania kaczego do niejawnego spełniania interfejsów, istnieją konkretne zasady określające, czy dany typ rzeczywiście spełnia interfejs. Zrozumienie tych zasad pomaga uniknąć subtelnych błędów.
Najważniejsza zasada dotyczy odbiorców wskaźnikowych. Jeśli metoda jest zdefiniowana z odbiorcą wskaźnikowym, interfejs spełnia tylko wskaźnik do tego typu, a nie sama wartość:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // odbiornik wskaźnikowy
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Ponieważ Save() ma odbiorcę wskaźnikowego, tylko *Document spełnia interfejs Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // działa - wskaźnik spełnia wymagania interfejsu
// Process(doc) // błąd kompilacji - wartość nie spełnia wymagań interfejsu
}
Jednak sytuacja odwrotna jest bardziej elastyczna. Jeśli metoda ma odbiornik wartości, interfejs spełniają zarówno typ wartości, jak i typ wskaźnika. Go automatycznie dereferencjonuje wskaźniki podczas wywoływania metod z odbiornikiem wartości:
func (d Document) Info() string { // odbiornik przez wartość
return d.Name
}
// Zarówno Document, jak i *Document spełniają wymagania interfejsu wymagającego Info()
Ta asymetria istnieje, ponieważ Go zawsze może uzyskać wartość ze wskaźnika (przez dereferencję), ale nie zawsze może uzyskać wskaźnik z wartości (wartość może nie mieć adresu). Pamiętanie o tej zasadzie zapobiega mylącym błędom kompilatora podczas pracy z interfejsami.
Wyzwanie
ŁatwyZbudujmy system konfiguracji, który pokaże, jak odbiorniki wskaźnikowe i wartościowe wpływają na spełnianie interfejsu. Utworzysz typy, w przypadku których wybór odbiornika określa, czy z interfejsem można używać wartości, wskaźników, czy obu tych rodzajów.
Rozmieścisz kod w trzech plikach:
config.go: Zdefiniuj interfejsConfigurable, który wymaga dwóch metod:GetValue() stringiSetValue(string). Następnie utwórz dwa typy konfiguracji:ReadOnlyConfigz polemValue: użyj odbiornika wartościowego dlaGetValue()(zwraca Value) oraz odbiornika wskaźnikowego dlaSetValue()(aktualizuje Value)Settingz polemData: użyj odbiorników wartościowych dla obu metod (GetValuezwraca Data, aSetValuetylko wyświetla "Cannot modify", nie wprowadzając żadnych zmian)
processor.go: Utwórz funkcję o nazwieProcessConfig, która przyjmujeConfigurablei nową wartość typu string. Powinna wyświetlić bieżącą wartość za pomocąGetValue(), wywołaćSetValue()z nową wartością, a następnie ponownie wyświetlić wartość, aby pokazać ewentualne zmiany.main.go: Odczytaj szczegóły konfiguracji z wejścia i pokaż zasady spełniania interfejsu:- Utwórz
ReadOnlyConfigi przekaż doProcessConfigwskaźnik (jest to wymagane, ponieważSetValuema odbiornik wskaźnikowy) - Utwórz
Settingi przekaż doProcessConfigbezpośrednio wartość (działa, ponieważ obie metody mają odbiorniki wartościowe)
- Utwórz
Podane zostaną następujące dane wejściowe:
- Wiersz 1: Wartość początkowa dla ReadOnlyConfig
- Wiersz 2: Nowa wartość do ustawienia dla ReadOnlyConfig
- Wiersz 3: Wartość początkowa dla Setting
- Wiersz 4: Nowa wartość, którą należy spróbować ustawić dla Setting
Funkcja ProcessConfig powinna wyświetlać dane w następującym formacie:
Current: [value]
Current: [value after SetValue]Na przykład dla danych debug, production, localhost i remote wynik powinien wyglądać następująco:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostZwróć uwagę, że wartość ReadOnlyConfig rzeczywiście się zmienia (ponieważ przekazaliśmy wskaźnik), natomiast Setting pozostaje bez zmian (jego SetValue z odbiornikiem wartościowym nie może zmodyfikować oryginału). Najważniejszy wniosek jest taki, że same wartości ReadOnlyConfig nie spełniają wymagań interfejsu Configurable — spełniają je tylko wskaźniki, podczas gdy wartości Setting można przekazywać bezpośrednio, ponieważ wszystkie jego metody używają odbiorników wartościowych.
Spróbuj swoich sił
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Odczytaj dane wejściowe
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: Utwórz ReadOnlyConfig z wartością rocInitial
// TODO: Przekaż WSKAŹNIK do ProcessConfig (jest to wymagane, ponieważ SetValue ma odbiornik wskaźnikowy)
// TODO: Utwórz Setting z wartością settingInitial
// TODO: Przekaż WARTOŚĆ bezpośrednio do ProcessConfig (to działa, ponieważ obie metody mają odbiorniki wartościowe)
// Użyj tych zmiennych, aby uniknąć błędów związanych z nieużywanymi zmiennymi
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy OOP w Go
Pliki zewnętrzneObszar roboczy i moduły GoPakiety i importyNazwy eksportowane i nieeksportowaneWprowadzenie do OOP w GoStruktury jako klasyDefiniowanie metod dla strukturOdbiorniki wskaźnikowe i wartościoweInicjalizacja strukturFunkcje konstruktoraPodsumowanie – prosty kalkulator4Interfejsy
Wprowadzenie do interfejsówImplementacja niejawnaInterfejs jako kontraktPusty interfejs (any)Asercja typuPrzełącznik typówKompozycja interfejsówInterfejsy Stringer i ErrorPowtórka — kalkulator figur7Hermetyzacja
Pola eksportowane i nieeksportowaneHermetyzacja na poziomie pakietuMetody pobierające i ustawiająceUkrywanie informacji w GoPowtórka – dane studentów10Typy generyczne (Go 1.18+)
Wprowadzenie do typów generycznychParametry typówOgraniczenia typówStruktury generyczneObejście ograniczeń metod generycznychPodsumowanie — generyczna kolekcja2Typy i struktury w praktyce
Typy proste i złożoneDefiniowanie własnych typówTagi pól strukturStruktury anonimoweStruktury zagnieżdżoneWartości zerowe i domyślnePodsumowanie – książka kontaktów5Kompozycja zamiast dziedziczenia
Dlaczego Go nie ma dziedziczeniaPodstawy osadzania strukturPromowanie metodOsadzanie wielu strukturOsadzanie a agregacjaPrzesłanianie osadzonych metodPodsumowanie — hierarchia pracowników8Obsługa błędów i programowanie obiektowe
Interfejs błęduWłasne typy błędówOpakowywanie błędów (fmt.Errorf)Błędy wartowniczeerrors.Is() i errors.As()Panic, Defer i RecoverPodsumowanie — parser plików3Wskaźniki i pamięć
Podstawy wskaźników w GoWskaźniki do strukturPrzekazywanie przez wartość a przez referencjęFunkcja new()Odśmiecanie pamięci w GoPowtórka — kreator listy wiązanej6Polimorfizm w Go
Polimorfizm za pomocą interfejsówDuck typing w GoZasady spełniania interfejsówKolekcje polimorficzneWstrzykiwanie zależnościPowtórka — procesor płatności9Współbieżność i OOP
Podstawy goroutinesKanały i komunikacjaKanały buforowane i niebuforowaneInstrukcja selectsync.Mutex i sync.RWMutexsync.WaitGroupProjektowanie struktur bezpiecznych wątkowoPowtórka – pula workerów12Zaawansowane zagadnienia programowania obiektowego
Wzorzec opcji funkcyjnychWzorzec Builder w GoŁańcuchowe wywoływanie metodAliasy typów a definicje typówPodstawy refleksjiPrzegląd generowania koduPoćwicz samodzielnie: Kompilator Go online