NOTE阅读文档版本:
语言规约 Cangjie-0.53.18-Spec
具体开发指南 Cangjie-LTS-1.0.4
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证
有条件当然可以直接 配置 Canjie-SDK
WARNING博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅
且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验
WARNING在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解
所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释
此样式内容, 表示文档原文内容
泛型
泛型类型的子类型关系 **
实例化后的泛型类型间也有子类型关系
例如:
interface I<X, Y> { }class C<Z> <: I<Z, Z> { }根据
class C<Z> <: I<Z, Z> { },便知C<Bool> <: I<Bool, Bool>以及C<D> <: I<D, D>等可以解读为“于所有的(不含类型变元的)
Z类型,都有C<Z> <: I<Z, Z>成立”但是对于下列代码:
open class C { }class D <: C { }interface I<X> { }
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以外的类型实现接口,该类型和该接口之间的子类型关系不能作为协变和逆变的依据不型变限制了一些语言的表达能力,但也避免了一些安全问题,例如“协变数组运行时抛异常”的问题
仓颉泛型的父子类型关系, 与一个概念密切相关: 型变
关于型变 仓颉规定, 针对自定义类型Type, 如果存在类型形参X, 以及另外的已实例化类型A和B:
-
如果 有且仅有
A = B时, 才存在Type(A) <: Type(B), 那么 可称Type是不型变的 -
如果 有且仅有
A <: B时, 才存在Type(A) <: Type(B), 那么 可称Type是协变的 -
如果 有且仅有
B <: A时, 才存在Type(A) <: Type(B), 那么 可称Type是逆变的
同时仓颉规定, 现阶段, 所有用户自定义类型 都是不型变的
这也就意味着, 如果类型形参不同, 那么泛型自定义类型之间不可能存在父子类型关系
TIP文档描述, 内建类型除外:
内建元组类型 对其每个元素类型, 都是协变的
内建函数类型 对其形参列表类型是逆变的, 对其返回类型是协变的
这表示, 元组类型, 只要对应位置元素类型具有同向的父子类型关系, 那么元组类型之间就具有相同的父子类型关系
而 函数类型, 只要 参数列表每个形参对应位置类型 具有同向的父子类型关系, 且 返回值 具有与参数列表相反的父子类型关系, 那么 函数类型就具有与返回值类型相同的父子类型关系
类型别名
当某个类型的名字比较复杂或者在特定场景中不够直观时,可以选择使用类型别名的方式为此类型设置一个别名
type I64 = Int64类型别名的定义以 关键字
type开头,接着是类型的别名(如上例中的I64),然后是等号=,最后是原类型(即被取别名的类型,如上例中的Int64)只能在源文件顶层定义类型别名,并且原类型必须在别名定义处可见
例如,下例中
Int64的别名定义在main中将报错,LongNameClassB类型在为其定义别名时不可见,同样报错main() {type I64 = Int64 // Error, 类型别名只能在 源文件的顶层 定义}class LongNameClassA { }type B = LongNameClassB // Error, 'LongNameClassB' 类型没有被定义一个(或多个)类型别名定义中禁止出现(直接或间接的)循环引用
type A = (Int64, A) // Error, 'A' 循环引用自己type B = (Int64, C) // Error, 'B' 和 'C' 相互循环引用type C = (B, Int64)类型别名并不会定义一个新的类型,它仅仅是为原类型定义了另外一个名字,它有如下几种使用场景:
作为类型使用,例如:
type A = Bclass B {}var a: A = B() // 使用 类型别名A 作为类型 B当类型别名实际指向的类型为
class、struct时,可以 作为构造器名称使用:type A = Bclass B {}func foo() { A() } // 使用 类型别名A 作为 B 的构造函数当类型别名实际指向的类型为
class、interface、struct时,可以 作为访问内部静态成员变量或函数的类型名:type A = Bclass B {static var b : Int32 = 0;static func foo() {}}func foo() {A.foo() // 使用 类型别名A 访问 类B 的静态方法A.b}当类型别名实际指向的类型为
enum时,可以 作为enum声明的构造器的类型名:enum TimeUnit {Day | Month | Year}type Time = TimeUnitvar a = Time.Dayvar b = Time.Month // 使用 类型别名Time 来访问 TimeUnit 中的构造函数需要注意的是,当前用户自定义的类型别名 暂不支持在类型转换表达式中使用,参考如下示例:
type MyInt = Int32MyInt(0) // Error, 没有匹配的函数用于 操作符'()' 函数调用
仓颉允许针对类型起别名, 可以类比C语言的typedef
语法为:
type 别名 = 实际类姓名类型别名并不是定义一个新的类型, 而是为已有类型重新起一个名字
但, 类型别名只能在顶层作用域定义, 且 不能在类型转换表达式中使用
泛型别名
类型别名也是可以声明类型形参的,但是不能对其形参使用
where声明约束,对于泛型变元的约束会在后面给出解释当一个泛型类型的名称过长时,可以使用类型别名来为其声明一个更短的别名
例如,有一个类型为
RecordData,可以把他用类型别名简写为RD:struct RecordData<T> {var a: Tpublic init(x: T){a = x}}type RD<T> = RecordData<T>main(): Int64 {var struct1: RD<Int32> = RecordData<Int32>(2)return 1}在使用时就可以用
RD<Int32>来代指RecordData<Int32>类型
仓颉类型别名可以声明类型形参, 也就意味着 也可以给泛型类型起别名, 被称为泛型别名
但 泛型别名不能使用泛型约束
泛型别名的语法是:
type 别名<类型形参列表> = 泛型类型<类型形参列表>泛型别名在使用上与泛型类型保持一致
泛型约束
泛型约束的作用是在
function、class、interface、struct、enum声明时, 明确泛型形参所具备的操作与能力只有声明了这些约束才能调用相应的成员函数
在很多场景下泛型形参是需要加以约束的
以
id函数为例:func id<T>(a: T) {return a}开发者唯一能做的事情就是将函数形参
a这个值返回,而不能进行a + 1,println("${a}")等操作,因为它可能是一个任意的类型,比如(Bool) -> Bool,这样就无法与整数相加,同样因为是函数类型,也不能通过println函数来输出在命令行上而如果这一泛型形参上有了约束,那么就可以做更多操作了
约束大致分为接口约束与
class类型约束语法为在函数、类型的声明体之前使用
where关键字来声明,对于声明的泛型形参T1,T2,可以使用where T1 <: Interface,T2 <: Class这样的方式来声明泛型约束,同一个类型变元的多个约束可以使用&连接例如:
where T1 <: Interface1 & Interface2仓颉中的
println函数能接受类型为字符串的参数如果需要把一个泛型类型的变量转为字符串后打印在命令行上,可以对这个泛型类型变元加以约束,这个约束是
core中定义的ToString接口,显然它是一个接口约束:package std.core // `ToString` 被定义在`core`里public interface ToString {func toString(): String}这样就可以利用这个约束,定义一个名为
genericPrint的函数:func genericPrint<T>(a: T) where T <: ToString {println(a)}main() {genericPrint<Int64>(10)return 0}结果为:
10如果
genericPrint函数的类型实参没有实现ToString接口,那么编译器会报错例如传入一个函数做为参数时:
func genericPrint<T>(a: T) where T <: ToString {println(a)}main() {genericPrint<(Int64) -> Int64>({ i => 0 })return 0}如果对上面的文件进行编译,那么编译器会抛出 泛型类型参数不满足约束的错误
因为
genericPrint函数的泛型的类型实参不满足约束(Int64) -> Int64 <: ToString除了上述通过接口来表示约束,还可以使用
class类型来约束一个泛型类型变元例如:当要声明一个动物园类型
Zoo<T>,但是需要这里声明的类型形参T受到约束,这个约束就是T需要是动物类型Animal的子类型,Animal类型中声明了run成员函数这里声明两个子类型
Dog与Fox都实现了run成员函数,这样在Zoo<T>的类型中,就可以对于animals数组列表中存放的动物实例调用run成员函数:import std.collection.*abstract class Animal {public func run(): String}class Dog <: Animal {public func run(): String {return "dog run"}}class Fox <: Animal {public func run(): String {return "fox run"}}class Zoo<T> where T <: Animal {var animals: ArrayList<Animal> = ArrayList<Animal>()public func addAnimal(a: T) {animals.add(a)}public func allAnimalRuns() {for(a in animals) {println(a.run())}}}main() {var zoo: Zoo<Animal> = Zoo<Animal>()zoo.addAnimal(Dog())zoo.addAnimal(Fox())zoo.allAnimalRuns()return 0}程序的输出为:
dog runfox run注意:
泛型变元的约束只能是具体的
class类型或interface,且变元如果存在多个class类型的上界时,它们必须在同一继承链路上
仓颉泛型约束其实并不复杂:
class 类姓名<T> where T <: 目标约束类型 {}
func 函数名<T>() where T <: 目标约束类型 {}
// ...泛型约束的语法, 就是在定义泛型时, 后接where 目标类型变元 <: 目标约束类型
泛型约束是为了:
-
保证类型变元, 属于目标类型的子类型
如此, 在 泛型类型、泛型函数体 内使用类型变元时, 就能访问目标类型的成员
文档中举例子,
genericPrint<T>(a: T) where T <: ToString此时, 在函数体内, 就能够通过
print或println等函数, 直接打印形参a因为泛型约束, 保证了
a的类型实现了ToString接口, 所以可以直接调用print或println否则, 就不能直接调用
print或println打印约束其他类型时, 也是相同的作用
只要使用了泛型约束, 在泛型内 就能访问目标约束类型中的成员
-
限制类型实参, 属于目标类型的子类
同样以
genericPrint为例, 在调用genericPrint时, 必须保证调用时 类型实参实现了ToString接口否则编译直接报错