泛型和元编程的模型:Java, Go, Rust, Swift, D等

本文探讨了不同编程语言如何处理泛型,从装箱和单态化两种基本方法出发,详细分析了Java、Go、Rust、Swift和D等语言的泛型实现机制。装箱通过类型擦除和接口实现动态行为,而单态化通过代码生成和类型检查确保效率。文章还涉及了接口、反射、动态类型语言、编译期函数等多个相关主题。
摘要由CSDN通过智能技术生成

在程序设计的时候,我们通常希望使用同样的数据结构或算法,就可以处理许多不同类型的元素,比如通用的List或只需要实现compare函数的排序算法。对于这个问题,不同的编程语言已经提出了各种各样的解决方案:从只是提供对特定目标有用的通用函数(如C,Go),到功能强大的图灵完备的通用系统(如Rust,C++)。在本文中,我将带你领略不同语言中的泛型系统以及它们是如何实现的。我将从C这样的不具备泛型系统的语言如何解决这个问题开始,然后分别展示其他语言如何在不同的方向上逐渐添加扩展,从而发展出各具特色的泛型系统。

泛型是元编程领域内通用问题的简单案例:编写可以生成其他程序的程序。我将描述三种不同的完全通用的元编程方法,看看它们是如何在泛型系统空的不同方向进行扩展:像Python这样的动态语言,像Template Haskell这样的过程宏系统,以及像Zig和Terra这样的阶段性编译。

概述

下图包含了本文讨论的所有语言的泛型系统,用以概述本文主要内容以及它们是如何结合在一起的。

基本想法


假设我们用一种没有泛型系统的语言进行编程,我们想实现一个通用的堆栈数据结构,它对任何数据类型都有效。困难在于我们写的每一个函数和类型定义都只对那些大小相同、复制方式相同、行为相同的数据有效。

如何解决这个问题?有两个基本的想法,一是想办法让所有数据类型在我们的数据结构中有同样的行为方式,二是对我们的数据结构进行多份拷贝,并稍作调整,以特定的方式处理每种数据类型。这两个想法构成了两大类解决泛型问题的基础方法,即"装箱 "和 "单态化"。

装箱是指我们把所有的东西都放在统一的 "盒子 "里,使它们的行为方式都一样。通常是通过在堆上分配内存,只在数据结构中放指针来实现的。我们可以让不同类型的指针有同样的行为方式,这样,同样的代码就可以处理所有的数据类型了。然而这种做法可能要付出额外的内存分配、动态查找和缓存丢失的代价。在C语言中,这相当于让你的数据结构存储void*指针,也需要将你的数据指针转换为void*或从void*进行类型转换(如果数据还没有在堆上,则在堆上分配)。

单态化是针对我们要处理的不同类型的数据,多次复制代码。这样每份代码都直接使用对应的数据结构和函数,而不需要任何动态查找。这样运行效率足够快,但代价是代码大小和编译时间的膨胀,因为同样的代码只要稍加调整就会被编译多次。在C语言中,这相当于在一个宏中定义你的整个数据结构,并为在使用该结构的地方调用该宏。

总的来说,装箱有利于缩短编译时间,但会损害运行时性能,而单态化会生成的代码运行期效率高,但需要额外的时间来编译和优化生成的代码。当然它们在如何扩展方面这方面也有所不同。装箱允许在运行时有更多的动态行为,而单态化则可以更灵活地处理通用代码的不同实例。另外值得注意的是,在一些大型程序中,单态化的性能优势可能会被额外生成的代码所带来的额外指令导致缓存未命中所抵消。

两个基础流派中的每一个流派都有很多方向可以扩展,以增加额外的能力或安全性,不同的语言已经将两者带入了非常有趣的方向。有些语言如Rust和C#甚至提供了这两种选择!

装箱

让我们以go语言为例:

type Stack struct {
  values []interface{}
}


func (this *Stack) Push(value interface{}) {
  this.values = append(this.values, value)
}


func (this *Stack) Pop() interface{} {
  x := this.values[len(this.values)-1]
  this.values = this.values[:len(this.values)-1]
  return x
}
使用装箱的语言示例。C(void*)、Go(interface{})、无泛型的Java(Object)、无泛型的Objective-C(id)

基于类型擦除装箱的泛型

这里有一些基础装箱的问题。

  • 根据语言的不同,我们经常需要在每次读写数据结构的时候,进行类型转换。

  • 很难阻止使用者将不同类型的元素放入数据结构中,这可能会导致运行时异常。

解决方法是在类型系统中增加泛型功能,同时在运行时仍然和以前一样完全使用基本装箱方法。这种方法通常被称为类型擦除,因为类型系统中的类型都被 "擦除 "了,都变成了同一类型(比如Object)。

Java和Objective-C一开始都是使用基础装箱,后来又增加了基于类型擦除的泛型功能,为了兼容,甚至使用了和以前完全一样的集合类型,但可以选择泛型参数。请看下面的例子,其来自维基百科上关于Java泛型的文章。

List v = new ArrayList();
v.add("test"); // A String that cannot be cast to an Integer
Integer i = (Integer)v.get(0); // Run time error


List<String> v = new ArrayList<String>();
v.add("test");
Integer i = v.get(0); // (type error) compilation-time error

具有统一表达方式的推断装箱泛型

OCaml将这个想法更进一步,采用统一的表示方式,没有需要额外装箱分配的基元类型(就像Java中int需要变成Integer才能进入ArrayList一样),因为所有的对象要么已经被装箱,要么用一个指针大小的整数表示,所以一切都是一个机器字。然而当垃圾收集器查看存储在通用结构中的数据时,它需要区分指针和整数,所以用1位(指针不会有这1位)来标记整数,只留下31位或63位的范围。

OCaml还有一个类型推理系统,所以你可以写一

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值