仓颉-泛型
泛型概述
在仓颉编程语言中,泛型指的是参数化类型,参数化类型是一个在声明时未知并且需要在使用时指定的类型。类型声明与函数声明可以是泛型的。最为常见的例子就是 Array<T>、Set<T> 等容器类型。
在仓颉中,function、class、interface、struct 与 enum 的声明都可以声明类型形参,也就是说它们都可以是泛型的。
为了方便讨论,定义如下几个常用的术语:
- 类型形参:一个类型或者函数声明可能有一个或者多个需要在使用处被指定的类型,这些类型就被称为类型形参。在声明形参时,需要给定一个标识符,以便在声明体中引用。
- 类型变元:在声明类型形参后,当通过标识符来引用这些类型时,这些标识符被称为类型变元。
- 类型实参:当在使用泛型声明的类型或函数时指定了泛型参数,这些参数被称为类型实参。
- 类型构造器:一个需要零个、一个或者多个类型作为实参的类型称为类型构造器。
类型形参在声明时一般在类型名称的声明或者函数名称的声明后,使用尖括号 <...> 括起来。例如一个泛型的列表可声明为:
1 | class List<T> { |
其中 List<T> 中的 T 被称为类型形参。对于 elem: Option<T> 中对 T 的引用称为类型变元,同理 tail: Option<List<T>> 中的 T 也称为类型变元。函数 sumInt 的参数中 List<Int64> 的 Int64 被称为 List 的类型实参。 List 就是类型构造器,List<Int64> 通过 Int64 类型实参构造出了一个类型 Int64 的列表类型。
泛型函数
如果一个函数声明了一个或多个类型形参,则将其称为泛型函数。语法上,类型形参紧跟在函数名后,并用 <> 括起,如果有多个类型形参,则用 , 分离。
全局泛型函数
在声明全局泛型函数时,只需要在函数名后使用尖括号声明类型形参,然后就可以在函数形参、返回类型及函数体中对这一类型形参进行引用。例如 id 函数定义为:
1 | func id<T>(a: T): T { |
其中 (a: T) 是函数声明的形参,其中使用到了 id 函数声明的类型形参 T,并且在 id 函数的返回类型使用。
再比如另一个复杂的例子,定义如下一个泛型函数 composition,该函数声明了 3 个类型形参,分别是 T1, T2, T3,其功能是把两个函数 f: (T1) -> T2, g: (T2) -> T3 复合成类型为 (T1) -> T3 的函数。
1 | func composition<T1, T2, T3>(f: (T1) -> T2, g: (T2) -> T3): (T1) -> T3 { |
因为可以被用来复合的函数是任意类型,例如可以是 (Int32) -> Bool, (Bool) -> Int64 的复合,也可以是 (Int64) -> Rune, (Rune) -> Int8 的复合,所以才需要使用泛型函数。
1 | func times2(a: Int64): Int64 { |
这里,复合两个 (Int64) -> Int64 的函数,将 9 先乘以 2,再加 10,结果会是 28。
1 | 28 |
局部泛型函数
局部函数也可以是泛型函数。例如泛型函数 id 可以嵌套定义在其他函数中:
1 | func foo(a: Int64) { |
这里由于 id 的单位元性质,函数 id<Int64> ~> double 和 double ~> id<Int64> 是等价的,结果是 true。
1 | true |
泛型成员函数
class、struct、enum 与 interface 的成员函数可以是泛型的。例如:
1 | class A { |
程序输出的结果为:
1 | 10 |
在为类型使用 extend 声明进行扩展时,扩展中的函数也可以是泛型的,例如可以为 Int64 类型增加一个泛型成员函数:
1 | extend Int64 { |
程序输出的结果将为:
1 | 12 |
静态泛型函数
interface、class、struct、enum 与 extend 中可以定义静态泛型函数,例如下例 ToPair class 中从 ArrayList 中返回一个元组:
1 | import std.collection.* |
泛型接口
泛型可以用来定义泛型接口,以标准库中定义的 Iterable 为例,它的成员函数 iterator 需要返回一个 Iterator 类型,这一类型是一个容器的遍历器。 Iterator 是一个泛型接口,Iterator 内部有一个从容器类型中返回下一个元素的 next 成员函数,next 成员函数返回的类型是一个需要在使用时指定的类型,所以 Iterator 需要声明泛型参数。
1 | public interface Iterable<E> { |
泛型类
泛型接口中介绍了泛型接口的定义和使用,本节介绍泛型类的定义和使用。如 Map 的键值对就是使用泛型类来定义的。
Map 类型中的键值对 Node 类型就可以使用泛型类来定义:
1 | public open class Node<K, V> where K <: Hashable & Equatable<K> { |
由于键与值的类型有可能不相同,且可以为任意满足条件的类型,所以 Node 需要两个类型形参 K 与 V ,K <: Hashable, K <: Equatable<K> 是对于键类型的约束,意为 K 要实现 Hashable 与 Equatable<K> 接口,也就是 K 需要满足的条件。对于泛型约束,详见泛型约束章节。
由于泛型类的静态成员变量的内存是共享的,因此,静态成员变量或属性的类型声明和表达式中不能引用类型参数或包含未实例化泛型类型表达式。另外,静态变量或属性初始化表达式中不能调用泛型类的静态成员函数或属性。
1 | class A<T> {} |
泛型结构体
struct 类型的泛型与 class 是类似的,下面可以使用 struct 定义一个类似于二元元组的类型:
1 | struct Pair<T, U> { |
程序输出的结果为:
1 | hello |
在 Pair 中提供了 first 与 second 两个函数来取得元组的第一个与第二个元素。
泛型枚举
在仓颉编程语言的泛型 enum 类型设计中,Option 类型是一个典型的示例,关于 Option 详细描述请参见 Option 类型章节。 Option 类型用于表示在某一类型上的值可能是个空的值。这样,Option 就可以用来表示在某种类型上计算的失败。这里是何种类型上的失败是不确定的,所以很明显,Option 是一个泛型类型,需要声明类型形参。
1 | package std.core // `Option` is defined in std.core. |
可以看到,Option<T> 分成两种情况,一种是 Some(T),用来表示一个有值的返回结果,另一种是 None 用来表示一个空的结果。其中的 getOrThrow 函数会是将 Some(T) 内部的值返回出来的函数,返回的结果就是 T 类型,而如果参数是 None,那么直接抛出异常。
例如:如果想定义一个安全的除法,因为在除法上的计算是可能失败的。如果除数为 0,那么返回 None ,否则返回一个用 Some 包装过的结果:
1 | func safeDiv(a: Int64, b: Int64): Option<Int64> { |
这样,在除数为 0 时,程序运行的过程中不会因除以 0 而抛出算术运算异常。
泛型类型的子类型关系
实例化后的泛型类型间也有子类型关系。例如:
1 | interface I<X, Y> { } |
根据 class C<Z> <: I<Z, Z> { },便知 C<Bool> <: I<Bool, Bool> 以及 C<D> <: I<D, D> 等。可以解读为“于所有的(不含类型变元的) Z 类型,都有 C<Z> <: I<Z, Z> 成立”。
但是对于下列代码:
1 | open class C { } |
I<D> <: I<C> 是不成立的(即使 D <: C 成立),这是因为在仓颉语言中,用户定义的类型构造器在其类型参数处是不型变的。
型变的具体定义为:如果 A 和 B 是(实例化后的)类型,T 是类型构造器,设有一个类型参数 X(例如 interface T<X>),那么
- 如果
T(A) <: T(B)当且仅当A = B,则T是不型变的。 - 如果
T(A) <: T(B)当且仅当A <: B,则T在X处是协变的。 - 如果
T(A) <: T(B)当且仅当B <: A,则T在X处是逆变的。
因为现阶段的仓颉中,所有用户自定义的泛型类型在其所有的类型变元处都是不变的,所以给定 interface I<X> 和类型 A、B,只有 A = B,才能得到 I<A> <: I<B>;反过来,如果知道了 I<A> <: I<B>,也可推出 A = B(内建类型除外:内建的元组类型对其每个元素类型来说,都是协变的;内建的函数类型在其入参类型处是逆变的,在其返回类型处是协变的。)
注意:
class以外的类型实现接口,该类型和该接口之间的子类型关系不能作为协变和逆变的依据。
不型变限制了一些语言的表达能力,但也避免了一些安全问题,例如“协变数组运行时抛异常”的问题。
类型别名
当某个类型的名字比较复杂或者在特定场景中不够直观时,可以选择使用类型别名的方式为此类型设置一个别名。
1 | type I64 = Int64 |
类型别名的定义以关键字 type 开头,接着是类型的别名(如上例中的 I64),然后是等号 =,最后是原类型(即被取别名的类型,如上例中的 Int64)。
只能在源文件顶层定义类型别名,并且原类型必须在别名定义处可见。例如,下例中 Int64 的别名定义在 main 中将报错,LongNameClassB 类型在为其定义别名时不可见,同样报错。
1 | main() { |
一个(或多个)类型别名定义中禁止出现(直接或间接的)循环引用。
1 | type A = (Int64, A) // Error, 'A' refered itself |
类型别名并不会定义一个新的类型,它仅仅是为原类型定义了另外一个名字,它有如下几种使用场景:
-
作为类型使用,例如:
1
2
3type A = B
class B {}
var a: A = B() // Use typealias A as type B -
当类型别名实际指向的类型为 class、struct 时,可以作为构造器名称使用:
1
2
3type A = B
class B {}
func foo() { A() } // Use type alias A as constructor of B -
当类型别名实际指向的类型为 class、interface、struct 时,可以作为访问内部静态成员变量或函数的类型名:
1
2
3
4
5
6
7
8
9type A = B
class B {
static var b : Int32 = 0;
static func foo() {}
}
func foo() {
A.foo() // Use A to access static method in class B
A.b
} -
当类型别名实际指向的类型为 enum 时,可以作为 enum 声明的构造器的类型名:
1
2
3
4
5
6enum TimeUnit {
Day | Month | Year
}
type Time = TimeUnit
var a = Time.Day
var b = Time.Month // Use type alias Time to access constructors in TimeUnit
需要注意的是,当前用户自定义的类型别名暂不支持在类型转换表达式中使用,参考如下示例:
1 | type MyInt = Int32 |
泛型别名
类型别名也是可以声明类型形参的,但是不能对其形参使用 where 声明约束,对于泛型变元的约束会在后面给出解释。
当一个泛型类型的名称过长时,可以使用类型别名来为其声明一个更短的别名。例如,有一个类型为 RecordData ,可以把他用类型别名简写为 RD :
1 | struct RecordData<T> { |
在使用时就可以用 RD<Int32> 来代指 RecordData<Int32> 类型。
泛型约束
泛型约束的作用是在 function、class、interface、struct、enum 声明时明确泛型形参所具备的操作与能力。只有声明了这些约束才能调用相应的成员函数。在很多场景下泛型形参是需要加以约束的。以 id 函数为例:
1 | func id<T>(a: T) { |
开发者唯一能做的事情就是将函数形参 a 这个值返回,而不能进行 a + 1,println("${a}") 等操作,因为它可能是一个任意的类型,比如 (Bool) -> Bool,这样就无法与整数相加,同样因为是函数类型,也不能通过 println 函数来输出在命令行上。而如果这一泛型形参上有了约束,那么就可以做更多操作了。
约束大致分为接口约束与 class 类型约束。语法为在函数、类型的声明体之前使用 where 关键字来声明,对于声明的泛型形参 T1, T2,可以使用 where T1 <: Interface, T2 <: Class 这样的方式来声明泛型约束,同一个类型变元的多个约束可以使用 & 连接。例如:where T1 <: Interface1 & Interface2。
仓颉中的 println 函数能接受类型为字符串的参数。如果需要把一个泛型类型的变量转为字符串后打印在命令行上,可以对这个泛型类型变元加以约束,这个约束是 core 中定义的 ToString 接口,显然它是一个接口约束:
1 | package std.core // `ToString` is defined in core. |
这样就可以利用这个约束,定义一个名为 genericPrint 的函数:
1 | func genericPrint<T>(a: T) where T <: ToString { |
结果为:
1 | 10 |
如果 genericPrint 函数的类型实参没有实现 ToString 接口,那么编译器会报错。例如传入一个函数做为参数时:
1 | func genericPrint<T>(a: T) where T <: ToString { |
如果对上面的文件进行编译,那么编译器会抛出泛型类型参数不满足约束的错误。因为 genericPrint 函数的泛型的类型实参不满足约束 (Int64) -> Int64 <: ToString。
除了上述通过接口来表示约束,还可以使用 class 类型来约束一个泛型类型变元。例如:当要声明一个动物园类型 Zoo<T>,但是需要这里声明的类型形参 T 受到约束,这个约束就是 T 需要是动物类型 Animal 的子类型, Animal 类型中声明了 run 成员函数。这里声明两个子类型 Dog 与 Fox 都实现了 run 成员函数,这样在 Zoo<T> 的类型中,就可以对于 animals 数组列表中存放的动物实例调用 run 成员函数:
1 | import std.collection.* |
程序的输出为:
1 | dog run |
注意:
泛型变元的约束只能是具体的 class 类型或 interface,且变元如果存在多个 class 类型的上界时,它们必须在同一继承链路上。