泛型

go

泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。ーー换句话说,在编写某些代码或数据结构时先不提供值的类型,而是之后再提供。

这个函数只能接收[]int类型的参数,如果我们想支持[]float64类型的参数,我们就需要再定义一个reverseFloat64Slice函数。

一遍一遍地编写相同的功能是低效的,实际上这个反转切片的函数并不需要知道切片中元素的类型,但为了适用不同的类型我们把一段代码重复了很多遍。

泛型实现

泛型为Go语言添加了三个新的重要特性:

  1. 函数和类型的类型参数。
  2. 将接口类型定义为类型集,包括没有方法的类型。
  3. 类型推断,它允许在调用函数时在许多情况下省略类型参数。
func min[T int | float64](a, b T) T {
if a <= b {
return a
}
return b
}


m1 := min[int](1, 2) // 1

m2 := min[float64](-0.1, -0.2) // -0.2

//向 min 函数提供类型参数(在本例中为int和float64)称为实例化( instantiation )。

//首先,编译器在整个泛型函数或类型中将所有类型形参(type parameters)替换为它们各自的类型实参(type arguments)。
//其次,编译器验证每个类型参数是否满足相应的约束。

fmin := min[float64] // 类型实例化,编译器生成T=float64的min函数
m2 = fmin(1.2, 2.3) // 1.2

类型参数的使用

类型参数的使用

除了函数中支持使用类型参数列表外,类型也可以使用类型参数列表。

type Slice[T int | string] []T

type Map[K int | string, V float32 | float64] map[K]V

type Tree[T interface{}] struct {
left, right *Tree[T]
value T
}

要使用泛型类型,必须进行实例化。Tree[string]是使用类型实参string实例化 Tree 的示例。

var stringTree Tree[string]

类型约束

//作为类型约束使用的接口类型可以事先定义并支持复用。

// 类型约束字面量,通常外层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
}

类型集

Go1.18开始接口类型的定义也发生了改变,由过去的接口类型定义方法集(method set)变成了接口类型定义类型集(type set)。也就是说,接口类型现在可以用作值的类型,也可以用作类型约束。

type V interface {
int | string | bool
}

上面的代码就定义了一个包含 intstringbool 类型的类型集。

在 Go 1.18中,接口既可以像以前一样包含方法和嵌入式接口,但也可以嵌入非接口类型、类型并集和基础类型的集合。

当用作类型约束时,由接口定义的类型集精确地指定允许作为相应类型参数的类型。

  • |符号

    T1 | T2表示类型约束为T1和T2这两个类型的并集,例如下面的Integer类型表示由SignedUnsigned组成。

    type Integer interface {
    Signed | Unsigned
    }
  • ~符号

    ~T表示所以底层类型是T的类型,例如~string表示所有底层类型是string的类型集合。

    type MyString string  // MyString的底层类型是string

    注意:~符号后面只能是基本类型。

接口作为类型集是一种强大的新机制,是使类型约束能够生效的关键。目前,使用新语法表的接口只能用作类型约束。

any

空接口在类型参数列表中很常见,在Go 1.18引入了一个新的预声明标识符,作为空接口类型的别名。

// src/builtin/builtin.go

type any = interface{}

由此,我们可以使用如下代码:

func foo[S ~[]E, E any]() {
// ...
}

类型推断

最后一个新的主要语言特征是类型推断。从某些方面来说,这是语言中最复杂的变化,但它很重要,因为它能让人们在编写调用泛型函数的代码时更自然。

在许多情况下,编译器可以从普通参数推断 T 的类型实参。这使得代码更短,同时保持清晰。

var a, b, m float64

m = min(a, b) // 无需指定类型实参

约束类型推断

Point 类型有方法 但是泛型后无法找到String()方法,需要声明一下Point类型

// 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
}


type Point []int32

func (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类型。这源于泛型代码的编写方式,但这不是我们想要的。

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
}

总之,如果你发现自己多次编写完全相同的代码,而这些代码之间的唯一区别就是使用的类型不同,这个时候你就应该考虑是否可以使用类型参数。

泛型和接口类型之间并不是替代关系,而是相辅相成的关系。泛型的引入是为了配合接口的使用,让我们能够编写更加类型安全的Go代码,并能有效地减少重复代码。

这意味着使用类型参数通常不会比使用接口类型快。因此,不要为了速度而从接口类型更改为类型参数,因为它可能不会运行得更快

Go反射 泛型

Go具有运行时反射。反射允许一种泛型编程,因为它允许你编写适用于任何类型的代码。

如果某些操作甚至必须支持没有方法的类型(不能使用接口类型),并且每个类型的操作都不同(不能使用类型参数),请使用反射。

encoding/json包就是一个例子。我们不想要求我们编码的每个类型都有MarshalJSON方法,所以我们不能使用接口类型。但对接口类型的编码与对结构类型的编码不同,因此我们不应该使用类型参数。相反,该包使用反射。代码不简单,但它有效

最后,关于何时使用泛型的讨论可以简化为一个简单的指导原则。

如果您发现自己多次编写完全相同的代码,其中副本之间的唯一区别是代码使用不同的类型,请考虑是否可以使用类型参数。

另一种说法是,在注意到要多次编写完全相同的代码之前,应该避免使用类型参数。

python

Python 是一种动态类型语言,它的变量类型是在运行时动态推断的。与其他静态类型语言不同,Python 并没有直接的泛型类型。但是,可以通过使用类型变量来实现类似泛型的效果.

在 Python 中,我们可以使用类型提示(Type Hints)和 TypeVar 两个模块来模拟泛型。


from typing import List, TypeVar

T = TypeVar('T')


def print_list(lst: List[T]) -> None:
for item in lst:
print(item)

int_list = [1, 2, 3, 4, 5]
print_list[int](int_list)

str_list = ['apple', 'banana', 'cherry']
print_list[str](str_list)


挺简单的223333333

除了类型提示和类型变量之外,Python 还提供了一些其他的泛型工具和库,例如 typing 模块中的 UnionOptional 类型,以及第三方库如 Genericsdataclasses 等。这些工具可以帮助开发人员更好地进行类型检查和处理,提高代码的可读性和可维护性。

另外,Python 的泛型机制虽然比其他语言灵活,但也存在一些限制和缺陷。例如,Python 的类型提示只是一种静态检查,运行时仍然可以接受不同的类型,这可能导致类型错误和运行时异常。此外,在使用泛型时需要谨慎处理类型转换和类型推断等问题,避免出现意外行为和难以调试的问题。

typing 模块

typing 模块是 Python 3.5 引入的,它为类型提示提供了很多有用的工具和注释。

该模块提供了许多类型提示,例如:

  • List[T]:代表一个由类型 T 组成的列表。
  • Tuple[T1, T2, ..., Tn]:代表一个由类型 T1、T2、…、Tn 组成的元组。
  • Dict[Tkey, Tvalue]:代表一个由键值对组成的字典,其中键的类型为 Tkey,值的类型为 Tvalue。
  • Set[T]:代表一个由类型 T 组成的集合。
  • Union[T1, T2, ..., Tn]:代表类型 T1、T2、…、Tn 中的任何一个类型。
from typing import List

def average(numbers: List[float]) -> float:
return sum(numbers) / len(numbers)

当然,这只是类型提示的一部分,实际上 Python 解释器并不会检查这些类型。不过,这些类型提示对于代码维护和可读性来说是很有帮助的。

C++

模板(Template)

C++ 的泛型是通过模板(Template)实现的。模板是一种代码生成机制,可以在编译时根据参数类型自动生成代码。C++ 模板支持函数模板和类模板两种

函数模板
template <typename T>
void swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}


类模板
template <typename T>
class Stack {
public:
void push(T val);
T pop();
private:
vector<T> data;
};

我们可以使用泛型来实现一个通用的排序算法,可以接受任何类型的数组,并按照一定的规则进行排序。比如

#include <iostream>
#include <vector>
#include <algorithm>

template<typename T>
void printArray(std::vector<T> arr) {
for (auto x : arr) {
std::cout << x << " ";
}
std::cout << std::endl;
}

template<typename T>
void bubbleSort(std::vector<T>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
std::swap(arr[j], arr[j + 1]);
}
}
}
}

int main() {
std::vector<int> arr1 = {3, 2, 1, 5, 4};
std::vector<double> arr2 = {3.14, 2.71, 1.41, 4.0, 1.73};
std::vector<std::string> arr3 = {"hello", "world", "foo", "bar", "baz"};

bubbleSort(arr1);
bubbleSort(arr2);
bubbleSort(arr3);

printArray(arr1);
printArray(arr2);
printArray(arr3);

return 0;
}

函数模板 高级用法

  1. 模板特化(Template Specialization):对于某些特定的类型,需要使用不同于模板定义的实现,这时候可以使用模板特化来实现。

例如,以下代码定义了一个函数模板 print,用于输出传入的参数,当模板参数为 int 类型时,需要使用特殊的实现:

template <typename T>
void print(T val) {
std::cout << val << std::endl;
}

template <>
void print<int>(int val) {
std::cout << "The int value is: " << val << std::endl;
}

2.可变参数模板(Variadic Templates):函数模板可以接受任意数量的参数,这个数量在编译期是不确定的。可变参数模板就是指能够接受可变数量参数的函数模板。

例如,以下代码定义了一个可变参数模板 print,可以输出任意数量的参数:

cppCopy codetemplate <typename T>
void print(T arg) {
std::cout << arg << std::endl;
}

template <typename T, typename... Args>
void print(T arg, Args... args) {
std::cout << arg << ", ";
print(args...);
}

3.SFINAE(Substitution Failure Is Not An Error):这是一种编译期技术,用于在编译期对模板参数进行过滤,使得只有符合条件的模板参数才能够使用模板函数。

例如,以下代码定义了一个函数模板 foo,使用了 SFINAE 技术,只有当模板参数是一个整型类型时才会编译通过

cppCopy codetemplate <typename T>
typename std::enable_if<std::is_integral<T>::value>::type
foo(T t) {
std::cout << "Integral type" << std::endl;
}

template <typename T>
typename std::enable_if<!std::is_integral<T>::value>::type
foo(T t) {
std::cout << "Not integral type" << std::endl;
}

类模板 高级用法

  1. 模板特化(Template specialization):有时候,针对某些特定的类型,可以为类模板提供专门的实现,这被称为模板特化。例如:
c++Copy codetemplate<typename T>
class myclass {
public:
void foo() { std::cout << "generic implementation" << std::endl; }
};

template<>
class myclass<int> {
public:
void foo() { std::cout << "specialized implementation for int" << std::endl; }
};

int main() {
myclass<double> obj1;
obj1.foo(); // generic implementation

myclass<int> obj2;
obj2.foo(); // specialized implementation for int
}
  1. 模板嵌套(Template nesting):在类模板中定义另一个类模板或函数模板。例如:
c++Copy codetemplate<typename T>
class myclass {
public:
template<typename U>
class nested_class {
public:
void foo() { std::cout << "template nesting" << std::endl; }
};
};

int main() {
myclass<int>::nested_class<double> obj;
obj.foo(); // template nesting
}
  1. 模板模板参数(Template template parameter):允许将模板作为模板参数传递。例如:
c++Copy codetemplate<typename T>
class myclass {
public:
void foo() { std::cout << "generic implementation" << std::endl; }
};

template<template<typename> class Container>
void bar() {
Container<int> obj;
obj.foo();
}

int main() {
bar<myclass>(); // generic implementation
}