青岛网站建设免费,十大免费ppt网站在线,广告优化,收录情况有几种1.泛型
泛型是一种独立于所使用的特定类型的编写代码的方法。使用泛型可以编写出适用于一组类型中的任何一种的函数和类型。
1.1 为什么需要泛型
func reverse(s []int) []int {l : len(s)r : make([]int, l)for i, e : range s {r[l-i-1] e}return r
}fmt.Println(reverse…1.泛型
泛型是一种独立于所使用的特定类型的编写代码的方法。使用泛型可以编写出适用于一组类型中的任何一种的函数和类型。
1.1 为什么需要泛型
func reverse(s []int) []int {l : len(s)r : make([]int, l)for i, e : range s {r[l-i-1] e}return r
}fmt.Println(reverse([]int{1, 2, 3, 4})) // [4 3 2 1]可是这个函数只能接收[]int类型的参数如果我们想支持[]float64类型的参数我们就需要再定义一个reverseFloat64Slice函数。
func reverseFloat64Slice(s []float64) []float64 {l : len(s)r : make([]float64, l)for i, e : range s {r[l-i-1] e}return r
}如果要想支持[]string类型切片就要定义reverseStringSlice函数如果想支持[]xxx就需要定义一个reverseXxxSlice.
一遍一遍地编写相同的功能是低效的实际上这个反转切片的函数并不需要知道切片中元素的类型但为了适用不同的类型我们把一段代码重复了很多遍。
Go1.18之前我们可以尝试使用反射去解决上述问题但是使用反射在运行期间获取变量类型会降低代码的执行效率并且失去编译期的类型检查同时大量的反射代码也会让程序变得晦涩难懂。
从Go1.18开始使用泛型就能够编写出适用所有元素类型的“普适版”reverse函数。
func reverseWithGenerics[T any](s []T) []T {l : len(s)r : make([]T, l)for i, e : range s {r[l-i-1] e}return r
}1.2 泛型语法
泛型为Go语言添加了三个新的重要特性:
函数和类型的类型参数。将接口类型定义为类型集包括没有方法的类型。类型推断它允许在调用函数时在许多情况下省略类型参数。
类型参数
类型形参和类型实参
函数定义时可以指定形参函数调用时需要传入实参。
func min(a, b int) int {//a,b两个形参
if a b {return a
}
return b
}min(10, 20)//调用函数min传入两个实参 10,20 现在Go语言中的函数和类型支持添加类型参数。类型参数列表看起来像普通的参数列表只不过它使用方括号[]而不是圆括号()。 借助泛型我们可以声明一个适用于一组类型的min函数。
func min[T int | float64](a, b T) T {if a b {return a}return b
}类型实例化
这次定义的min函数就同时支持int和float64两种类型也就是说当调用min函数时我们既可以传入int类型的参数。
m1 : min[int](1, 2) // 1也可以传入float64类型的参数。
m2 : min[float64](-0.1, -0.2) // -0.2向 min 函数提供类型参数(在本例中为int和float64)称为实例化 instantiation 。
类型实例化分两步进行
首先编译器在整个泛型函数或类型中将所有类型形参type parameters替换为它们各自的类型实参type arguments。其次编译器验证每个类型参数是否满足相应的约束。
在成功实例化之后我们将得到一个非泛型函数它可以像任何其他函数一样被调用。例如
fmin : min[float64] // 类型实例化编译器生成Tfloat64的min函数
m2 fmin(1.2, 2.3) // 1.2min[float64]得到的是类似我们之前定义的minFloat64函数——fmin我们可以在函数调用中使用它。
类型参数的使用
除了函数中支持使用类型参数列表外类型也可以使用类型参数列表。
type Slice[T int | string] []Ttype Map[K int | string, V float32 | float64] map[K]Vtype Tree[T interface{}] struct {left, right *Tree[T]value T
}在上述泛型类型中T、K、V都属于类型形参类型形参后面是类型约束类型实参需要满足对应的类型约束。
泛型类型可以有方法例如为上面的Tree实现一个查找元素的Lookup方法。
func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }要使用泛型类型必须进行实例化。Tree[string]是使用类型实参string实例化 Tree 的示例。
var stringTree Tree[string]类型约束
普通函数中的每个参数都有一个类型; 该类型定义一系列值的集合。例如我们上面定义的非泛型函数minFloat64那样声明了参数的类型为float64那么在函数调用时允许传入的实际参数就必须是可以用float64类型表示的浮点数值。
类似于参数列表中每个参数都有对应的参数类型类型参数列表中每个类型参数都有一个类型约束。类型约束定义了一个类型集——只有在这个类型集中的类型才能用作类型实参。
Go语言中的类型约束是接口类型。
就以上面提到的min函数为例我们来看一下类型约束常见的两种方式。
类型约束接口可以直接在类型参数列表中使用。
// 类型约束字面量通常外层interface{}可省略
func min[T interface{ int | float64 }](a, b T) T {if a b {return a}return b
}作为类型约束使用的接口类型可以事先定义并支持复用。
// 事先定义好的类型约束类型
type Value interface {int | float64
}
func min[T Value](a, b T) T {if a b {return a}return b
}在使用类型约束时如果省略了外层的interface{}会引起歧义那么就不能省略。例如
type IntPtrSlice [T *int] []T // T*int ?type IntPtrSlice[T *int,] []T // 只有一个类型约束时可以添加,
type IntPtrSlice[T interface{ *int }] []T // 使用interface{}包裹
类型集
Go1.18开始接口类型的定义也发生了改变由过去的接口类型定义方法集method set变成了接口类型定义类型集type set。也就是说接口类型现在可以用作值的类型也可以用作类型约束。 把接口类型当做类型集相较于方法集有一个优势: 我们可以显式地向集合添加类型从而以新的方式控制类型集。
Go语言扩展了接口类型的语法让我们能够向接口中添加类型。例如
type V interface {int | string | bool
}上面的代码就定义了一个包含 int、 string 和 bool 类型的类型集。 从 Go 1.18 开始一个接口不仅可以嵌入其他接口还可以嵌入任何类型、类型的联合或共享相同底层类型的无限类型集合。
当用作类型约束时由接口定义的类型集精确地指定允许作为相应类型参数的类型。 |符号 T1 | T2表示类型约束为T1和T2这两个类型的并集例如下面的Integer类型表示由Signed和Unsigned组成。 type Integer interface {Signed | Unsigned
}~符号 ~T表示所以底层类型是T的类型例如~string表示所有底层类型是string的类型集合。 type MyString string // MyString的底层类型是string注意~符号后面只能是基本类型。
接口作为类型集是一种强大的新机制是使类型约束能够生效的关键。目前使用新语法表的接口只能用作类型约束。
any接口
空接口在类型参数列表中很常见在Go 1.18引入了一个新的预声明标识符作为空接口类型的别名。
// src/builtin/builtin.gotype any interface{}由此我们可以使用如下代码
func foo[S ~[]E, E any]() {// ...
}类型推断
最后一个新的主要语言特征是类型推断。从某些方面来说这是语言中最复杂的变化但它很重要因为它能让人们在编写调用泛型函数的代码时更自然。
函数参数类型推断
对于类型参数需要传递类型参数这可能导致代码冗长。回到我们通用的 min函数
func min[T int | float64](a, b T) T {if a b {return a}return b
}类型形参T用于指定a和b的类型。我们可以使用显式类型实参调用它
var a, b, m float64
m min[float64](a, b) // 显式指定类型实参在许多情况下编译器可以从普通参数推断 T 的类型实参。这使得代码更短同时保持清晰。
var a, b, m float64m min(a, b) // 无需指定类型实参这种从实参的类型推断出函数的类型实参的推断称为函数实参类型推断。函数实参类型推断只适用于函数参数中使用的类型参数而不适用于仅在函数结果中或仅在函数体中使用的类型参数。例如它不适用于像 MakeT [ T any ]() T 这样的函数因为它只使用 T 表示结果。
约束类型推断
Go 语言支持另一种类型推断即约束类型推断。接下来我们从下面这个缩放整数的例子开始
// Scale 返回切片中每个元素都乘c的副本切片
func Scale[E constraints.Integer](s []E, c E) []E {r : make([]E, len(s))for i, v : range s {r[i] v * c}return r
}这是一个泛型函数适用于任何整数类型的切片。
现在假设我们有一个多维坐标的 Point 类型其中每个 Point 只是一个给出点坐标的整数列表。这种类型通常会实现一些业务方法这里假设它有一个String方法。
type Point []int32func (p Point) String() string {b, _ : json.Marshal(p)return string(b)
}由于一个Point其实就是一个整数切片我们可以使用前面编写的Scale函数
func ScaleAndPrint(p Point) {r : Scale(p, 2)fmt.Println(r.String()) // 编译失败
}不幸的是这代码会编译失败输出r.String undefined (type []int32 has no field or method String的错误。
问题是Scale函数返回类型为[]E的值其中E是参数切片的元素类型。当我们使用Point类型的值调用Scale其基础类型为[]int32时我们返回的是[]int32类型的值而不是Point类型。这源于泛型代码的编写方式但这不是我们想要的。
为了解决这个问题我们必须更改 Scale 函数以便为切片类型使用类型参数。
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {r : make(S, len(s))for i, v : range s {r[i] v * c}return r
}我们引入了一个新的类型参数S它是切片参数的类型。我们对它进行了约束使得基础类型是S而不是[]E函数返回的结果类型现在是S。由于E被约束为整数因此效果与之前相同第一个参数必须是某个整数类型的切片。对函数体的唯一更改是现在我们在调用make时传递S而不是[]E。
现在这个Scale函数不仅支持传入普通整数切片参数也支持传入Point类型参数。
这里需要思考的是为什么不传递显式类型参数就可以写入 Scale 调用也就是说为什么我们可以写 Scale(p, 2)没有类型参数而不是必须写 Scale[Point, int32](p, 2)
新 Scale 函数有两个类型参数——S 和 E。在不传递任何类型参数的 Scale(p, 2) 调用中如上所述函数参数类型推断让编译器推断 S 的类型参数是 Point。但是这个函数也有一个类型参数 E它是乘法因子 c 的类型。相应的函数参数是2因为2是一个非类型化的常量函数参数类型推断不能推断出 E 的正确类型(最好的情况是它可以推断出2的默认类型是 int而这是错误的因为Point 的基础类型是[]int32)。相反编译器推断 E 的类型参数是切片的元素类型的过程称为约束类型推断。
约束类型推断从类型参数约束推导类型参数。当一个类型参数具有根据另一个类型参数定义的约束时使用。当其中一个类型参数的类型参数已知时约束用于推断另一个类型参数的类型参数。
通常的情况是当一个约束对某种类型使用 ~type 形式时该类型是使用其他类型参数编写的。我们在 Scale 的例子中看到了这一点。S 是 ~[]E后面跟着一个用另一个类型参数写的类型[]E。如果我们知道了 S 的类型实参我们就可以推断出E的类型实参。S 是一个切片类型而 E是该切片的元素类型。
2.什么时候使用泛型
2.1 泛型使用方式
使用语言定义的容器类型时
当我们编写的是操作 Go 语言定义的特殊容器类型slice、map和chennel的函数。如果函数具有包含这些类型的参数并且函数的代码并不关心元素的类型那么使用类型参数可能是有用的。
例如返回任何类型map中所有的key
// MapKeys 返回m中所有key组成的切片
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {s : make([]Key, 0, len(m))for k : range m {s append(s, k)}return s
}通用数据结构
类型参数另一个适用场景就是用于通用数据结构。通用数据结构类似于slice或map但不是内置在语言中的例如链表或二叉树。
对于类型参数优先选择函数而不是方法
Tree 示例说明了另一个一般原则当你需要比较函数之类的东西时更喜欢使用函数而不是方法。
实现通用方法
类型参数可能有用的另一种情况是不同类型需要实现某些公共方法而不同类型的实现看起来都是相同的。
例如考虑标准库的 sort.Interface它要求类型实现三个方法: Len、 Swap 和 Less。
下面是一个泛型类型 SliceFn 的示例它为切片类型实现 sort.Interface
// SliceFn 为T类型切片实现 sort.Interface
type SliceFn[T any] struct {s []Tless func(T, T) bool
}func (s SliceFn[T]) Len() int {return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) {s.s[i], s.s[j] s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool {return s.less(s.s[i], s.s[j])
}2.2 不应该使用类型参数
不要用类型参数替换接口类型
众所周知Go有接口类型。接口类型允许一种通用编程。
例如广泛使用的io.Reader接口提供了一种通用机制用于从包含信息例如文件或产生信息例如随机数生成器的任何值读取数据。如果对某个类型的值只需要调用该值的方法则使用接口类型而不是类型参数。io.Reader易于阅读、高效且有效。不需要使用类型参数通过调用read方法从值中读取数据。
例如你可能会尝试将这里的第一个函数签名仅使用接口类型更改为第二个版本使用类型参数。
func ReadSome(r io.Reader) ([]byte, error)func ReadSome[T io.Reader](r T) ([]byte, error)不要做出那种改变。省略type参数使函数更容易编写更容易读取并且执行时间可能相同。
最后一点值得强调。虽然可以用几种不同的方式实现泛型而且随着时间的推移实现也会发生变化和改进但在许多情况下Go 1.18中使用的实现将处理类型为类型参数的值就像处理类型为接口类型的值一样。这意味着使用类型参数通常不会比使用接口类型快。因此不要为了速度而从接口类型更改为类型参数因为它可能不会运行得更快。
如果方法实现不同不要使用类型参数
在决定是否使用类型参数或接口类型时请考虑方法的实现。前面我们说过如果一个方法的实现对于所有类型都是相同的那么就使用一个类型参数。相反如果每种类型的实现都不同则使用接口类型并编写不同的方法实现不要使用类型参数。
例如从文件读取的实现与从随机数生成器读取的实现完全不同。这意味着我们应该编写两个不同的Read方法并使用像io.Reader这样的接口类型。
在适当的地方使用反射
Go具有运行时反射。反射允许一种泛型编程因为它允许你编写适用于任何类型的代码。
如果某些操作甚至必须支持没有方法的类型不能使用接口类型并且每个类型的操作都不同不能使用类型参数请使用反射。
encoding/json包就是一个例子。我们不想要求我们编码的每个类型都有MarshalJSON方法所以我们不能使用接口类型。但对接口类型的编码与对结构类型的编码不同因此我们不应该使用类型参数。相反该包使用反射。代码不简单但它有效。有关详细信息请参阅源代码。 参考文章
https://www.fansimao.com/1006524.html