Golang - 初识泛型的详细指南

108 阅读6分钟

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算法和数据结构的完全删除,能够大大节省代码行数。