O typowanych nilach

W tym wpisie chciałbym naświetlić jak w Go wygląda sprawa z pustym wskaźnikiem i jakie to ma konsekwencje oraz jak można to naprawić przy użyciu generyków.

Zacznijmy od podstaw

W Go wszystko ma typ oraz wartość. Brzmi to rozsądnie bo mamy silnie typowany język więc wszechobecne typy nie powinny nas dziwić. Zgodnie ze specyfikacją języka każda zadeklarowana zmienna musi mieć typ. Czy to jawnie określony przez var <nazwa zmiennej> <typ> czy też wyliczony podczas kompilacji na podstawie przypisania <nazwa zmiennej> := <wartość. Dlatego też nie zadziała nam deklaracja postaci x := nil gdyż w tym przypadku nie wiadomo jakiego typu jest x.

Problem

Załóżmy że chcemy w funkcji sprawdzać czy argument nie jest nilem. Wydaje się, że najlepszą metodą będzie zwykłe porównanie coś == nil. I oczywiście w prostych przypadkach to działa. Spójrzmy na poniższy przykład, który jest uproszczoną wersją tego pull requesta. Mamy tam funkcję isNil która dla argumentu i który jest typu I wykonuje dokładnie to sprawdzenie. I teraz mamy sytuację która jest kontr intuicyjna, okazuje się że nil nilowi nie równy. Dlaczego? Bo nie zgadza się typ obiektu. Z jednej strony będzie implementacja a z drugiej interfejs o wartości nil a to spowoduje, że zmienne nie są równe.

Mamy najprostszy (bo pusty) interfejs oraz jego implementację 

type I interface{}
type impl struct{}

W funkcji która akceptuje I chcemy sprawdzić czy argument nie jest nil i wygląda ona tak

func isNil(i I) bool {
    return i == nil
}
fmt.Println("isNil(a)", isNil(a)) // isNil(a) false
fmt.Println("a == nil", a == nil) // a == nil false
fmt.Println("isNil(b)", isNil(b)) // isNil(b) true
fmt.Println("b == nil", b == nil) // b == nil true
fmt.Println("isNil(c)", isNil(c)) // isNil(c) false !!!
fmt.Println("c == nil", c == nil) // c == nil true

https://goplay.tools/snippet/1El6FCM3mZP

Rozwiązanie

Rozwiązaniem tego problemu może być zastosowanie refleksji i sprawdzenie czy wartość jest nil

func isNilRef(i I) bool {
    return i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil())
}

Clear is better than clever.
Reflection is never clear.

Rob Pike

Innym podejściem jest użycie generyków. Wtedy funkcja wygląda tak

func isNil[T any, PT *T](i PT) bool {
    var empty PT
    return i == empty
}

Jednak ma ona jeden mankament nie potrafi sobie poradzić z pustym interfejsem bez implementacji czyli var i I czy alternatywnie i := I(nil). Na szczęście dowiemy się o tym podczas kompilacji.

https://classicprogrammerpaintings.com/post/144854447139/go-programmer-claims-he-doesnt-need-generics

Tagged

Leave a Reply