书画院网站建设,三合一网站平台,国外做logo的网站,软件开发文档怎么编写类型推导和泛型
就像在使用:时支持类型推导一样#xff0c;在调用泛型函数时Go同样支持类型推导。可在上面对Map、Filter和Reduce调用中看出。有些场景无法进行类型推导#xff08;如类型参数仅用作返回值#xff09;。这时#xff0c;必…类型推导和泛型
就像在使用:时支持类型推导一样在调用泛型函数时Go同样支持类型推导。可在上面对Map、Filter和Reduce调用中看出。有些场景无法进行类型推导如类型参数仅用作返回值。这时必须指定所有的参数类型。下面的代码演示了无法进行类型推导的场景
type Integer interface {int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}func Convert[T1, T2 Integer](in T1) T2 {return T2(in)
}func main() {var a int 10b : Convert[int, int64](a) // 无法推导返回类型fmt.Println(b)
}
可在The Go Playground 或第8章的GitHub代码库的sample_code/type_inference目录下测试这段代码。
类型元素限定常量
类型元素也可指定哪些常量可赋值给泛型变量。和运算符一样常需要对类型元素中的所有类型名有效。没有常量可同时赋值给Ordered中列出的所有类型因此无法将一个常量赋值给该泛型类型的变量。如果使用Integer接口以下代码无法编译通过因为不能将1,000赋值给8位的整型
// INVALID!
func PlusOneThousand[T Integer](in T) T {return in 1_000
}
但下面的就是有效的
// VALID
func PlusOneHundred[T Integer](in T) T {return in 100
}
组合泛型函数和泛型数据结构
回到二叉树示例来看如何使用所学的知识生成适用所有实体类型的树。
核心在于理解该树需要一个泛型函数可比较两个值给出排序
type OrderableFunc [T any] func(t1, t2 T) int
有了OrderableFunc我们就可以稍稍修改树的实现。首先将其分成两种类型Tree和Node
type Tree[T any] struct {f OrderableFunc[T]root *Node[T]
}type Node[T any] struct {val Tleft, right *Node[T]
}
通过构造函数构造一个新Tree
func NewTree[T any](f OrderableFunc[T]) *Tree[T] {return Tree[T]{f: f,}
}
Tree的方法非常简单因为它调用Node来完成任务
func (t *Tree[T]) Add(v T) {t.root t.root.Add(t.f, v)
}func (t *Tree[T]) Contains(v T) bool {return t.root.Contains(t.f, v)
}
Node的Add和Contains方法与之前的非常类似。唯一的区别是传递了用于排序元素的函数
func (n *Node[T]) Add(f OrderableFunc[T], v T) *Node[T] {if n nil {return Node[T]{val: v}}switch r : f(v, n.val); {case r -1:n.left n.left.Add(f, v)case r 1:n.right n.right.Add(f, v)}return n
}func (n *Node[T]) Contains(f OrderableFunc[T], v T) bool {if n nil {return false}switch r : f(v, n.val); {case r -1:return n.left.Contains(f, v)case r 1:return n.right.Contains(f, v)}return true
}
现在我们需要匹配OrderedFunc定义的函数。所幸我们已经见过一个cmp包中的Compare。在对Tree使用它时是这样
t1 : NewTree(cmp.Compare[int])
t1.Add(10)
t1.Add(30)
t1.Add(15)
fmt.Println(t1.Contains(15))
fmt.Println(t1.Contains(40))
对于结构体有两种选项。可以编写一个函数
type Person struct {Name stringAge int
}func OrderPeople(p1, p2 Person) int {out : cmp.Compare(p1.Name, p2.Name)if out 0 {out cmp.Compare(p1.Age, p2.Age)}return out
}
然后在创建树进传递该函数
t2 : NewTree(OrderPeople)
t2.Add(Person{Bob, 30})
t2.Add(Person{Maria, 35})
t2.Add(Person{Bob, 50})
fmt.Println(t2.Contains(Person{Bob, 30}))
fmt.Println(t2.Contains(Person{Fred, 25}))
不使用函数我们了可以为NewTree提供一个方法。在方法也是函数中我们讨论过可以使用方法表达式来将方法看作函数。下面上手操作。首先编写方法
func (p Person) Order(other Person) int {out : cmp.Compare(p.Name, other.Name)if out 0 {out cmp.Compare(p.Age, other.Age)}return out
}
然后使用该方法
t3 : NewTree(Person.Order)
t3.Add(Person{Bob, 30})
t3.Add(Person{Maria, 35})
t3.Add(Person{Bob, 50})
fmt.Println(t3.Contains(Person{Bob, 30}))
fmt.Println(t3.Contains(Person{Fred, 25}))
可在The Go Playground 或第8章的GitHub代码库的sample_code/generic_tree目录下测试这段代码。
再谈可比较类型
在接口可比较一节中我们学到接口中也是Go中一种可比较类型。这也就表示在对接口类型变量使用和!时要小心。如果接口的底层类型不可比较代码会在运行时panic。
这个坑在使用带泛型的可比较接口时依然存在。假设我们定义了一个接口以及一些实现
type Thinger interface {Thing()
}type ThingerInt intfunc (t ThingerInt) Thing() {fmt.Println(ThingInt:, t)
}type ThingerSlice []intfunc (t ThingerSlice) Thing() {fmt.Println(ThingSlice:, t)
}
还需要定义一个泛型函数仅接收可比较的值
func Comparer[T comparable](t1, t2 T) {if t1 t2 {fmt.Println(equal!)}
}
调用带类型为int或ThingerInt的变量的函数完全合法
var a int 10
var b int 10
Comparer(a, b) // prints truevar a2 ThingerInt 20
var b2 ThingerInt 20
Comparer(a2, b2) // prints true
编译器不允许我们调用变量类型为ThingerSlice或[]int的函数
var a3 ThingerSlice []int{1, 2, 3}
var b3 ThingerSlice []int{1, 2, 3}
Comparer(a3, b3) // compile fails: ThingerSlice does not satisfy comparable
但所调用的变量类型为Thinger时完全合法。如果使用ThingerInt代码可正常编译、运行
var a4 Thinger a2
var b4 Thinger b2
Comparer(a4, b4) // prints true
但也可以将ThingerSlice赋值给Thinger类型的变量。这时会出问题
a4 a3
b4 b3
Comparer(a4, b4) // compiles, panics at runtime
编译器允许我们构建这段代码但运行后程序会panic参见panic和recover一节了解更多信息消息为panic: runtime error: comparing uncomparable type main.ThingerSlice。可在The Go Playground 或第8章的GitHub代码库的sample_code/more_comparable目录下测试这段代码。
在关可比较类型和泛型交互以及为何做出这种设计决策的更多技术细节请阅读Go团队Robert Griesemer的博客文章All your comparable types。
未实现的功能
Go仍是一种小型且聚焦的编程语言Go对泛型的实现并未包含部分在其它语言泛型中存在的特性。下面是一些Go泛型尚未实现的特性。
虽然我们可以构建一个同时能处理自定义和内置类型的树但在Python、Ruby和C中处理的方法却不同。它们有运算符重载允许用户自定义类型指定运算符的实现。 Go没有添加这种特性。也就意味着我们不能使用range遍历自定义容器类型也不能对其使用[]进行索引。
没添加运算符重载有一些原因。其一是Go语言中有极其大量的运算符。Go也不支持函数或方法重载那就需要为不同的类型指定不同的运算函数。此外重载的代码会不易理解因为开发人员会为符号巧立各种含义在C中对一些类型表示按位左移而对另一些类型则在左侧值的右侧写值。Go努力避免这类易读性问题。
另一个未实现的有用特性是Go的泛型实现对方法没有附加类型参数。回看Map/Reduce/Filter函数你可能觉得它们可像方法那样使用如
type functionalSlice[T any] []T// THIS DOES NOT WORK
func (fs functionalSlice[T]) Map[E any](f func(T) E) functionalSlice[E] {out : make(functionalSlice[E], len(fs))for i, v : range fs {out[i] f(v)}return out
}// THIS DOES NOT WORK
func (fs functionalSlice[T]) Reduce[E any](start E, f func(E, T) E) E {out : startfor _, v : range fs {out f(out, v)}return out
}
你以为可以这样用
var numStrings functionalSlice[string]{1, 2, 3}
sum : numStrings.Map(func(s string) int {v, _ : strconv.Atoi(s)return v
}).Reduce(0, func(acc int, cur int) int {return acc cur
})
可惜对于函数式编程的拥趸们并不能这样用。我们不能做链式方法调用而要嵌套函数调用或使用更易读一次调用一次函数的方式将中间值赋给变量。类型参数提案中详细讨论了未支持参数化方法的原因。
没有可变类型参数。在可变参数和切片一节中讨论到要实现接收可变数量参数的函数需要指定最后一个参数其类型以...开头。比如无法对可变参数指定某种类型模式像可交替的string和int。所有的可变变量必须为同一种声明类型是不是泛型皆可。
Go泛型未实现的其它特性就更加晦涩些了。有
特化Specialization函数或方法可通过泛型版本外的一个或多个指定类型版本进行重载。因Go语言没有重载这一特性不在考虑范围内。柯里化Currying允许我们通过指定某些类型参数根据另一个泛型函数或类型部分实例化函数。元编程允许我们指定在编译时运行的代码并生成运行时运行的代码。
地道的Go和泛型
添加泛型显然会改变一些地道使用Go的建议。使用float64来表示所有的数值类型的时代结束了。应当使用any来代替interface{}表示数据结构或函数参数中未指定的类型。可以用一个点函数处理不同的切片类型。但不要觉得要马上使用类型参数切换掉所有的代码。在新设计模式发明和深化的同时老代码依然正常可用。
现在判断泛型对性能的长期影响还为时尚早。在写本文时它对编译时间并没有影响。Go 1.18的编译器要慢于之前的版本但Go 1.20的编译器解决了这一问题。
有一些关于泛型对运行时间影响的影响。Vicent Marti写了一篇深入的文章探讨了一些导致代码变慢的泛型案例并详细讲解了产生这一问题的实现细节。相反Eli Bendersky写了一篇博客文章说明泛型让排序算法变快了。
一般来说不要期望将带接口参数的函数修改为泛型类型参数的函数能提升性能。比如将下面的小函数
type Ager interface {age() int
}func doubleAge(a Ager) int {return a.age() * 2
}
转化为
func doubleAgeGeneric[T Ager](a T) int {return a.age() * 2
}
会使得该函数在Go 1.20变慢约30%。对于大型函数没有显著的性能区别。可以使用第8章的GitHub代码库的sample_code/perf directory目录下代码进行基准测试。
使用过其它语言泛型的开发者可能会感到意外。比如在C中编译器使用抽象数据类型的泛型来将运行时运算确定所使用的实体类型转化为编译时运算为每种实体类型生成独立的函数。这会让二进制变大但也让其变快。Vicent在博客文章中提到当前的Go编译器仅为不同的底层类型生成独立函数。此外所有指针类型共享同一个生成函数。为区分传递给共享生成函数的类型编译器添加了额外的运行时查询。这会减慢性能。
随着Go未来版本中泛型实现渐趋成熟运行时性能也会提升。目标并没有改变还是要编写满足需求且易维护的快速运行代码。使用基准测试一节中讨论的基准测试和性能测试工具来度量和提升你的代码。
向标准库添加泛型
Go 1.18刚发布泛型时是很保守的。在全局添加了any和comparable接口但并未在标准库中做出支持泛型的API调整。只做出了样式变化将大部分标准库中的interface{}改成了any。
现在Go社区更适应了泛型我们也看到了更多的变化。从Go 1.21起标准库中包含了一些函数使用泛型实现切片、字典和并发的常用算法。在复合类型一文中我们讲到了slices和maps包中的Equal和EqualFunc函数。这些包中的其它函数简化了切片和字典操作。slices包中的Insert、Delete和DeleteFunc 函数让开发展不必构建极其复杂的切片处理代码。maps.Clone函数利用Go Runtime来提供更快速的方式来创建字典的浅拷贝。在代码精确地只运行一次一节中我们学到sync.OnceValue和sync.OnceValues它们使用泛型来构建只运行一次并返回一到两个值的函数。推荐使用这些包中的函数而不要自己去实现。未来版本的标准库还会包含更多用到泛型的函数和类型。
解锁未来特性
泛型可能是其它未来特性的基础。一个可能是sum types。就像类型元素用于指定可替换类型参数的类型一样和类型可用于变量参数中的接口。这会出现一些有趣的特性。如今Go在JSON的常见场景存在问题其字段可以是单个值也可是值列表。即使是有泛型处理这种情况的唯一方式是装饰字段类型设为any。添加和类型可让我们创建指定字段可为字符串、字符串切片及其它类型的接口。然后类型switch可以枚举每种有效类型提升类型案例。指定类型边界集的能力可以让现代语言包括Rust和Swift使用和类型替代枚举。而Go当前在枚举特性上存在不足这会成为一种有吸引力的解决方案但需要时间来评估和探讨这些想法 。
小结
本文中我们学习了泛型以及如何使用泛型来简化代码。对于Go来说泛型还处于早期除非。有它伴随Go语言不忘初心的成长还是很让人激动的。
本文来自正在规划的Go语言云原生自我提升系列欢迎关注后续文章。