Golang - 第一眼看到的泛型
这篇文章是一个系列的一部分,在这个系列中,我尽力组织我的想法,围绕Go:它的范式和作为编程语言的可用性。我作为一个尊重Elegant Objects原则的Java程序员来写这篇文章。
Go 1.18 Beta 1刚刚发布:这些是我对这次发布的主要功能的初步印象:泛型。
语法
Go的语法拥有与Java非常相似的结构:
Go
// A function.
func Print[T any](t T) {
fmt.Printf("printing type: %T\n", t)
}
// A type.
type Tree[T any] struct {
left, right *Tree[T]
data T
}
Java
// A function.
public static <T> void print(T t) {
System.out.println("printing type: " + t.getClass().getName());
}
// A type.
class Tree<T> {
private Tree<T> left, right;
private T data;
}
有一点不同。Go要求类型参数明确地受到一个类型的约束(例如:T any ),而Java则不需要(T 本身就被隐含地推断为java.lang.Object )。在Go中不提供约束将导致类似以下的错误。
./prog.go:95:13: 语法错误:缺少类型约束
我怀疑区别在于Java的统一类型层次(每个东西都是一个java.lang.Object )。Go则没有这种模式。
类型转换
下面的编译错误让我很吃惊:
func print[T any](t T) {
switch t.(type) {
case string: fmt.Println("printing a string: ", t) // error: cannot use type switch on type parameter value t (variable of type T constrained by any)
}
}
...因为下面是合法的Go代码:
func print(t interface{}) {
switch t.(type) {
case string: fmt.Println("printing a string: ", t)
}
}
这似乎意味着any 并不是Robert Griesemer和Ian Lance Taylor在这次演讲中所声明的interface{} 的简单别名。在这个问题上,提出了在提案的早期草案中指出了这个理由。这在联盟类型参数上尤其令人惊讶。
func print[T int64|float64](t T) {
switch t.(type) { // error: cannot use type switch on type parameter value t (variable of type T constrained by int64|float64)
case int64: fmt.Println("printing an int64: ", t)
case float64: fmt.Println("printing a float64: ", t)
}
}
在看这个问题和相关问题的评论时,我得到的印象是,类型参数上的类型转换在未来有很大的可能性。只是在1.18中没有。要解决这个问题,可以把t 赋值给一个类型为interface{} 的变量,并对其进行类型转换。
同时,这里有Java中的相同功能(结合了Java 14中的切换表达式和Java 17中切换表达式的模式匹配(预览))。
public static <T> void print(T t) {
switch(t) {
case String s -> System.out.println("you sent string: " + s);
default -> System.out.println("you sent an unknown type: " + t.getClass().getName());
};
}
类型约束
在Go中,类型参数约束T any 表示T 不受任何特定接口的约束。换句话说,T 实现了interface{} (不完全是,见类型切换)。
在Go中,我们可以进一步约束T 的类型集,表示除any 以外的东西,例如:
// T is now constrained to int types.
type Tree[T int] struct {
left, right *Tree[T]
data T
}
等同于Java:
class Tree<T extends Integer> {
private Tree<T> left, right;
private T data;
}
在Go中,类型参数声明可以指定具体的类型(像Java),并且可以内联声明或引用:
// inlined
func PrintInt64[T int64](t T) {
fmt.Printf("%v\n", t)
}
// referenced
func PrintInt64[T Int64Type](t T) {
fmt.Printf("%v\n", t)
}
// reusable (like constraints.Integer)
type Bit64Type interface {
int64
}
Go的可重复使用的类型约束
Go的可重复使用的类型约束有点......奇怪。
以这个简单的接口为例,Tester :
package main
type Tester interface {
Test()
}
type myTester struct {}
func (m *myTester) Test() {}
func test(t Tester) {
t.Test()
}
func main() {
test(&myTester{})
}
...然后添加一个类型约束:
package main
type Tester interface {
int64
Test()
}
type myTester struct {}
func (m *myTester) Test() {}
func test(t Tester) { // ERROR: interface contains type constraints
t.Test()
}
func main() {
test(&myTester{})
}
不要紧,int64 并没有实现Tester - 这个错误意味着参数不能是包含类型约束的接口类型。即使两种类型都实现了相同的方法,也可以证明这一点。
package main
type Tester interface {
*myTester1
Test()
}
type myTester1 struct {}
func (m *myTester1) Test() {}
type myTester2 struct {}
func (m *myTester2) Test() {}
func test(t Tester) { // ERROR: interface contains type constraints
t.Test()
}
func main() {
test(&myTester1{})
}
我的惊讶源于声明类型约束时对interface 结构的重复使用。将类型约束添加到一个接口中,完全改变了它的性质,并将其用途限制在通用类型参数声明中。这对于那些习惯于Go的结构化类型系统的老手来说,会感到很奇怪。
联合类型
Go 和 Java 都支持联合类型作为类型参数,但它们的方式非常不同。
Go中的联合类型
Go只允许具体类型的联合类型。
// GOOD
func PrintInt64OrFloat64[T int64|float64](t T) {
fmt.Printf("%v\n", t)
}
type someStruct {}
// GOOD
func PrintInt64OrSomeStruct[T int64|*someStruct](t T) {
fmt.Printf("t: %v\n", t)
}
// BAD
func handle[T io.Closer | Flusher](t T) { // error: cannot use io.Closer in union (interface contains methods)
err := t.Flush()
if err != nil {
fmt.Println("failed to flush: ", err.Error())
}
err = t.Close()
if err != nil {
fmt.Println("failed to close: ", err.Error())
}
}
type Flusher interface {
Flush() error
}
Go的联合类型(在他们的提案中被称为类型集)背后的主要动机似乎是为了实现通用操作,在支持这些操作的原始类型上使用诸如< (来源:提案)。
类型集的其他例子见于constraints 包。
令我惊讶的是,有可能声明一个不可能满足的可重用类型约束。
package main
type Tester interface {
int // int does not implement method `Test()`
Test()
}
func test[T Tester](t T) {
t.Test()
}
func main() {
test(two(2)) // ERROR: two does not implement Tester (possibly missing ~ for int in constraint Tester)
}
type two int
func (t two) Test() {}
这个错误给了我们一个线索--使用一个近似的约束元素。
package main
type Tester interface {
~int // any type alias whose underlying type is an `int` will make do
Test()
}
func test[T Tester](t T) {
t.Test()
}
func main() {
test(two(2)) // works
}
type two int
func (t two) Test() {}
近似约束元素是Go在1.18中所得到的最接近协整的约束。
Java中的联合类型
Java只允许接口类型的联合类型,或者在非接口类型和接口类型之间的联合类型。
// GOOD
public static class Tree<T extends Closeable & Flushable> {
private Tree<T> left, right;
private T data;
}
// GOOD
public static <T extends Number & Closeable> void printNumberAndClose(T t) {
System.out.println(t.intValue());
try {
t.close();
} catch (IOException e) {
System.out.println("io exception: " + e.getMessage());
}
}
// BAD
public static <T extends Integer & Float> void printIntegerOrFloat(T t) { // error: interface expected here
System.out.println(t.toString()); // error: ambiguous call
System.out.println(t.isNaN());
}
正如& ("和")运算符所暗示的,Java中的联合类型的类型参数必须满足所有引用的 "接口"。
public class Main {
public static void main(String... args) {
printNumberAndClose(new CloseableNumber());
printNumberAndClose(12); // ERROR: no instance(s) of type variable(s) exist so that Integer conforms to Closeable
printNumberAndClose(new InputStreamReader(System.in)); // ERROR: no instance(s) of type variable(s) exist so that InputStreamReader conforms to Number
}
static class CloseableNumber extends Number implements Closeable {
// implements methods from Closeable
// implements abstract methods from Number
}
public static <T extends Number & Closeable> void printNumberAndClose(T t) {
System.out.println(t.intValue());
try {
t.close();
} catch (IOException e) {
System.out.println("io exception: " + e.getMessage());
}
}
}
与上面的Go例子类似,尽管考虑到java.lang.Integer 是一个final 类,联合类型的条件不可能满足,下面的printNumberAndClose 还是会被编译。
public class Main {
public static void main(String... args) {
printNumberAndClose(new CloseableNumber(0));
printNumberAndClose(12); // ERROR: no instance(s) of type variable(s) exist so that Integer conforms to Closeable
printNumberAndClose(new InputStreamReader(System.in)); // ERROR: no instance(s) of type variable(s) exist so that InputStreamReader conforms to Integer
}
static class CloseableNumber extends Integer implements Closeable { // ERROR: Cannot inherit from final 'java.lang.Integer'
CloseableNumber(int n) {
super(n);
}
// implements methods from Closeable
}
public static <T extends Integer & Closeable> void printNumberAndClose(T t) {
System.out.println(t.intValue());
try {
t.close();
} catch (IOException e) {
System.out.println("io exception: " + e.getMessage());
}
}
}
最坏的情况是,没有人会使用printNumberAndClose 。
差异
Go的建议不包括协方差和反方差。
Java通过使用通配符支持这两者。
// covariance
private static void sort(List<? extends Number≶ list) {
// sort
}
// contravariance
private static void reverse(List<? super Number≶ list) {
// reverse
}
我在之前的一篇博文中总结了Java的方差。
最后的思考
尽管实现了Java泛型功能的一个子集,尽管可重复使用的类型集很黑,但Go的提议很有说服力,值得在1.18版本推出后在实际生产代码中试用。
泛型是Go中一个被严重遗漏的功能。我期待着随着地图/reduce算法和数据结构的完全删除,能够大大节省代码行数。
