Generics
Generics หรือชื่อทางวิชาการว่า Parameterized Polymorphism หมายถึงการใช้ประเภทพารามิเตอร์เพื่อให้เกิดการใช้โค้ดซ้ำและความยืดหยุ่น ในภาษาโปรแกรมหลายภาษา Parameterized Polymorphism เป็นแนวคิดที่สำคัญ ทำให้ฟังก์ชันหรือโครงสร้างข้อมูลสามารถจัดการข้อมูลประเภทต่างๆ ได้ โดยไม่ต้องเขียนโค้ดแยกสำหรับแต่ละประเภท Go เดิมทีไม่มี generics แต่ตั้งแต่เกิดมา เสียงเรียกร้องจากชุมชนเกี่ยวกับ Go ที่สูงที่สุดคือหวังให้เพิ่ม generics ในที่สุด Go ก็เพิ่มการสนับสนุน generics ในเวอร์ชัน 1.18 ในปี 2022
การออกแบบ
Go เมื่อออกแบบ generics ได้พิจารณาแผนการต่อไปนี้
- stenciling: monomorphization แบบคลาสสิกเช่น C++, Rust คือสร้างโค้ดเทมเพลตหนึ่งชุดสำหรับแต่ละประเภทที่ใช้ ประสิทธิภาพแบบนี้ดีที่สุด ไม่มีค่าใช้จ่ายขณะรันไทม์เลย ประสิทธิภาพเท่ากับการเรียกโดยตรง ข้อเสียคือทำให้ความเร็วการคอมไพล์ช้าลงอย่างมาก (เทียบกับ Go เอง) และเนื่องจากสร้างโค้ดสำหรับแต่ละประเภท จึงทำให้ขนาดไฟล์ไบนารีที่คอมไพล์แล้วบวมขึ้น
- dictionaries: จะสร้างโค้ดเพียงชุดเดียว และสร้าง dictionary ประเภทหนึ่งเก็บในส่วนข้อมูลอ่านอย่างเดียวขณะคอมไพล์ เก็บข้อมูลประเภททั้งหมดที่จะใช้ เมื่อเรียกฟังก์ชัน จะค้นหาข้อมูลประเภทตาม dictionary วิธีนี้ไม่ทำให้ความเร็วการคอมไพล์ช้าลง และไม่ทำให้ขนาดบวม แต่จะสร้างค่าใช้จ่ายขณะรันไทม์มหาศาล ประสิทธิภาพของ generics แย่มาก
สองวิธีข้างต้นแทนสองขั้วสุด Go เลือกแผนการนำไปใช้สุดท้ายคือ Gcshape stenciling เป็นทางเลือกประนีประนอม สำหรับประเภทที่มีรูปร่างหน่วยความจำเดียวกัน (รูปร่างดูโดยตัวจัดสรรหน่วยความจำตัดสินใจ) จะใช้ monomorphization สร้างโค้ดชุดเดียวกัน เช่น type Int int กับ int จริงๆ แล้วเป็นประเภทเดียวกัน จึงใช้โค้ดชุดเดียวกันได้ แต่สำหรับพอยน์เตอร์แล้ว แม้ประเภทพอยน์เตอร์ทั้งหมดจะมีรูปร่างหน่วยความจำเดียวกัน เช่น *int, *Person ล้วนเป็นรูปร่างหน่วยความจำเดียวกัน แต่ไม่สามารถใช้โค้ดชุดเดียวกันได้ เพราะเมื่อ dereference แล้วประเภทเป้าหมายที่ดำเนินการมีเลย์เอาต์หน่วยความจำต่างกันทั้งหมด ดังนั้น Go จึงใช้ dictionary รับข้อมูลประเภทขณะรันไทม์ด้วย ดังนั้น generics ของ Go จึงมีค่าใช้จ่ายขณะรันไทม์เช่นกัน
บทนำ
มาดูตัวอย่างง่ายๆ ก่อน
func Sum(a, b int) int {
return a + b
}นี่เป็นฟังก์ชันที่ง่ายมาก หน้าที่คือบวกจำนวนเต็มประเภท int สองตัวแล้วส่งคืนผลลัพธ์ หากต้องการส่งจำนวนทศนิยมประเภท float64 สองตัวเพื่อหาผลรวม แน่นอนว่าไม่ได้ เพราะประเภทไม่ตรงกัน วิธีแก้วิธีหนึ่งคือนิยามฟังก์ชันใหม่หนึ่งฟังก์ชัน ดังนี้
func SumFloat64(a, b float64) float64 {
return a + b
}ปัญหาคือ หากพัฒนาชุดเครื่องมือคณิตศาสตร์ คำนวณผลรวมของตัวเลขสองตัวทุกประเภท难道ต้องเขียนฟังก์ชันสำหรับทุกประเภทหรือ? แน่นอนว่าไม่น่าจะเป็นไปได้ หรือสามารถใช้ประเภท any บวกกับการสะท้อนเพื่อตัดสินได้ ดังนี้
func SumAny(a, b any) (any, error) {
tA, tB := reflect.ValueOf(a), reflect.ValueOf(b)
if tA.Kind() != tB.Kind() {
return nil, errors.New("disMatch type")
}
switch tA.Kind() {
case reflect.Int:
case reflect.Int32:
...
}
}แต่การเขียนแบบนี้ยุ่งยาก และประสิทธิภาพต่ำ แต่ตรรกะของฟังก์ชัน Sum เหมือนกันหมด เพียงแค่บวกตัวเลขสองตัวเท่านั้น ในเวลานี้ต้องใช้ generics ดังนั้นทำไมต้องการ generics generics เกิดมาเพื่อแก้ปัญหาที่การดำเนินการไม่เกี่ยวข้องกับประเภท ปัญหาประเภทนี้ไม่สนใจว่าประเภทที่ให้มาคืออะไร เพียงแค่ดำเนินการที่สอดคล้องกันก็พอ
ไวยากรณ์
การเขียน generics มีดังนี้
func Sum[T int | float64](a, b T) T {
return a + b
}ประเภทพารามิเตอร์: T คือประเภทพารามิเตอร์หนึ่งตัว ประเภทพารามิเตอร์เฉพาะเป็นประเภทอะไรขึ้นอยู่กับส่งเข้ามามาเป็นประเภทอะไร
ข้อจำกัดประเภท: int | float64 ประกอบเป็นข้อจำกัดประเภทหนึ่ง ข้อจำกัดประเภทนี้กำหนดว่าประเภทใดบ้างที่อนุญาต 限定ขอบเขตประเภทของประเภทพารามิเตอร์
ประเภทอาร์กิวเมนต์: Sum[int](1,2) ระบุประเภท int ด้วยตนเอง int คือประเภทอาร์กิวเมนต์
วิธีใช้第一种 ระบุอย่างชัดเจนว่าใช้ประเภทใด ดังนี้
Sum[int](2012, 2022)วิธีใช้第二种 ไม่ระบุประเภท ให้คอมไพเลอร์อนุมานเอง ดังนี้
Sum(3.1415926, 1.114514)นี่เป็นสไลซ์ generics ข้อจำกัดประเภทคือ int | int32 | int64
type GenericSlice[T int | int32 | int64] []Tที่นี่เมื่อใช้ไม่สามารถละเว้นประเภทอาร์กิวเมนต์ได้
GenericSlice[int]{1, 2, 3}นี่เป็นแฮชแมป generics คีย์ต้องเป็นประเภทที่เปรียบเทียบได้ จึงใช้อินเทอร์เฟซ comparable ข้อจำกัดประเภทของค่าคือ V int | string | byte
type GenericMap[K comparable, V int | string | byte] map[K]Vเมื่อใช้
gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)นี่เป็นสตรักต์ generics ข้อจำกัดประเภทคือ T int | string
type GenericStruct[T int | string] struct {
Name string
Id T
}เมื่อใช้
GenericStruct[int]{
Name: "jack",
Id: 1024,
}
GenericStruct[string]{
Name: "Mike",
Id: "1024",
}นี่เป็นตัวอย่างพารามิเตอร์สไลซ์ generics
type Company[T int | string, S []T] struct {
Name string
Id T
Stuff S
}
//หรือสามารถเขียนดังนี้
type Company[T int | string, S []int | []string] struct {
Name string
Id T
Stuff S
}เมื่อใช้
Company[int, []int]{
Name: "lili",
Id: 1,
Stuff: []int{1},
}TIP
ในสตรักต์ generics แนะนำให้เขียนแบบนี้มากกว่า
type Company[T int | string, S int | string] struct {
Name string
Id T
Stuff []S
}SayAble เป็นอินเทอร์เฟซ generics Person นำไปใช้อินเทอร์เฟซนี้
type SayAble[T int | string] interface {
Say() T
}
type Person[T int | string] struct {
msg T
}
func (p Person[T]) Say() T {
return p.msg
}
func main() {
var s SayAble[string]
s = Person[string]{"hello world"}
fmt.Println(s.Say())
}อินเทอร์เฟซ generics
อินเทอร์เฟซ generics สามารถให้ความสามารถในการจำกัดนามธรรมที่ดีขึ้น ด้านล่างนี้เป็นตัวอย่าง
func PrintObj[T fmt.Stringer](s T) {
fmt.Println(s.String())
}
type Person struct {
Name string
}
func (p Person) String() string {
return fmt.Sprintf("Person: %s", p.Name)
}
func main() {
PrintObj(Person{Name: "Alice"})
}ยังสามารถใช้อินเทอร์เฟซที่ไม่ใช่ generics เป็นประเภทพารามิเตอร์ของ generics ได้
func Write[W io.Writer](w W, bs []byte) (int, error) {
return w.Write(bs)
}type assertion แบบ generics
เราสามารถใช้ generics เพื่อทำ type assertion สำหรับประเภท any ได้ เช่นฟังก์ชันด้านล่างนี้สามารถทำ assertion สำหรับทุกประเภทได้
func Assert[T any](v any) (bool, T) {
var av T
if v == nil {
return false, av
}
av, ok := v.(T)
return ok, av
}Type Set
หลังจาก 1.18 นิยามของอินเทอร์เฟซเปลี่ยนเป็น type set อินเทอร์เฟซที่มี type set เรียกว่า General interfaces คืออินเทอร์เฟซทั่วไป
An interface type defines a type set
Type set ใช้ได้เฉพาะในข้อจำกัดประเภทของ generics เท่านั้น ไม่สามารถใช้สำหรับการประกาศประเภท การแปลงประเภท type assertion Type set เป็นเซตหนึ่ง จะมีเซตว่าง ยูเนียน อินเตอร์เซกชัน ต่อไปจะอธิบายสามสถานการณ์นี้
ยูเนียน
ประเภทอินเทอร์เฟซ SignedInt เป็น type set หนึ่ง ยูเนียนของประเภทจำนวนเต็มมีเครื่องหมายคือ SignedInt ในทางกลับกัน SignedInt คือซูเปอร์เซตของพวกมัน
type SignedInt interface {
int8 | int16 | int | int32 | int64
}ประเภทข้อมูลพื้นฐานเป็นเช่นนี้对待อินเทอร์เฟซทั่วไปอื่นๆ ก็เป็นเช่นนี้
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnSignedInt interface {
uint8 | uint16 | uint32 | uint64
}
type Integer interface {
SignedInt | UnSignedInt
}อินเตอร์เซกชัน
Type set ของอินเทอร์เฟซที่ไม่ใช่เซตว่างคืออินเตอร์เซกชันของ type set ขององค์ประกอบทั้งหมดของมัน แปลเป็นภาษามนุษย์คือ: หากอินเทอร์เฟซหนึ่งมี type set ที่ไม่ใช่เซตว่างหลายตัว แล้วอินเทอร์เฟซนี้คืออินเตอร์เซกชันของ type set เหล่านี้ ตัวอย่างดังนี้
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type Integer interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type Number interface {
SignedInt
Integer
}อินเตอร์เซกชันในตัวอย่างแน่นอนคือ SignedInt
func Do[T Number](n T) T {
return n
}
Do[int](2)
DO[uint](2) //ไม่สามารถผ่านการคอมไพล์เซตว่าง
เซตว่างคือไม่มีอินเตอร์เซกชัน ตัวอย่าง如下 ด้านล่างนี้ Integer ในตัวอย่างคือ type set เซตว่าง
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnsignedInt interface {
uint8 | uint16 | uint | uint32 | uint64
}
type Integer interface {
SignedInt
UnsignedInt
}เพราะจำนวนเต็มไม่มีเครื่องหมายและจำนวนเต็มมีเครื่องหมายสองอย่างนี้แน่นอนไม่มีอินเตอร์เซกชัน ดังนั้นอินเตอร์เซกชันจึงเป็นเซตว่าง ไม่ว่าส่งประเภทอะไรในตัวอย่างด้านล่างก็ไม่สามารถผ่านการคอมไพล์ได้
Do[Integer](1)
Do[Integer](-100)อินเทอร์เฟซว่าง
อินเทอร์เฟซว่างไม่เหมือนกับเซตว่าง อินเทอร์เฟซว่างคือเซตของ type set ทั้งหมด คือมีทุกประเภท
func Do[T interface{}](n T) T {
return n
}
func main() {
Do[struct{}](struct{}{})
Do[any]("abc")
}แต่เรามักใช้ any เป็นพารามิเตอร์ generics เพราะ interface{} ไม่สวย
ประเภทพื้นฐาน
เมื่อใช้คำสำคัญ type ประกาศประเภทใหม่หนึ่งประเภท แม้ประเภทพื้นฐานของมันรวมอยู่ใน type set แล้ว เมื่อส่งเข้ามาก็ยังคงไม่สามารถผ่านการคอมไพล์ได้
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type TinyInt int8
func Do[T Int](n T) T {
return n
}
func main() {
Do[TinyInt](1) // ไม่สามารถผ่านการคอมไพล์ แม้ประเภทพื้นฐานของมันอยู่ในขอบเขตของ type set Int
}มีวิธีแก้สองวิธี วิธีแรกคือเพิ่มประเภทนั้นเข้าไปใน type set แต่สิ่งนี้ไร้ความหมาย เพราะ TinyInt กับ int8 มีประเภทพื้นฐานเดียวกัน ดังนั้นจึงมีวิธีแก้วิธีที่สอง
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64 | TinyInt
}ใช้สัญลักษณ์ ~ เพื่อแสดงประเภทพื้นฐาน หากประเภทพื้นฐานของประเภทหนึ่งอยู่ใน type set นี้ แล้วประเภทนั้นอยู่ใน type set นี้ ดังแสดงด้านล่าง
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
}หลังจากแก้ไขแล้วสามารถผ่านการคอมไพล์ได้
func main() {
Do[TinyInt](1) // สามารถผ่านการคอมไพล์ได้ เพราะ TinyInt อยู่ใน type set Int
}ข้อควรระวัง
generics ไม่สามารถใช้เป็นประเภทพื้นฐานของประเภทได้
การเขียนต่อไปนี้ผิด พารามิเตอร์ generics T ไม่สามารถใช้เป็นประเภทพื้นฐานได้
type GenericType[T int | int32 | int64] Tแม้การเขียนต่อไปนี้จะได้รับอนุญาต แต่ไร้ความหมายและอาจทำให้เกิดปัญหาตัวเลขล้น ดังนั้นไม่แนะนำ
type GenericType[T int | int32 | int64] intประเภท generics ไม่สามารถใช้ type assertion ได้
การใช้ type assertion กับประเภท generics จะไม่สามารถผ่านการคอมไพล์ได้ generics ต้องแก้ปัญหาที่ ไม่เกี่ยวข้องกับประเภท หากปัญหาหนึ่งต้องการทำตรรกะที่ต่างกันตามประเภทที่ต่างกัน แล้วไม่ควรใช้ generics ควรใช้ interface{} หรือ any
func Sum[T int | float64](a, b T) T {
ints,ok := a.(int) // ไม่ได้รับอนุญาต
switch a.(type) { // ไม่ได้รับอนุญาต
case int:
case bool:
...
}
return a + b
}สตรักต์ไม่ระบุชื่อไม่รองรับ generics
สตรักต์ไม่ระบุชื่อไม่รองรับ generics โค้ดต่อไปนี้将无法ผ่านการคอมไพล์
testStruct := struct[T int | string] {
Name string
Id T
}[int]{
Name: "jack",
Id: 1
}ฟังก์ชันไม่ระบุชื่อไม่รองรับ generics ที่กำหนดเอง
การเขียนสองรูปแบบต่อไปนี้将无法ผ่านการคอมไพล์
var sum[T int | string] func (a, b T) T
sum := func[T int | string](a,b T) T{
...
}แต่สามารถ ใช้ ประเภท generics ที่มีอยู่แล้ว ได้ เช่นใน closure
func Sum[T int | float64](a, b T) T {
sub := func(c, d T) T {
return c - d
}
return sub(a,b) + a + b
}ไม่รองรับเมธอด generics
เมธอดไม่สามารถมีพารามิเตอร์ generics ได้ แต่ receiver สามารถมีพารามิเตอร์ generics ได้ โค้ดต่อไปนี้将无法ผ่านการคอมไพล์
type GenericStruct[T int | string] struct {
Name string
Id T
}
func (g GenericStruct[T]) name[S int | float64](a S) S {
return a
}Type set ไม่สามารถใช้เป็นประเภทอาร์กิวเมนต์ได้
อินเทอร์เฟซใดๆ ที่มี type set ไม่สามารถใช้เป็นประเภทอาร์กิวเมนต์ได้
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
func Do[T SignedInt](n T) T {
return n
}
func main() {
Do[SignedInt](1) // ไม่สามารถผ่านการคอมไพล์
}ปัญหาอินเตอร์เซกชันใน type set
สำหรับประเภทที่ไม่ใช่อินเทอร์เฟซ ในยูเนียนของประเภท不能有อินเตอร์เซกชัน เช่นในตัวอย่างด้านล่าง TinyInt กับ ~int8 มีอินเตอร์เซกชัน
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // ไม่สามารถผ่านการคอมไพล์
}
type TinyInt int8但对于อินเทอร์เฟซประเภทแล้ว อนุญาตให้มีอินเตอร์เซกชันได้ ดังตัวอย่างด้านล่าง
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // สามารถผ่านการคอมไพล์
}
type TinyInt interface {
int8
}Type set ไม่สามารถยูเนียนกับตัวเองโดยตรงหรือโดยอ้อมได้
ในตัวอย่างด้านล่าง Floats ยูเนียนกับตัวเองโดยตรง และ Double ยูเนียนกับ Floats ดังนั้นจึงยูเนียนกับตัวเองโดยอ้อม
type Floats interface { // โค้ดไม่สามารถผ่านการคอมไพล์
Floats | Double
}
type Double interface {
Floats
}อินเทอร์เฟซ comparable ไม่สามารถยูเนียนกับ type set ได้
ในทำนองเดียวกัน ไม่สามารถยูเนียนกับข้อจำกัดประเภทได้ ดังนั้นโดยพื้นฐานแล้วใช้แยกกัน
func Do[T comparable | Integer](n T) T { //ไม่สามารถผ่านการคอมไพล์
return n
}
type Number interface { // ไม่สามารถผ่านการคอมไพล์
Integer | comparable
}
type Comparable interface { // สามารถผ่านการคอมไพล์ได้แต่ไร้ความหมาย
comparable
}Method set ไม่สามารถยูเนียนกับ type set ได้
อินเทอร์เฟซใดๆ ที่มีเมธอด ไม่สามารถยูเนียนกับเซตประเภทได้
type I interface {
int | fmt.Stringer // cannot use fmt.Stringer in union (fmt.Stringer contains methods)
}แต่พวกมันสามารถทำอินเตอร์เซกชันได้ แต่การทำเช่นนี้ไม่มีประโยชน์ใดๆ
type I interface {
int
fmt.Stringer
}การใช้งาน
โครงสร้างข้อมูลเป็นสถานการณ์การใช้งานที่พบบ่อยที่สุดของ generics ด้านล่างนี้ใช้โครงสร้างข้อมูลสองอย่างเพื่อแสดงวิธีการใช้ generics
คิว
ด้านล่างนี้ใช้ generics 实现คิวอย่างง่าย ก่อนอื่นประกาศประเภทคิว ประเภทองค์ประกอบในคิวสามารถเป็นอะไรก็ได้ ดังนั้นข้อจำกัดประเภทคือ any
type Queue[T any] []Tมีเพียงสี่เมธอด Pop, Peek, Push, Size โค้ดมีดังนี้
type Queue[T any] []T
func (q *Queue[T]) Push(e T) {
*q = append(*q, e)
}
func (q *Queue[T]) Pop(e T) (_ T) {
if q.Size() > 0 {
res := q.Peek()
*q = (*q)[1:]
return res
}
return
}
func (q *Queue[T]) Peek() (_ T) {
if q.Size() > 0 {
return (*q)[0]
}
return
}
func (q *Queue[T]) Size() int {
return len(*q)
}ในเมธอด Pop และ Peek จะเห็นว่าค่าส่งกลับคือ _ T นี่เป็นการใช้ค่าส่งกลับที่มีชื่อ แต่ใช้ขีดล่าง _ แสดงว่านี่เป็นนิรนาม นี่ไม่ใช่ทำมากเกินไป แต่เพื่อแสดงค่าศูนย์ generics เนื่องจากใช้ generics เมื่อคิวว่าง ต้องส่งคืนค่าศูนย์ แต่เนื่องจากประเภทไม่ทราบ ไม่สามารถส่งคืนประเภทที่เฉพาะเจาะจงได้ ผ่านวิธีข้างต้น就可以ส่งคืนค่าศูนย์ generics ยังสามารถประกาศตัวแปร generics เพื่อแก้ปัญหาค่าศูนย์ได้ สำหรับตัวแปร generics หนึ่งตัว ค่าเริ่มต้นของมันคือค่าศูนย์ของประเภทนั้น ดังนี้
func (q *Queue[T]) Pop(e T) T {
var res T
if q.Size() > 0 {
res = q.Peek()
*q = (*q)[1:]
return res
}
return res
}ฮีพ
ตัวอย่างคิวข้างต้น เนื่องจากไม่มีความต้องการใดๆ สำหรับองค์ประกอบ ดังนั้นข้อจำกัดประเภทคือ any แต่ฮีพไม่เหมือนกัน ฮีพเป็นโครงสร้างข้อมูลพิเศษชนิดหนึ่ง สามารถตัดสินค่าสูงสุดหรือต่ำสุดได้ในเวลา O(1) ดังนั้นจึงมีความต้องการหนึ่งสำหรับองค์ประกอบ นั่นคือต้องเป็นประเภทที่สามารถเรียงลำดับได้ แต่ประเภทที่สามารถเรียงลำดับได้ในตัวมีเพียงตัวเลขและสตริงเท่านั้น ดังนั้นในการเริ่มต้นฮีพ ต้องส่ง comparator ที่กำหนดเอง comparator จัดให้ผู้เรียก และ comparator ต้องใช้ generics ด้วย ดังนี้
type Comparator[T any] func(a, b T) intด้านล่างนี้เป็น การนำไปใช้ binary heap อย่างง่าย ก่อนอื่นประกาศสตรักต์ generics ยังคงใช้ any เพื่อจำกัด这样可以เก็บประเภทใดๆ ได้
type Comparator[T any] func(a, b T) int
type BinaryHeap[T any] struct {
s []T
c Comparator[T]
}การนำไปใช้ของเมธอดหลายตัว
func (heap *BinaryHeap[T]) Peek() (_ T) {
if heap.Size() > 0 {
return heap.s[0]
}
return
}
func (heap *BinaryHeap[T]) Pop() (_ T) {
size := heap.Size()
if size > 0 {
res := heap.s[0]
heap.s[0], heap.s[size-1] = heap.s[size-1], heap.s[0]
heap.s = heap.s[:size-1]
heap.down(0)
return res
}
return
}
func (heap *BinaryHeap[T]) Push(e T) {
heap.s = append(heap.s, e)
heap.up(heap.Size() - 1)
}
func (heap *BinaryHeap[T]) up(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
for parentIndex := i>>1 - 1; parentIndex >= 0; parentIndex = i>>1 - 1 {
// more than or equal to
if heap.compare(heap.s[i], heap.s[parentIndex]) >= 0 {
break
}
heap.s[i], heap.s[parentIndex] = heap.s[parentIndex], heap.s[i]
i = parentIndex
}
}
func (heap *BinaryHeap[T]) down(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
size := heap.Size()
for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 {
rsonIndex := lsonIndex + 1
if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 {
lsonIndex = rsonIndex
}
// less than or equal to
if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 {
break
}
heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i]
i = lsonIndex
}
}
func (heap *BinaryHeap[T]) Size() int {
return len(heap.s)
}
func NewHeap[T any](n int, c Comparator[T]) BinaryHeap[T] {
var heap BinaryHeap[T]
heap.s = make([]T, 0, n)
heap.Comparator = c
return heap
}เมื่อใช้ดังนี้
type Person struct {
Age int
Name string
}
func main() {
heap := NewHeap[Person](10, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
heap.Push(Person{Age: 10, Name: "John"})
heap.Push(Person{Age: 18, Name: "mike"})
heap.Push(Person{Age: 9, Name: "lili"})
heap.Push(Person{Age: 32, Name: "miki"})
fmt.Println(heap.Peek())
fmt.Println(heap.Pop())
fmt.Println(heap.Peek())
}ผลลัพธ์
{9 lili}
{9 lili}
{10 John}ด้วยการสนับสนุนของ generics ประเภทที่ไม่สามารถเรียงลำดับได้เดิมหลังจากส่ง comparator แล้ว也可以使用ฮีพได้ การทำเช่นนี้แน่นอนว่าสง่างามและสะดวกกว่าการใช้ interface{} เพื่อแปลงประเภทและ assertion มาก
พูลออบเจกต์
พูลออบเจกต์เวอร์ชันเดิมใช้ได้เพียงประเภท any ทุกครั้งที่ดึงออกมาต้องทำ type assertion หลังจากปรับปรุงด้วย generics อย่างง่าย就可以ประหยัดงานนี้
package main
import (
"bytes"
"fmt"
"sync"
)
func NewPool[T any](newFn func() T) *Pool[T] {
return &Pool[T]{
pool: &sync.Pool{
New: func() interface{} {
return newFn()
},
},
}
}
type Pool[T any] struct {
pool *sync.Pool
}
func (p *Pool[T]) Put(v T) {
p.pool.Put(v)
}
func (p *Pool[T]) Get() T {
var v T
get := p.pool.Get()
if get != nil {
v, _ = get.(T)
}
return v
}
func main() {
bufferPool := NewPool(func() *bytes.Buffer {
return bytes.NewBuffer(nil)
})
for range 100 {
buffer := bufferPool.Get()
buffer.WriteString("Hello, World!")
fmt.Println(buffer.String())
buffer.Reset()
bufferPool.Put(buffer)
}
}สรุป
จุดเด่นหนึ่งของ go คือความเร็วในการคอมไพล์เร็วมาก การคอมไพล์เร็วเป็นเพราะการเพิ่มประสิทธิภาพในช่วงคอมไพล์ทำน้อย การเพิ่ม generics จะทำให้工作量ของคอมไพเลอร์เพิ่มขึ้น งานซับซ้อนมากขึ้น สิ่งนี้แน่นอนจะทำให้ความเร็วในการคอมไพล์ช้าลง ความจริงแล้วเมื่อ go1.18推出 generics ตอนแรก确实ทำให้การคอมไพล์ช้าลง ทีม go ทั้งต้องการเพิ่ม generics ไม่ต้องการทำให้ความเร็วในการคอมไพล์ช้าเกินไป ผู้พัฒนาใช้สะดวก คอมไพเลอร์ก็ลำบาก ในทางกลับกันคอมไพเลอร์สบายแล้ว (สบายที่สุดแน่นอนคือ不要 generics เลย) ผู้พัฒนาก็ลำบากแล้ว generics ในปัจจุบันเป็นผลผลิตจากการประนีประนอมระหว่างสองสิ่งนี้
