ITPub博客

首页 > 数据库 > 数据库开发技术 > 3个例子,灵活掌握泛型

3个例子,灵活掌握泛型

原创 数据库开发技术 作者:皇子谈技术 时间:2024-06-03 08:29:00 0 删除 编辑

泛型看起来复杂,用起来实则很方便。

大家好,我是皇子。

Go 语言在1.18版本中正式引入了泛型(Generics)的支持,我觉得这是一个重要且实用的更新,它允许我们开发者编写可以应用于多种类型的代码,而无需重复实现相似逻辑。

Golang 的泛型除了可以处理基础类型,在处理自定义类型、集合操作、或是复杂的算法实现时特别有用。

接下来,我们将给出3个例子,每个例子都包含使用泛型处理和不使用泛型的写法,方便对比、灵活掌握 Golang 泛型。

例子:列表追加函数

检查并追加元素到切片中,如果该元素尚未存在,则追加,反之直接返回。

使用泛型的版本

package main

import (
"fmt"
)

// AppendIfNotPresent 是一个泛型函数,用于检查并追加元素到切片中,如果该元素尚未存在,则追加,反之直接返回。
// 定义了一个泛型函数,T 是类型参数,必须是可比较的(使用 comparable 约束)。
// 函数遍历切片 slice,检查 element 是否已存在。
// 如果存在,则直接返回原切片;如果不存在,则使用 append 函数追加元素到切片并返回。
func AppendIfNotPresent[T comparable](slice []T, element T) []T {
for _, v := range slice {
if v == element {
return slice // 元素已存在,不追加直接返回原切片
}
}
return append(slice, element) // 元素不存在,追加后返回
}

func main() {
slice := []int{1, 2, 3}
newSlice := AppendIfNotPresent(slice, 4)
fmt.Println(newSlice) // 输出: [1 2 3 4]

words := []string{"apple", "banana"}
newWords := AppendIfNotPresent(words, "cherry")
fmt.Println(newWords) // 输出: [apple banana cherry]
}

不使用泛型的版本

为了对比,如果我们不使用泛型来实现同样的功能,我们需要分别为不同类型的切片编写几乎相同的函数。

package main

import (
"fmt"
)

// AppendIntIfNotPresent 检查并追加整数到整数切片中。
func AppendIntIfNotPresent(slice []int, element int) []int {
for _, v := range slice {
if v == element {
return slice
}
}
return append(slice, element)
}

// AppendStringIfNotPresent 检查并追加字符串到字符串切片中。
func AppendStringIfNotPresent(slice []string, element string) []string {
for _, v := range slice {
if v == element {
return slice
}
}
return append(slice, element)
}

func main() {
slice := []int{1, 2, 3}
newSlice := AppendIntIfNotPresent(slice, 4)
fmt.Println(newSlice) // 输出: [1 2 3 4]

words := []string{"apple", "banana"}
newWords := AppendStringIfNotPresent(words, "cherry")
fmt.Println(newWords) // 输出: [apple banana cherry]
}

小结

在使用泛型例子中,我们仅需AppendIfNotPresent一个函数列表追加元素的功能。

而不使用泛型的情况下,需要分别为整数和字符串类型定义了 AppendIntIfNotPresent 和 AppendStringIfNotPresent 函数,这两个函数的逻辑几乎完全相同,只是处理的类型不同。这种做法导致代码重复,且每当需要处理新的类型时,都需要添加一个新的函数。

通过对比可以看出,泛型版本的代码更简洁、更易于维护,同时也提高了代码的复用性。

例子:键值对映射

假设我们想创建一个函数,该函数接收一个键值对的切片,并根据提供的映射函数转换键值对中的值。

使用泛型的版本

package main

import "fmt"

// MapKeyValue 是一个泛型函数,用于映射键值对切片中的值
func MapKeyValue[K comparable, V, R any](pairs []struct{ Key K; Value V }, mapFn func(V) R) []struct{ Key K; Value R } {
result := make([]struct{ Key K; Value R }, len(pairs))
for i, pair := range pairs {
result[i] = struct{ Key K; Value R }{Key: pair.Key, Value: mapFn(pair.Value)}
}
return result
}

func main() {
pairs := []struct{ Key string; Value int }{
{"one", 1},
{"two", 2},
{"three", 3},
}

// 将值乘以2
mappedPairs := MapKeyValue(pairs, func(v int) int { return v * 2 })
fmt.Println("Mapped pairs:", mappedPairs)
}

不使用泛型的版本:

如果不使用泛型,我们需要为每种特定类型值的映射逻辑编写单独的函数。例如,仅针对整数值的映射函数如下:

package main

import "fmt"

// KeyValuePair 定义一个非泛型的键值对结构体,值限定为int
type KeyValuePair struct {
Key string
Value int
}

// MapValuesToInt 非泛型版本的映射函数,专门处理int值的映射
func MapValuesToInt(pairs []KeyValuePair, mapFn func(int) int) []KeyValuePair {
result := make([]KeyValuePair, len(pairs))
for i, pair := range pairs {
result[i] = KeyValuePair{Key: pair.Key, Value: mapFn(pair.Value)}
}
return result
}

func main() {
pairs := []KeyValuePair{
{"one", 1},
{"two", 2},
{"three", 3},
}

// 将值乘以2
mappedPairs := MapValuesToInt(pairs, func(v int) int { return v * 2 })
fmt.Println("Mapped pairs:", mappedPairs)
}

小结

在这个非泛型版本中,我们只处理了KeyValuePair结构体中的值为int类型的情况。

如果需要处理其他类型的值,就需要为每种类型重复定义类似的映射函数。

而使用泛型的MapKeyValue函数则更加通用,可以处理任何类型的值映射,提高了代码的复用性和灵活性。

例子:自定义类型排序

假设我们有一个Person结构体,我们想要根据年龄或姓名对Person类型的切片进行排序。

使用泛型的例子

package main

import (
"fmt"
"sort"
)

// Person 结构体
type Person struct {
Name string
Age int
}

// ByField 定义一个泛型比较器
type ByField[T any] func(p1, p2 T) bool

// SortByField 使用泛型的排序函数
func SortByField[T any](s []T, less ByField[T]) {
sort.Slice(s, func(i, j int) bool {
return less(s[i], s[j])
})
}

func main() {
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}

// 按照年龄排序
SortByField(people, func(p1, p2 Person) bool {
return p1.Age < p2.Age
})
fmt.Println("Sorted by age:", people)

// 按照名字排序
SortByField(people, func(p1, p2 Person) bool {
return p1.Name < p2.Name
})
fmt.Println("Sorted by name:", people)
}

不使用泛型的例子

在Go泛型引入之前,为了实现类似功能,我们需要为每一种类型的排序逻辑编写单独的函数。

以下是如何分别为Person的年龄和姓名排序的代码:

package main

import (
"fmt"
"sort"
)

// Person 结构体保持不变

// ByAge 定义一个非泛型的年龄比较器
type ByAge []Person

func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }

// ByName 定义一个非泛型的名字比较器
type ByName []Person

func (n ByName) Len() int { return len(n) }
func (n ByName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n ByName) Less(i, j int) bool { return n[i].Name < n[j].Name }

func main() {
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}

// 按照年龄排序
sort.Sort(ByAge(people))
fmt.Println("Sorted by age:", people)

// 重置people以便再次排序
people = []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}

// 按照名字排序
sort.Sort(ByName(people))
fmt.Println("Sorted by name:", people)
}

小结

在这个非泛型的版本中,我们为Person切片分别实现了两个排序接口ByAge和ByName,每个接口都包含了Len, Swap, 和 Less 方法,这导致了代码的重复。

而泛型版本的SortByField函数则更加灵活和可重用,只需一个函数就能处理多种类型的排序逻辑。

其他语言关于泛型的说法

Java 泛型

Java 5 引入了泛型,这是一种允许在类、接口和方法中使用类型参数的机制。泛型提供了类型安全并且消除了强制类型转换的需求,同时保留了Java的静态类型检查特性。例如,在Java中,你可以这样定义一个泛型类:

public class Box<T> {
private T t;

public void set(T t) {
this.t = t;
}

public T get() {
return t;
}
}

Python 类型注解

Python 本身是一种动态类型语言,但Python 3.5开始引入了类型注解(Type Hints),这允许开发者为变量、函数参数和返回值提供类型提示。虽然这并不提供编译时的类型检查(Python仍然是动态类型检查),但它增强了代码的可读性和工具的支持,如编辑器的自动完成、类型检查工具(如mypy)等。Python的类型注解看起来像这样:

from typing import List

def multiply_all(numbers: List[int]) -> int:
total = 1
for number in numbers:
total *= number
return total

PHP 类型声明

PHP 7 开始引入了类型声明,允许在函数参数和返回值上指定类型。这提高了代码的清晰度和错误检测能力,尽管PHP仍然是动态类型语言。PHP的类型声明支持原生类型(如int、string等)和类名。从PHP 7.4起,也支持了返回类型和标量类型声明。示例如下:

function add(int $a, int $b): int {
return $a + $b;
}

尽管Python的类型注解(Type Annotations)和PHP的类型声明(Type Declarations)的特性提供了对类型的更严格控制和更好的文档化,但它们在概念上与Go或Java的泛型有所不同。

特别是,Python的类型注解和PHP的类型声明不改变语言的动态类型本质,也不提供编译时的泛型类型擦除和类型安全检查,这是Golang、Java泛型的核心特性之一。

关于泛型类型擦除

泛型类型擦除是指在编译时期,编译器移除代码中关于泛型类型的具体信息,替换为一个非泛型的等效形式,以便生成的字节码能在不直接支持泛型的运行环境中执行。

这个过程使得泛型代码能够在编译后与非泛型代码兼容,同时保持一定的类型安全。

Java是最典型的实施类型擦除的例证,它在编译时将泛型类型参数替换为它们的上限类型(通常是Object),并在必要时插入类型转换代码以维持类型安全。

对于Go语言的泛型实现并不完全依赖类型擦除机制。

Go采取了一种更为直接的方法,它在编译时生成特定于类型实例的代码,这意味着对于每一个使用的类型参数组合,都会生成相应的具体类型代码。

尽管Go泛型在某种程度上也涉及编译时的类型信息处理,但它的处理方式与Java的类型擦除有本质区别,更倾向于保持类型信息并在编译时生成特定类型的实现,从而在运行时保持类型精确性。


推荐阅读

腾讯三面,被拷打了

B站三面,挑战有点大

得物不错,可惜止步于二面

米哈游放弃了,五一先稳住


关于作者:一位热爱技术,并在职场与自媒体间探索的实践者,希望通过分享个人经验和见解,帮助更多人实现自我成长和价值。

如果您对我的公众号内容感兴趣,欢迎关注我。

也可以在下方添加我微信和一支烟花AI社区技术群,一起交流技术、职场心得、自媒体运营经验。欢迎~

原文作者:皇子谈技术;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2023-12-21

  • 博文量
    113
  • 访问量
    56539