Sebelum pengenalan generik, terdapat beberapa pendekatan untuk melaksanakan fungsi generik yang menyokong jenis data yang berbeza:
Pendekatan 1: Laksanakan fungsi untuk setiap jenis data
Pendekatan ini membawa kepada kod yang sangat berlebihan dan kos penyelenggaraan yang tinggi. Sebarang pengubahsuaian memerlukan operasi yang sama dilakukan pada semua fungsi. Selain itu, memandangkan bahasa Go tidak menyokong lebihan fungsi dengan nama yang sama, ia juga menyusahkan untuk mendedahkan fungsi ini untuk panggilan modul luaran.
Pendekatan 2: Gunakan jenis data dengan julat terbesar
Untuk mengelakkan lebihan kod, kaedah lain ialah menggunakan jenis data dengan julat terbesar, iaitu, Pendekatan 2. Contoh biasa ialah matematik.Max, yang mengembalikan dua nombor yang lebih besar. Untuk dapat membandingkan data pelbagai jenis data, math.Max menggunakan float64, jenis data dengan julat terbesar antara jenis angka dalam Go, sebagai parameter input dan output, sekali gus mengelakkan kehilangan ketepatan. Walaupun ini menyelesaikan masalah redundansi kod sedikit sebanyak, sebarang jenis data perlu ditukar kepada jenis float64 terlebih dahulu. Contohnya, apabila membandingkan int dengan int, pemutus jenis masih diperlukan, yang bukan sahaja merendahkan prestasi tetapi juga kelihatan tidak wajar.
Pendekatan 3: Gunakan antara muka{} jenis
Menggunakan jenis antara muka{} menyelesaikan masalah di atas dengan berkesan. Walau bagaimanapun, jenis antara muka{} memperkenalkan overhed masa jalan tertentu kerana ia memerlukan penegasan jenis atau pertimbangan jenis semasa masa jalan, yang mungkin membawa kepada kemerosotan prestasi. Selain itu, apabila menggunakan jenis antara muka{}, pengkompil tidak boleh melakukan semakan jenis statik, jadi sesetengah ralat jenis hanya boleh ditemui semasa masa jalan.
Go 1.18 memperkenalkan sokongan untuk generik, yang merupakan perubahan ketara sejak sumber terbuka bahasa Go.
Generik ialah ciri bahasa pengaturcaraan. Ia membolehkan pengaturcara menggunakan jenis generik dan bukannya jenis sebenar dalam pengaturcaraan. Kemudian, melalui hantaran eksplisit atau potongan automatik semasa panggilan sebenar, jenis generik diganti, mencapai tujuan penggunaan semula kod. Dalam proses menggunakan generik, jenis data yang akan dikendalikan ditentukan sebagai parameter. Jenis parameter sedemikian dipanggil kelas generik, antara muka generik dan kaedah generik dalam kelas, antara muka dan kaedah masing-masing.
Kelebihan utama generik ialah meningkatkan kebolehgunaan semula kod dan keselamatan jenis. Berbanding dengan parameter formal tradisional, generik menjadikan penulisan kod universal lebih ringkas dan fleksibel, memberikan keupayaan untuk mengendalikan pelbagai jenis data dan meningkatkan lagi ekspresif dan kebolehgunaan semula bahasa Go. Pada masa yang sama, memandangkan jenis generik khusus ditentukan pada masa penyusunan, semakan jenis boleh disediakan, mengelakkan ralat penukaran jenis.
Dalam bahasa Go, kedua-dua antara muka{} dan generik ialah alat untuk mengendalikan berbilang jenis data. Untuk membincangkan perbezaan mereka, mari kita lihat pada prinsip pelaksanaan antara muka{} dan generik dahulu.
antara muka{} ialah antara muka kosong tanpa kaedah dalam jenis antara muka. Memandangkan semua jenis melaksanakan antara muka{}, ia boleh digunakan untuk mencipta fungsi, kaedah atau struktur data yang boleh menerima sebarang jenis. Struktur asas antara muka{} pada masa jalan diwakili sebagai eface, yang strukturnya ditunjukkan di bawah, terutamanya mengandungi dua medan, _type dan data.
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
_type ialah penunjuk kepada struktur _type, yang mengandungi maklumat seperti saiz, jenis, fungsi cincang dan perwakilan rentetan bagi nilai sebenar. data adalah penunjuk kepada data sebenar. Jika saiz data sebenar kurang daripada atau sama dengan saiz penunjuk, data akan disimpan terus dalam medan data; jika tidak, medan data akan menyimpan penunjuk kepada data sebenar.
Apabila objek jenis tertentu diperuntukkan kepada pembolehubah jenis antara muka{}, bahasa Go secara tersirat melaksanakan operasi tinju eface, menetapkan medan _type kepada jenis nilai dan medan data kepada data nilai . Contohnya, apabila antara muka penyataan var i{} = 123 dilaksanakan, Go akan mencipta struktur eface, dengan medan _type mewakili jenis int dan medan data mewakili nilai 123.
Apabila mendapatkan semula nilai yang disimpan daripada antara muka{}, proses penyahkotak berlaku, iaitu, taip penegasan atau pertimbangan jenis. Proses ini memerlukan secara eksplisit menyatakan jenis yang dijangkakan. Jika jenis nilai yang disimpan dalam antara muka{} sepadan dengan jenis yang dijangkakan, penegasan jenis akan berjaya dan nilai boleh diambil semula. Jika tidak, penegasan jenis akan gagal dan pengendalian tambahan diperlukan untuk situasi ini.
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
Ia boleh dilihat bahawa antara muka{} menyokong operasi pada berbilang jenis data melalui operasi tinju dan nyah kotak pada masa jalan.
Pasukan teras Go sangat berhati-hati semasa menilai skim pelaksanaan Go generik. Sebanyak tiga skim pelaksanaan telah dikemukakan:
Skim Stensil juga merupakan skim pelaksanaan yang diterima pakai oleh bahasa seperti C dan Rust untuk melaksanakan generik. Prinsip pelaksanaannya ialah semasa tempoh penyusunan, mengikut parameter jenis tertentu apabila fungsi generik dipanggil atau elemen jenis dalam kekangan, pelaksanaan berasingan fungsi generik dijana untuk setiap hujah jenis untuk memastikan keselamatan jenis dan prestasi optimum . Walau bagaimanapun, kaedah ini akan melambatkan pengkompil. Kerana apabila terdapat banyak jenis data yang dipanggil, fungsi generik perlu menjana fungsi bebas untuk setiap jenis data, yang mungkin menghasilkan fail tersusun yang sangat besar. Pada masa yang sama, disebabkan isu seperti kesilapan cache CPU dan ramalan cawangan arahan, kod yang dijana mungkin tidak berjalan dengan cekap.
Skim Kamus hanya menjana satu logik fungsi untuk fungsi generik tetapi menambah dict parameter sebagai parameter pertama pada fungsi. Parameter dict menyimpan maklumat berkaitan jenis bagi argumen jenis apabila fungsi generik dipanggil dan menghantar maklumat kamus menggunakan daftar AX (AMD) semasa panggilan fungsi. Kelebihan skim ini ialah ia mengurangkan overhed fasa kompilasi dan tidak meningkatkan saiz fail binari. Walau bagaimanapun, ia meningkatkan overhed masa jalan, tidak dapat melaksanakan pengoptimuman fungsi pada peringkat penyusunan dan mempunyai masalah seperti rekursi kamus.
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
Go akhirnya menyepadukan dua skim di atas dan mencadangkan skim Stensil Bentuk GC untuk pelaksanaan generik. Ia menjana kod fungsi dalam unit Bentuk GC sesuatu jenis. Jenis dengan Bentuk GC yang sama menggunakan semula kod yang sama (Bentuk GC bagi sesuatu jenis merujuk kepada perwakilannya dalam pengagih memori Go/pengumpul sampah). Semua jenis penunjuk menggunakan semula jenis *uint8. Untuk jenis dengan Bentuk GC yang sama, kod fungsi instantiated dikongsi digunakan. Skim ini juga secara automatik menambahkan parameter dict pada setiap kod fungsi yang di instantiated untuk membezakan jenis yang berbeza dengan Bentuk GC yang sama.
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
Daripada prinsip pelaksanaan asas antara muka{} dan generik, kita boleh mendapati bahawa perbezaan utama antara keduanya ialah antara muka{} menyokong pengendalian jenis data yang berbeza semasa masa jalan, manakala generik menyokong pengendalian jenis data yang berbeza secara statik pada peringkat penyusunan. Terdapat terutamanya perbezaan berikut dalam penggunaan praktikal:
(1) Perbezaan prestasi: Operasi tinju dan nyahbox yang dilakukan apabila jenis data yang berbeza ditetapkan atau diambil daripada antara muka{} adalah mahal dan memperkenalkan overhed tambahan. Sebaliknya, generik tidak memerlukan operasi tinju dan nyahbox, dan kod yang dijana oleh generik dioptimumkan untuk jenis tertentu, mengelakkan overhed prestasi masa jalan.
(2) Keselamatan jenis: Apabila menggunakan jenis antara muka{}, pengkompil tidak boleh melakukan semakan jenis statik dan hanya boleh melakukan penegasan jenis pada masa jalan. Oleh itu, beberapa jenis ralat hanya boleh ditemui pada masa jalan. Sebaliknya, kod generik Go dijana pada masa penyusunan, jadi kod generik boleh mendapatkan maklumat jenis pada masa penyusunan, memastikan keselamatan jenis.
Dalam bahasa Go, parameter jenis tidak dibenarkan dibandingkan secara langsung dengan sifar kerana parameter jenis disemak jenis pada masa penyusunan, manakala sifar ialah nilai khas pada masa jalan. Oleh kerana jenis asas parameter jenis tidak diketahui pada masa penyusunan, pengkompil tidak dapat menentukan sama ada jenis asas parameter jenis menyokong perbandingan dengan nol. Oleh itu, untuk mengekalkan keselamatan jenis dan mengelakkan kemungkinan ralat masa jalan, bahasa Go tidak membenarkan perbandingan langsung antara parameter jenis dan sifar.
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
Jenis T elemen asas mestilah jenis asas dan tidak boleh menjadi jenis antara muka.
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
Elemen jenis kesatuan tidak boleh menjadi parameter jenis dan elemen bukan antara muka mesti dipisahkan secara berpasangan. Jika terdapat lebih daripada satu elemen, ia tidak boleh mengandungi jenis antara muka dengan kaedah tidak kosong, dan ia juga tidak boleh dibandingkan atau membenamkan setanding.
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
type Op interface{ int|float } func Add[T Op](m, n T) T { return m + n } // After generation => const dict = map[type] typeInfo{ int : intInfo{ newFunc, lessFucn, //...... }, float : floatInfo } func Add(dict[T], m, n T) T{}
Untuk menggunakan generik dengan baik, perkara berikut harus diperhatikan semasa penggunaan:
type V interface{ int|float|*int|*float } func F[T V](m, n T) {} // 1. Generate templates for regular types int/float func F[go.shape.int_0](m, n int){} func F[go.shape.float_0](m, n int){} // 2. Pointer types reuse the same template func F[go.shape.*uint8_0](m, n int){} // 3. Add dictionary passing during the call const dict = map[type] typeInfo{ int : intInfo{}, float : floatInfo{} } func F[go.shape.int_0](dict[int],m, n int){}
Kod di atas akan melaporkan ralat: operasi tidak sah: penunjuk ptr (pembolehubah jenis T dikekang oleh *int | *uint) mesti mempunyai jenis asas yang sama. Sebab ralat ini ialah T ialah parameter jenis, dan parameter jenis bukan penunjuk dan tidak menyokong operasi penyahrujukan. Ini boleh diselesaikan dengan menukar definisi kepada yang berikut:
// Wrong example func ZeroValue0[T any](v T) bool { return v == nil } // Correct example 1 func Zero1[T any]() T { return *new(T) } // Correct example 2 func Zero2[T any]() T { var t T return t } // Correct example 3 func Zero3[T any]() (t T) { return }
Secara keseluruhan, faedah generik boleh diringkaskan dalam tiga aspek:
Akhir sekali, izinkan saya memperkenalkan Leapcell, platform yang paling sesuai untuk menggunakan perkhidmatan Go.
Teroka lebih lanjut dalam dokumentasi!
Twitter Leapcell: https://x.com/LeapcellHQ
Atas ialah kandungan terperinci Go Generics: A Deep Dive. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!