• Go语言泛型1.18(2)


    3.1 其他的泛型类

    所有类型定义都可使用类型形参,所以下面这种结构体以及接口的定义也可以使用类型形参:

    // 一个泛型类型的结构体。可用 int 或 sring 类型实例化
    type MyStruct[T int | string] struct {  
        Name string
        Data T
    }
    
    // 一个泛型接口(关于泛型接口在后半部分会详细讲解)
    type IPrintData[T int | float32 | string] interface {
        Print(data T)
    }
    
    // 一个泛型通道,可用类型实参 int 或 string 实例化
    type MyChan[T int | string] chan T
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.2 类型形参的互相套用

    类型形参是可以互相套用的,如下

    type WowStruct[T int | float32, S []T] struct {
        Data     S
        MaxValue T
        MinValue T
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个例子看起来有点复杂且难以理解,但实际上只要记住一点:任何泛型类型都必须传入类型实参实例化才可以使用。所以我们这就尝试传入类型实参看看:

    var ws WowStruct[int, []int]
    // 泛型类型 WowStuct[T, S] 被实例化后的类型名称就叫 WowStruct[int, []int]
    
    • 1
    • 2

    上面的代码中,我们为T传入了实参 int,然后因为 S 的定义是 []T ,所以 S 的实参自然是 []int 。经过实例化之后 WowStruct[T,S] 的定义类似如下:

    // 一个存储int类型切片,以及切片中最大、最小值的结构体
    type WowStruct[int, []int] struct {
        Data     []int
        MaxValue int
        MinValue int
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为 S 的定义是 []T ,所以 T 一定决定了的话 S 的实参就不能随便乱传了,下面这样的代码是错误的

    // 错误。S的定义是[]T,这里T传入了实参int, 所以S的实参应当为 []int 而不能是 []float32
    ws := WowStruct[int, []float32]{
            Data:     []float32{1.0, 2.0, 3.0},
            MaxValue: 3,
            MinValue: 1,
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.3 几种语法错误

    定义泛型类型的时候,基础类型不能只有类型形参,如下:

    // 错误,类型形参不能单独使用
    type CommonType[T int|string|float32] T
    
    • 1
    • 2

    当类型约束的一些写法会被编译器误认为是表达式时会报错。如下:

    //✗ 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
    type NewType[T *int] []T
    // 上面代码再编译器眼中:它认为你要定义一个存放切片的数组,数组长度由 T 乘以 int 计算得到
    type NewType [T * int][]T 
    
    //✗ 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
    type NewType2[T *int|*float64] []T 
    
    //✗ 错误
    type NewType2 [T (int)] []T 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    为了避免这种误解,解决办法就是给类型约束包上 interface{} 或加上逗号消除歧义(关于接口具体的用法会在后半篇提及)

    type NewType[T interface{*int}] []T
    type NewType2[T interface{*int|*float64}] []T 
    
    // 如果类型约束中只有一个类型,可以添加个逗号消除歧义
    type NewType3[T *int,] []T
    
    //✗ 错误。如果类型约束不止一个类型,加逗号是不行的
    type NewType4[T *int|*float32,] []T 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    因为上面逗号的用法限制比较大,这里推荐统一用 interface{} 解决问题

    3.4 特殊的泛型类型

    这里讨论种比较特殊的泛型类型,如下:

    type Wow[T int | string] int
    
    var a Wow[int] = 123     // 编译正确
    var b Wow[string] = 123  // 编译正确
    var c Wow[string] = "hello" // 编译错误,因为"hello"不能赋值给底层类型int
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里虽然使用了类型形参,但因为类型定义是 type Wow[T int|string] int ,所以无论传入什么类型实参,实例化后的新类型的底层类型都是 int 。所以int类型的数字123可以赋值给变量a和b,但string类型的字符串 “hello” 不能赋值给c

    这个例子没有什么具体意义,但是可以让我们理解泛型类型的实例化的机制

    3.5 泛型类型的套娃

    泛型和普通的类型一样,可以互相嵌套定义出更加复杂的新类型,如下:

    // 先定义个泛型类型 Slice[T]
    type Slice[T int|string|float32|float64] []T
    
    // ✗ 错误。泛型类型Slice[T]的类型约束中不包含uint, uint8
    type UintSlice[T uint|uint8] Slice[T]  
    
    // ✓ 正确。基于泛型类型Slice[T]定义了新的泛型类型 FloatSlice[T] 。FloatSlice[T]只接受float32和float64两种类型
    type FloatSlice[T float32|float64] Slice[T] 
    
    // ✓ 正确。基于泛型类型Slice[T]定义的新泛型类型 IntAndStringSlice[T]
    type IntAndStringSlice[T int|string] Slice[T]  
    // ✓ 正确 基于IntAndStringSlice[T]套娃定义出的新泛型类型
    type IntSlice[T int] IntAndStringSlice[T] 
    
    // 在map中套一个泛型类型Slice[T]
    type WowMap[T int|string] map[string]Slice[T]
    // 在map中套Slice[T]的另一种写法
    type WowMap2[T Slice[int] | Slice[string]] map[string]T
    3.6 类型约束的两种选择
    观察下面两种类型约束的写法
    
    type WowStruct[T int|string] struct {
        Name string
        Data []T
    }
    
    type WowStruct2[T []int|[]string] struct {
        Name string
        Data T
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    仅限于这个例子,这两种写法和实现的功能其实是差不多的,实例化之后结构体相同。但是像下面这种情况的时候,我们使用前一种写法会更好:

    type WowStruct3[T int | string] struct {
        Data     []T
        MaxValue T
        MinValue T
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.7 匿名结构体不支持泛型
    我们有时候会经常用到匿名的结构体,并在定义好匿名结构体之后直接初始化:

    testCase := struct {
            caseName string
            got      int
            want     int
        }{
            caseName: "test OK",
            got:      100,
            want:     100,
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    那么匿名结构体能不能使用泛型呢?答案是不能,下面的用法是错误的:

    testCase := struct[T int|string] {
            caseName string
            got      T
            want     T
        }[int]{
            caseName: "test OK",
            got:      100,
            want:     100,
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    所以在使用泛型的时候我们只能放弃使用匿名结构体,对于很多场景来说这会造成麻烦(最主要麻烦集中在单元测试的时候,为泛型做单元测试会非常麻烦,这点我之后的文章将会详细阐述)

    4. 泛型receiver

    看了上的例子,你一定会说,介绍了这么多复杂的概念,但好像泛型类型根本没什么用处啊?

    是的,单纯的泛型类型实际上对开发来说用处并不大。但是如果将泛型类型和接下来要介绍的泛型receiver相结合的话,泛型就有了非常大的实用性了

    我们知道,定义了新的普通类型之后可以给类型添加方法。那么可以给泛型类型添加方法吗?答案自然是可以的,如下:

    
    type MySlice[T int | float32] []T
    
    func (s MySlice[T]) Sum() T {
        var sum T
        for _, value := range s {
            sum += value
        }
        return sum
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个例子为泛型类型 MySlice[T] 添加了一个计算成员总和的方法 Sum() 。注意观察这个方法的定义:

    首先看receiver (s MySlice[T]) ,所以我们直接把类型名称 MySlice[T] 写入了receiver中
    然后方法的返回参数我们使用了类型形参 T **(实际上如果有需要的话,方法的接收参数也可以实用类型形参)
    在方法的定义中,我们也可以使用类型形参 T (在这个例子里,我们通过 var sum T 定义了一个新的变量 sum )
    对于这个泛型类型 MySlice[T] 我们该如何使用?还记不记得之前强调过很多次的,泛型类型无论如何都需要先用类型实参实例化,所以用法如下:

    
    var s MySlice[int] = []int{1, 2, 3, 4}
    fmt.Println(s.Sum()) // 输出:10
    
    var s2 MySlice[float32] = []float32{1.0, 2.0, 3.0, 4.0}
    fmt.Println(s2.Sum()) // 输出:10.0
    该如何理解上面的实例化?首先我们用类型实参 int 实例化了泛型类型 MySlice[T],所以泛型类型定义中的所有 T 都被替换为 int,最终我们可以把代码看作下面这样:
    
    type MySlice[int] []int // 实例化后的类型名叫 MyIntSlice[int]
    
    // 方法中所有类型形参 T 都被替换为类型实参 int
    func (s MySlice[int]) Sum() int {
        var sum int 
        for _, value := range s {
            sum += value
        }
        return sum
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    用 float32 实例化和用 int 实例化同理,此处不再赘述。

    通过泛型receiver,泛型的实用性一下子得到了巨大的扩展。在没有泛型之前如果想实现通用的数据结构,诸如:堆、栈、队列、链表之类的话,我们的选择只有两个:

    为每种类型写一个实现
    使用 接口+反射
    而有了泛型之后,我们就能非常简单地创建通用数据结构了。接下来用一个更加实用的例子 —— 队列 来讲解

    4.1 基于泛型的队列

    队列是一种先入先出的数据结构,它和现实中排队一样,数据只能从队尾放入、从队首取出,先放入的数据优先被取出来

    // 这里类型约束使用了空接口,代表的意思是所有类型都可以用来实例化泛型类型 Queue[T] (关于接口在后半部分会详细介绍)

    type Queue[T interface{}] struct {
        elements []T
    }
    
    // 将数据放入队列尾部
    func (q *Queue[T]) Put(value T) {
        q.elements = append(q.elements, value)
    }
    
    // 从队列头部取出并从头部删除对应数据
    func (q *Queue[T]) Pop() (T, bool) {
        var value T
        if len(q.elements) == 0 {
            return value, true
        }
    
        value = q.elements[0]
        q.elements = q.elements[1:]
        return value, len(q.elements) == 0
    }
    
    // 队列大小
    func (q Queue[T]) Size() int {
        return len(q.elements)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    💡 为了方便说明,上面是队列非常简单的一种实现方法,没有考虑线程安全等很多问题
    Queue[T] 因为是泛型类型,所以要使用的话必须实例化,实例化与使用方法如下所示:

    
    var q1 Queue[int]  // 可存放int类型数据的队列
    q1.Put(1)
    q1.Put(2)
    q1.Put(3)
    q1.Pop() // 1
    q1.Pop() // 2
    q1.Pop() // 3
    
    var q2 Queue[string]  // 可存放string类型数据的队列
    q2.Put("A")
    q2.Put("B")
    q2.Put("C")
    q2.Pop() // "A"
    q2.Pop() // "B"
    q2.Pop() // "C"
    
    var q3 Queue[struct{Name string}] 
    var q4 Queue[[]int] // 可存放[]int切片的队列
    var q5 Queue[chan int] // 可存放int通道的队列
    var q6 Queue[io.Reader] // 可存放接口的队列
    // ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    4.2 动态判断变量的类型

    使用接口的时候经常会用到类型断言或 type swith 来确定接口具体的类型,然后对不同类型做出不同的处理,如:

    
    var i interface{} = 123
    i.(int) // 类型断言
    
    // type switch
    switch i.(type) {
        case int:
            // do something
        case string:
            // do something
        default:
            // do something
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    那么你一定会想到,对于 valut T 这样通过类型形参定义的变量,我们能不能判断具体类型然后对不同类型做出不同处理呢?答案是不允许的,如下:

    
    func (q *Queue[T]) Put(value T) {
        value.(int) // 错误。泛型类型定义的变量不能使用类型断言
    
        // 错误。不允许使用type switch 来判断 value 的具体类型
        switch value.(type) {
        case int:
            // do something
        case string:
            // do something
        default:
            // do something
        }
        
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    虽然type switch和类型断言不能用,但我们可通过反射机制达到目的:

    
    func (receiver Queue[T]) Put(value T) {
        // Printf() 可输出变量value的类型(底层就是通过反射实现的)
        fmt.Printf("%T", value) 
    
        // 通过反射可以动态获得变量value的类型从而分情况处理
        v := reflect.ValueOf(value)
    
        switch v.Kind() {
        case reflect.Int:
            // do something
        case reflect.String:
            // do something
        }
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这看起来达到了我们的目的,可是当你写出上面这样的代码时候就出现了一个问题:

    你为了避免使用反射而选择了泛型,结果到头来又为了一些功能在在泛型中使用反射
    当出现这种情况的时候你可能需要重新思考一下,自己的需求是不是真的需要用泛型(毕竟泛型机制本身就很复杂了,再加上反射的复杂度,增加的复杂度并不一定值得)
    当然,这一切选择权都在你自己的手里,根据具体情况斟酌

  • 相关阅读:
    如何玩转CSDN AI工具集
    GEE案例——一个完整的火灾监测案例dNBR差异化归一化烧毁指数
    2334. 元素值大于变化阈值的子数组-单调栈法和暴力求解法
    npm证书过期问题
    Android NDK 在Linux下使用mk文件生成SO库
    toB应用私有化交付发展历程、技术对比和选型
    java 开发框架 Redis 之 sentinel 和集群
    CAD布局图纸导出为模型图纸
    1. 两数之和、Leetcode的Python实现
    你也许不再需要使用 CSS Media Queries(媒体查询)了
  • 原文地址:https://blog.csdn.net/ZN175623/article/details/127742043