写点什么

深度思考:为什么需要泛型?

作者:Barry Yan
  • 2022 年 10 月 07 日
    北京
  • 本文字数:3184 字

    阅读完需:约 10 分钟

深度思考:为什么需要泛型?

不知道大家平时在进行后端编程的时候有没有考虑过一个概念:泛型编程,就像面向对象、面向接口编程一样,很常用以致于用成为了大家广泛的习惯,在后端常用编程语言中,无论是 Java、C++都支持泛型编程,而在 2022 年的 3 月份,随着 Go1.18 稳定版本的发布,Go 自 1.18 版本起,也支持了“泛型(Generics)”这一特性,这同时也是 Go 语言在语法层面的一次重大改变。


虽然之前在使用 Java 进行编程时经常用到泛型,但是未曾思考过到底为什么需要泛型?没有泛型会怎样?泛型带来了什么作用?泛型的实现原理是怎样的?等等问题。


因为 Go1.18 版本发布已有几个月的时间,各个 IDE 也陆续支持 Go 语言泛型编码,因此也通过一些资料学习了 Go 语言泛型这个新特性,并且对此做了一些思考,想以一篇文章来向大家分享自己的思考经验和见解,同时也会以实际代码的方式使用 Java、Go 语言的泛型特性,剖析其原理,下面开始正文。

1 什么是泛型?

维基百科提到:最初泛型编程这个概念来自于缪斯·大卫和斯捷潘诺夫. 亚历山大合著的“泛型编程”一文。那篇文章对泛型编程的诠释是:“泛型编程的中心思想是对具体的、高效的算法进行抽象,以获得通用的算法,然后这些算法可以与不同的数据表示法结合起来,产生各种各样有用的软件”。说白了就是将算法与类型解耦,实现算法更广泛的复用。


在我看来,泛型是同接口类似,也是编程的一种规范也可以说是一种风格,泛型编程可以让开发者在编写代码时约束变量、容器、对象、结构体的类型,对类型清晰的掌握可以减少 bug 的产生,增强代码的可读性,让抽象变得更加具体和实用。基于泛型的程序,由于传入的参数不同,程序会实现不同的功能。这也被叫做一种多态现象,叫做参数化多态(Parametric Polymorphism)。

2 编程语言中泛型编程的实例

2.1 Java 泛型编程

请移步这篇文章《玩转Java泛型》

2.2 Go 泛型编程

package main
import "fmt"
type MyList[T any] struct { Items []Item[T]}
type Item[T any] struct { Index int Value T}
func (list *MyList[T]) AddItem(i T) { item := Item[T]{Value: i, Index: len(list.Items)} list.Items = append(list.Items, item)}
func (list *MyList[T]) GetItem(index int) T { l := list.Items var val T for i := range l { if l[i].Index == index { val = l[i].Value } } return val}
func (list *MyList[T]) Print() { for i := range list.Items { fmt.Println(list.Items[i]) }}
type MyHashMap[K comparable, V any] struct { Value map[K]V}
func (m *MyHashMap[K, V]) SetValue(k K, v V) { m.Value[k] = v}
func (m *MyHashMap[K, V]) GetValue(k K) V { return m.Value[k]}
func (m *MyHashMap[K, V]) Print() { for k := range m.Value { fmt.Println(k, m.Value[k]) }}
func main() { list := MyList[int]{} list.AddItem(1) list.AddItem(2) item := list.GetItem(7) list.Print() hashMap := MyHashMap[string, int]{map[string]int{"A": 1, "B": 2}} hashMap.SetValue("s", 2) value := hashMap.GetValue("s") hashMap.Print()}
复制代码


具体 Go 泛型编程内容可以看下这篇文章哈:《一文搞懂Go1.18泛型新特性》

3 为什么需要泛型?

回答这个问题之前,我们不妨思考下,在一些场景下如果没有泛型会怎样:


public class Main {
static class Score { String name; int num;
public Score(String name, int num) { this.name = name; this.num = num; } }
public static int getSum(List<Score> scores) { int sum = 0; for (Score score : scores) { sum += score.num; } return sum; }
public static void main(String[] args) { List<Score> scores = Arrays.asList( new Score("zs", 100), new Score("ls", 80), new Score("ww", 90.5) //编译不通过 ); int sum = getSum(scores); System.out.println(sum); }}
复制代码


没有泛型时解决上述问题:


public class Main {
static class Score { String name; int num;
public Score(String name, int num) { this.name = name; this.num = num; } }
static class Score2 { String name; float num;
public Score2(String name, float num) { this.name = name; this.num = num; } }
public static int getIntSum(List<Score> scores) { int sum = 0; for (Score score : scores) { sum += score.num; } return sum; }
public static float getFloatSum(List<Score2> scores) { float sum = 0; for (Score2 score : scores) { sum += score.num; } return sum; }
public static void main(String[] args) { List<Score> scores = Arrays.asList( new Score("zs", 100), new Score("ls", 80) );
List<Score2> scores2 = Arrays.asList( new Score2("zs", 89.5f), new Score2("ls", 80.5f) ); int sum = getIntSum(scores); float sum2 = getFloatSum(scores2); System.out.println(sum+sum2); }}
复制代码


接下来我们引入泛型:


public class Main {
static class Score<T> { String name; T num;
public Score(String name, T num) { this.name = name; this.num = num; } }
public static int getIntSum(List<Score<Integer>> scores) { int sum = 0; for (Score<Integer> score : scores) { sum += score.num; } return sum; }
public static float getFloatSum(List<Score<Float>> scores) { float sum = 0; for (Score<Float> score : scores) { sum += score.num; } return sum; }
public static void main(String[] args) { List<Score<Integer>> scores = Arrays.asList( new Score("zs", 100), new Score("ls", 80) );
List<Score<Float>> scores2 = Arrays.asList( new Score("zs", 89.5f), new Score("ls", 80.5f) ); int sum = getIntSum(scores); float sum2 = getFloatSum(scores2); System.out.println(sum+sum2); }}
复制代码


所以,使用泛型的原因:


  • 泛化

  • 类型安全

  • 消除强制类型转换

  • 向后兼容


图示:


4 总结泛型的实现原理

大多数静态类型语言的泛型实现都是在编译期进行,也就是编译的前端实现,主要的技术包括类型擦除、具体化和基于元编程等进行的,比如 Java 的泛型就是基于类型擦除实现,在编译前端进行类型检查即可,编译之后的字节码不管有没有泛型都是一样的,运行时也是如此。而 Go 语言的泛型实现则不同,Go 使用类似于具体化的方式实现泛型,就是在运行时使用类型信息,根据类型参数创建不同的具体类型的变量。


参考:


https://time.geekbang.org/column/article/485140


https://baike.baidu.com/item/%E6%B3%9B%E5%9E%8B/4475207?fr=aladdin


https://time.geekbang.org/column/article/283229

发布于: 刚刚阅读数: 4
用户头像

Barry Yan

关注

做兴趣使然的Hero 2021.01.14 加入

Just do it.

评论

发布
暂无评论
深度思考:为什么需要泛型?_10月月更_Barry Yan_InfoQ写作社区