golang: Memahami perbezaan antara penunjuk nil dan antara muka nil

WBOY
Lepaskan: 2024-07-18 08:51:19
asal
633 orang telah melayarinya

golang: Understanding the difference between nil pointers and nil interfaces

Saya sedang memikirkan sedikit tentang cara yang berbeza di mana nil berfungsi, dan kadangkala, sesuatu boleh menjadi nil dan bukan nil pada masa yang sama.

Berikut ialah contoh kecil sesuatu yang boleh menjadi penunjuk sifar, tetapi bukan antara muka sifar. Mari kita lihat maksudnya.

Antara muka

Pertama, go mempunyai konsep antara muka, yang serupa, tetapi tidak sama dengan antara muka dalam sesetengah bahasa berorientasikan objek (go bukan OOP mengikut kebanyakan takrifan). Dalam go, antara muka ialah jenis yang mentakrifkan fungsi yang mesti dilaksanakan oleh jenis lain untuk memenuhi antara muka. Ini membolehkan kami mempunyai berbilang jenis konkrit yang boleh memenuhi antara muka dengan cara yang berbeza.

Sebagai contoh, ralat ialah antara muka terbina dalam yang mempunyai satu kaedah. Ia kelihatan seperti ini:

type error interface {
    Error() string
}
Salin selepas log masuk

Sebarang jenis yang ingin digunakan sebagai ralat mesti mempunyai kaedah yang dipanggil Ralat yang mengembalikan rentetan. Sebagai contoh, kod berikut boleh digunakan:

type ErrorMessage string

func (em ErrorMessage) Error() string {
    return string(em)
}

func DoSomething() error {
    // Try to do something, and it fails.
    if somethingFailed {
        var err ErrorMessage = "This failed"
        return err
    }
    return nil
}

func main() {
    err := DoSomething()
    if err != nil {
        panic(err)
    }
}
Salin selepas log masuk

Perhatikan dalam contoh ini bahawa DoSomething mengembalikan ralat jika berlaku kesilapan. Kami boleh menggunakan jenis ErrorMessage kami, kerana ia mempunyai fungsi Ralat, yang mengembalikan rentetan, dan oleh itu melaksanakan antara muka ralat.
Jika tiada ralat berlaku, kami mengembalikan sifar.

penunjuk

Dalam pergi, penunjuk menunjuk kepada nilai, tetapi ia juga boleh menunjuk kepada tiada nilai, dalam hal ini penunjuk adalah sifar. Contohnya:

var i *int = nil

func main() {
    if i == nil {
        j := 5
        i = &j
    }
    fmt.Println("i is", *i)
}
Salin selepas log masuk

Dalam kes ini, pembolehubah i ialah penunjuk kepada int. Ia bermula sebagai penunjuk nol, sehingga kita mencipta int, dan menunjukkannya.

Penunjuk dan antara muka

Memandangkan jenis yang ditentukan pengguna boleh mempunyai fungsi (kaedah) yang dilampirkan, kami juga boleh mempunyai fungsi untuk penunjuk kepada jenis. Ini adalah amalan yang sangat biasa dalam perjalanan. Ini juga bermakna penunjuk juga boleh melaksanakan antara muka. Dengan cara ini, kita boleh mempunyai nilai yang merupakan antara muka bukan nol, tetapi masih penunjuk nol. Pertimbangkan kod berikut:

type TruthGetter interface {
    IsTrue() bool
}

func PrintIfTrue(tg TruthGetter) {
    if tg == nil {
        fmt.Println("I can't tell if it's true")
        return
    }
    if tg.IsTrue() {
        fmt.Println("It's true")
    } else {
        fmt.Println("It's not true")
    }
}
Salin selepas log masuk

Mana-mana jenis yang mempunyai kaedah bool IsTrue() boleh dihantar ke PrintIfTrue, tetapi begitu juga nil. Jadi, kita boleh melakukan PrintIfTrue(nil) dan ia akan mencetak "Saya tidak tahu sama ada ia benar".

Kita juga boleh melakukan sesuatu yang mudah seperti ini:

type Truthy bool

func (ty Truthy) IsTrue() bool {
    return bool(ty)
}

func main() {
    var ty Truthy = true
    PrintIfTrue(ty)
}
Salin selepas log masuk

Ini akan mencetak "Memang benar".

Atau, kita boleh melakukan sesuatu yang lebih rumit, seperti:

type TruthyNumber int

func (tn TruthyNumber) IsTrue() bool {
    return tn > 0
}

func main() {
    var tn TruthyNumber = -4
    PrintIfTrue(tn)
}
Salin selepas log masuk

Itu akan mencetak "Ia tidak benar". Kedua-dua contoh ini bukan petunjuk, jadi tiada peluang untuk nol dengan salah satu daripada jenis ini, tetapi pertimbangkan ini:

type TruthyPerson struct {
    FirstName string
    LastName string
}

func (tp *TruthyPerson) IsTrue() bool {
    return tp.FirstName != "" && tp.LastName != ""
}
Salin selepas log masuk

Dalam kes ini TruthyPerson tidak melaksanakan TruthGetter, tetapi *TruthyPerson melakukannya. Jadi, ini sepatutnya berfungsi:

func main() {
    tp := &TruthyPerson{"Jon", "Grady"}
    PrintIfTrue(tp)
}
Salin selepas log masuk

Ini berfungsi kerana tp adalah penunjuk kepada TruthyPerson. Walau bagaimanapun, jika penunjuknya tiada, kita akan panik.

func main() {
    var tp *TruthyPerson
    PrintIfTrue(tp)
}
Salin selepas log masuk
Salin selepas log masuk

Ini akan panik. Walau bagaimanapun, panik tidak berlaku dalam PrintIfTrue. Anda akan fikir ia tidak mengapa, kerana PrintIfTrue menyemak sifar. Tetapi, inilah isunya. Ia menyemak nol terhadap TruthGetter. Dalam erti kata lain, ia menyemak antara muka sifar, tetapi bukan penunjuk sifar. Dan dalam func (tp *TruthyPerson) IsTrue() bool, kami tidak menyemak nol. Dalam perjalanan, kita masih boleh memanggil kaedah pada penunjuk sifar, jadi panik berlaku di sana. Pembetulan sebenarnya agak mudah.

func (tp *TruthyPerson) IsTrue() bool {
    if tp == nil {
        return false
    }
    return tp.FirstName != "" && tp.LastName != ""
}
Salin selepas log masuk

Sekarang, kami sedang menyemak antara muka nil dalam PrintIfTrue dan untuk penunjuk nil dalam func (tp *TruthyPerson) IsTrue() bool. Dan ia kini akan mencetak "Ia tidak benar". Kita boleh melihat semua kod ini berfungsi di sini.

Bonus: Semak kedua-dua nol sekaligus dengan refleksi

Dengan refleksi, kami boleh membuat sedikit perubahan kepada PrintIfTrue supaya ia boleh menyemak kedua-dua antara muka nil dan penunjuk nil. Ini kodnya:

func PrintIfTrue(tg TruthGetter) {
    if tg == nil {
        fmt.Println("I can't tell if it's true")
        return
    }

    val := reflect.ValueOf(tg)
    k := val.Kind()
    if (k == reflect.Pointer || k == reflect.Chan || k == reflect.Func || k == reflect.Map || k == reflect.Slice) && val.IsNil() {
        fmt.Println("I can't tell if it's true")
        return
    }

    if tg.IsTrue() {
        fmt.Println("It's true")
    } else {
        fmt.Println("It's not true")
    }
}
Salin selepas log masuk

Di sini, kami menyemak antara muka sifar dahulu, seperti sebelum ini. Seterusnya, kami menggunakan refleksi untuk mendapatkan jenis. chan, func, map dan slice juga boleh menjadi nil, sebagai tambahan kepada penunjuk, jadi kami menyemak sama ada nilai itu adalah salah satu daripada jenis tersebut, dan jika ya, semak sama ada ia adalah nil. Dan jika ya, kami juga mengembalikan mesej "Saya tidak tahu sama ada ia benar". Ini mungkin atau mungkin bukan apa yang anda mahukan, tetapi ini adalah pilihan. Dengan perubahan ini, kita boleh melakukan ini:

func main() {
    var tp *TruthyPerson
    PrintIfTrue(tp)
}
Salin selepas log masuk
Salin selepas log masuk

Kadangkala anda mungkin melihat cadangan kepada sesuatu yang lebih mudah, seperti:

// Don't do this
if tg == nil && reflect.ValueOf(tg).IsNil() {
    fmt.Println("I can't tell if it's true")
    return
}
Salin selepas log masuk

Terdapat dua sebab ini tidak berfungsi dengan baik. Pertama, ialah terdapat overhed prestasi apabila menggunakan refleksi. Jika anda boleh mengelak daripada menggunakan refleksi, anda mungkin perlu. Jika kita menyemak antara muka nil dahulu, kita tidak perlu menggunakan pantulan jika ia adalah antara muka nil.

Sebab kedua ialah reflect.Value.IsNil() akan panik jika jenis nilai itu bukan jenis yang boleh menjadi nol. Itulah sebabnya kami menambah cek untuk jenis itu. Jika kami tidak menyemak Jenis, maka kami akan menjadi panik pada jenis Truthy dan TruthyNumber.

Jadi, selagi kami memastikan kami menyemak jenisnya dahulu, ini kini akan mencetak "Saya tidak dapat memberitahu sama ada ia benar", bukannya "Ia tidak benar". Bergantung pada perspektif anda, ini mungkin penambahbaikan. Berikut ialah kod lengkap dengan perubahan ini.

Ini pada asalnya diterbitkan di Dan's Musings

Atas ialah kandungan terperinci golang: Memahami perbezaan antara penunjuk nil dan antara muka nil. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!