NOTE阅读文档版本:
语言规约 Cangjie-0.53.18-Spec
具体开发指南 Cangjie-LTS-1.0.3
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证
有条件当然可以直接 配置 Canjie-SDK
WARNING博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅
此样式内容, 表示文档原文内容
泛型
如果有一个声明, 在该声明中使用 尖括号声明了类型形参, 那么称这个声明是泛型的
在使用泛型声明时, 类型形参 可以被代换为 其他的类型
通过在函数签名中声明类型形参, 可以定义一个泛型函数; 通过在
class、interface、struct、enum、typealias定义中声明类型形参, 可以定义泛型类型
泛型, 基本所有面向对象的编程语言中都有泛型语法
类型形参和类型变元
在仓颉编程语言中, 用标识符表示类型形参, 并用
<>括起通过在
<>内用,分隔多个类型形参名称, 可以提供多个类型形参, 如<T1, T2, T3>,T1,T2,T3均为类型形参一旦声明了类型形参, 这些形参就可以被当做类型来使用
当使用标识符来引用声明的类型形参时, 这些标识符被称为类型变元
类型形参的语法如下:
typeParameters: '<' identifier (',' identifier)* '>';
仓颉中的泛型与 C++中的泛型, 在定义语法上存在一定的差异
C++依靠template关键字创建模板, 实现泛型
而仓颉则直接依靠identifier<typeParameters>创建泛型类型
类型参数的标识符被称为类型变元
泛型约束
在仓颉语言中可以用
<:来表示一个类型是另一个类型的子类型通过声明这种关系可以对泛型类型形参声明加以约束, 使得它只能被替换为满足特定约束的类型
泛型约束通过
where之后的<:运算符来声明, 由一个下界与一个上界来组成其中
<:左边称为约束的下界, 下界只能为类型变元
<:右边称为约束上界, 约束上界可以为类型当一个约束的上界是类型时, 该约束为子类型约束
当上界为类型时, 上界可以为任何类型
一个类型变元可能同时受到多个上界约束, 对于同一个类型形参的多个上界必须使用
&连接, 以此来简化一个类型形参有多个上界约束的情形, 它本质上还是多个泛型约束对不同类型变元的约束需要使用
,分隔.声明约束的语法如下:
upperBounds: type ('&' type)*;genericConstraints: 'where' identifier '<:' upperBounds (',' identifier '<:' upperBounds)*;例如以下示例展示了泛型约束声明的写法:
interface Enumerable<U> where U <: Bounded {...}interface Comparable<T> {...}func collectionCompare<T>(a: T, b: T) where T <: Comparable<T> & Seqence {...}func sort<T, V>(input: T)where T <: Enumerable<V>, V <: Object {...}类型变元
X和Y的约束相同, 指的是所有满足X约束的类型都满足Y的约束, 且所有满足Y的约束的类型也都满足X的约束;类型变元
X比Y的约束更严格, 指的是所有满足X约束的类型都满足Y的约束, 反之, 不一定满足;如果
X比Y的约束更严格, 则Y比X的约束更宽松两个泛型类型的约束相同, 指的是 泛型类型的类型变元数量相同, 且所有对应的类型变元约束均相同;
一个泛型类型
A的约束比另一个泛型类型B的约束更严格, 指的是A和B的类型变元数量相同, 且A的所有类型变元的约束均比B中对应的类型变元更严格;一个泛型类型
A的约束比另一个泛型类型B的约束更严格, 则B的约束比A更宽松比如下面的两个泛型
C和D, 假设有I1 <: I2,C的约束比D更严格:
class C<X, Y> where X <: I1, Y <: I1和class D<X, Y> where X <: I2, Y <: I2
泛型约束是指, 在定义泛型时 可以对 泛型的类型形参 添加约束
可以使泛型实例化时, 将实例化类型约束在指定类型范围内
用法是, 在声明泛型类型时, 在声明完类型参数列表之后, 添加where 约束下界 <: 约束上界, 约束下界 <: 约束上界, ...
约束下界, 在<:左边, 只能为 需要收到约束的类型形参
约束上界, 在<:右边, 即为需要进行的约束, 可以是类型, 也可以是其他的一些约束, 具体之后了解
当上界为类型时, 表示目标类型形参只能是约束类型及其子类型
且每个上界可以使用&连接多个约束, 以实现更严格的约束
, 可以针对不同的类型形参创建约束
类型型变
在正式介绍泛型函数与泛型类型前, 先简单介绍以下类型型变, 以此来说明在仓颉编程语言中, 泛型类型的子类型关系
仓颉中的泛型, 针对函数和类型
定义
如果
A和B是类型,T是类型构造器, 设其有一个类型参数X, 那么:
如果
T(A) <: T(B)当且仅当A <: B, 则T在X处是协变的如果
T(A) <: T(B)当且仅当B <: A, 则T在X处是逆变的如果
T(A) <: T(B)当且仅当A = B, 则T是不型变的
T1 <: T2, 表示T1是T2的子类型
协变, 表示如果类型形参A <: B, 那么泛型的实例化类型也是T(A) <: T(B)
逆变是相反的, 表示如果类型形参A <: B, 那么泛型的实例化类型是T(B) <: T(B)
泛型不型变
在仓颉编程语言中, 所有的泛型都是不型变的
这意味着如果
A是B的子类型,ClassName<A>和ClassName<B>之间没有子类型关系我们禁止这样的行为以保证运行时的安全
仓颉中, 对同一泛型类型, 不同的泛型实例化类型之间不存在子类型关系
函数类型的型变
函数的参数类型是逆变的, 函数的返回类型是协变的
假设存在函数
f1的类型是S1 -> T1, 函数f2的类型是S2 -> T2如果
S2 <: S1并且T1 <: T2, 则f1的类型是f2的类型的子类型
函数因为参数存在类型, 返回值也存在类型, 所以比较稍麻烦
对于函数, 参数类型逆变, 返回值类型协变, 说明两个函数只有参数和返回值类型, 存在相反的父子关系, 两个函数才可能存在父子关系
函数不只是泛型函数实例化出的函数类型, 对普通函数, 如果参数和返回值类型之间存在此关系, 也存在子类型关系
元组类型的协变
元组之间是存在子类型关系的, 如果一个元组的每一个元素 都是 另一个元组的对应位元素的子类型, 则该元组是另一个元组的子类型
假设有元组
Tuple1和Tuple2, 它们的类型分别为(A1, A2.., An)、(B1, B2.., Bn), 如果对于所有i都满足Ai <: Bi, 则Tuple1 <: Tuple2
元组, 比较简单, 只有所有元素均存在相同的父子类型关系, 元组之间才存在对应的父子类型关系
型变的限制
现在以下两种情况的型变关系被禁止:
class以外的类型实现接口, 该类型和该接口之间的子类型关系 不能作为协变和逆变的依据实现类型通过扩展实现接口, 该类型和该接口之间的子类型关系 不能作为协变和逆变的依据
这些限制除了影响型变关系以外, 同时也会影响
override对于子类型的判定不满足型变关系的类型, 在发生
override时不能作为子类型的依据interface I {func f(): Any}class Foo <: I {func f(): Int64 { // error...}}
可以再具体的分析一下类型型变
文档中提到:
-
如果
A和B是类型,T是类型构造器, 设其有一个类型参数X, 那么:-
如果
T(A) <: T(B)当且仅当A <: B, 则T在X处是协变的 -
如果
T(A) <: T(B)当且仅当B <: A, 则T在X处是逆变的 -
如果
T(A) <: T(B)当且仅当A = B, 则T是不型变的
-
这里的T是指类型构造器, 好像并不仅仅指泛型, 而是指任何可以通过类型构建出一个新的其他类型的东西
比如:
-
泛型, 无疑是最明确的, 只要类型参数传入不同的类型, 那么就会构建出不同的类型参数
但泛型是不型变的, 即 即使参数传入的类型之间存在父子关系, 实例化出来的类型也不存在父子关系
-
函数, 当参数类型、返回值类型不同时, 函数的类型也会出现差别, 这也说明 函数的实际类型是根据 参数类型和返回值类型构建出来的
函数是双变的, 当两个函数的参数列表之间存在父子关系, 且返回值类型也存在父子关系, 但两种父子关系相反时, 两个函数类型才存在父子关系
-
元组, 当元素的类型不同, 则 对应的元组类型也不同, 元组的实际类型 是根据元素的类型构建出来的
元组时协变的, 当两个不同的元组中元素类型之间均存在相同的父子关系时, 两个元组类型才存在父子关系
而且, 类型型变 是对两个被构建出来的类型之间对比产生的, 单个类型你好像没有办法说类型型变
而关于型变的限制, 均是关于实现接口时会出现的限制, 而且对比目标应该是实现接口的成员, 而不是类型本身
且除了class直接实现接口之外, 其他类型直接实现接口或扩展实现接口, 都无法型变判断, 也就无法确定子类型, 更无法进行override
泛型约束上界中导出的约束
对于一个约束
L <: T<T1..Tn>, 其中的上界T<T1..Tn>的声明T的类型形参可能还需要满足一些约束, 在实参Ti的代换后, 这些约束需要被隐式地引入到当前声明的上下文中例如:
interface Eq<T> {func eq(other: T): Bool}interface Ord<T> where T <: Eq<T> {func lt(other: T): Bool}func foo<T>(a: T) where T <: Ord<T> {a.eq(a)a.lt(a)}对于
foo函数, 虽然只声明了T受到Ord的约束, 但是由于Ord的T类型受到了Eq的约束, 所以在foo函数里是可以使用Eq中的eq函数的这样,
foo函数的约束实际上是T <: Eq & Ord这样在声明一个泛型参数满足一个约束时, 这一约束的上界中需要满足的约束也将被引入
对于其他泛型声明, 隐式地引入约束上界的约束这一规则也是有效的
例如:
interface A {}class B<T> where T <: A {}class C<U> where U <: B<U> {} // 实际约束是 U <: A & B<U>这里, 对于类
C, 它的泛型形参U所受到的约束实际上为U <: A & B<U>注意: 虽然当前声明中上界的约束会被隐式地引入, 但当前声明仍然可以将这些约束显式地写出
仓颉中的泛型约束, 如果约束上界本身也是被约束的类型, 那么约束链也是会被引入的, 实际会变成类似这样U <: A & B
最终的类型, 需要满足所有的约束才是正确的
猜测: 所有可以存在约束的语法应该都是这样的
约束上界存在接口类型时, 表示此约束判断的是是否实现了目标接口
泛型函数和泛型类型的定义
泛型函数
如果一个函数声明了一个或多个类型形参, 则将其称为泛型函数
语法上, 类型形参紧跟在函数名后, 并用
<>括起, 如果有多个类型形参, 则用,分离func f<T>() {...}func f1<T1, T2, T3, ...>() {...}泛型函数的语法如下:
functionDefinition: functionModifierList? 'func' identifiertypeParameters functionParameters(':' type)? genericConstraints?block?;需要注意的是:
<与>在使用时, 会优先解析为泛型, 如果成功, 则直接就是泛型表达式, 否则才为比较运算符, 例如:(c < d , e > (f))这一表达式会被解析为函数调用表达式
仓颉泛型函数的定义非常简单: 定义普通函数时, 在函数名后添加<类型参数列表>, 类型参数列表中的类型参数, 是可以在之后的函数列表、函数体定义中使用的
泛型类型
如果一个
class、interface、struct、enum、typealias的定义中声明了一个或多个类型形参, 则它们被称为泛型类型语法上, 类型形参紧跟在类型名(如类名、接口名等)后, 并用
<>括起, 如果有多个类型形参, 则用,分隔class C<T1, T2> {...} // 泛型 classinterface I<T1> {...} // 泛型 interfacetype BinaryOperator<T> = (T, T) -> T // 泛型 类型映射
泛型类型的定义也非常简单, 本质上就是 类型定义时, 在标识符后添加<类型参数列表>
类型映射也可以是泛型的
泛型类型检查
泛型声明的检查
泛型约束的健全性检查
对一个声明的所有类型形参, 其每个形参的约束上界可以分为两种情况:
上界也是类型变元, 这个类型变元可能是它本身, 也可能是其他的类型变元
上界为具体类型时, 可以分为两种情形:
第一种情形是上界
class与interface类型时, 称为类相关类型第二种情形是上界为除
class与interface类型以外的类型, 这些称为类无关类型在仓颉语言中, 对于一个类型变元的一个或多个具体类型上界需要满足如下规则:
所有的约束上界只能属于同一种情形, 即要么上界都是类相关类型, 要么是类无关类型
例如:
T <: Object & Int32不合法当上界是类相关类型时, 如果存在多个类的上界, 那么这些类需要在同一个继承链上, 对于接口没有此限制
一个类型变元的多个泛型上界中不允许包含冲突的成员定义, 具体来说, 冲突指同名函数或相同操作符之间不构成重载, 并且返回类型之间不具有子类关系
当上界是类无关类型的情形时, 只能包含一种类无关的具体类型, 不能同时为两个不同的具体类型
例如:
T <: Int32 & Bool不合法类型变元上界为类无关类型的情形时不能存在递归约束
递归泛型约束是 上界类型实参直接或间接依赖于 下界类型变元自身的约束
例如:
T <: Option<T>, 由于Option是通过enum关键字声明的, 所以此种递归泛型约束不合法
T <: U, U <: (Int32) -> T也不合法, 因为函数是值类型,T类型间接地通过U依赖了自身
类型兼容性检查
对于泛型声明的类型的检查主要是检查 泛型类型 与其 所在的类型上下文中 是否兼容, 对于成员函数、变量的访问是否合法
例如:
open class C {func coo() {...}}class D <: C {...}interface Tr {func bar(): Int64}func foo<T>(a: T) {var b: C = a // error, T 不是 C 的子类a.coo() // error, T 没有成员函数 cooa.bar() // error, T 没有实现 Tr}在上述示例代码的
foo的函数体中共有 3 处报错, 原因分别如下:
由于
foo函数中声明的变量b的期望的类型是C, 所以这里需要检查T是否是C的子类型, 即T <: C, 而这一约束不存在于T的上下文中, 所以变量声明处编译报错由于泛型类型
T在当前上下文与C无关, 所以也不能访问C的成员函数coo类似地, 由于
T类型的不存在Tr的约束, 所以也不能访问Tr的成员函数bar
如果, 需要通过类型变元转换指定子类型, 并访问其成员, 那么最好进行目标子类型约束
否则, 是会编译报错的
如果想要通过类型检查, 需要在 声明体前 加入泛型约束:
open class C {func coo() {...}}interface Tr {func bar(): Int64}func foo<T>(a: T) where T <: C & Tr {var b: C = a // OK, T 现在是 C 的子类型a.coo() // OK, T 是 C 的子类型, 所以拥有成员函数 cooa.bar() // OK, T 受 Tr 约束}特别地, 如果一个类型变元
T的泛型上界包含一个函数类型 或 重载了函数调用操作符()的类型, 则类型为T的值可以被作为函数调用当上界为函数类型时, 该调用的返回类型 为
T的上界的返回类型当上界为重载了函数调用操作符
()的类型时, 该调用的返回类型 为上界类型中匹配的函数调用操作符的返回类型
当实现了目标类型的约束之后, 在语法上 就可以通过类型检查 访问成员了
泛型声明使用的检查
对于泛型声明的使用检查, 主要是将 实参代入到泛型声明的形参, 然后检查约束是否成立
如果我们直接用 C 类型调用上一小节中定义的 foo 函数:
main(): Int64 {foo<C>(C()) // Error, C 没有实现 Trreturn 0}那么会得到: 类型
C没有实现Tr的错误, 这是因为在foo函数的约束中有T <: C & Tr, 其中的形参 T 会被代替为C, 首先C <: C成立, 但是C <: Tr不成立
实际就是, 实例化类型之后, 在判断约束是否成立
如果为
C类型加入了实现Tr的声明, 那么就可以满足T <: Tr这一约束:extend C <: Tr {func bar(): Int64 {...}}特别的是, 当
interface作为泛型约束时, 调用时 泛型变元实例化的类型 必须完全实现 所有上界约束中的interface static函数意味着如果作为泛型约束的
interface中存在static函数, 就无法将 未实现对应static函数的interface或抽象类作为泛型变元实例化的类型interface I {static func f(): Unit}func g<T>(): Unit where T <: I {}main() {g<I>() // Error, I 未实现所有static函数return 0}
泛型实例化的深度
为保证泛型实例化不会出现死循环或耗尽内存, 在编译过程中会对实例化的层数做出限制
例如:
class A<T>{func test(a: A<(A<T>, A<T>)>): Bool {true}}main(): Int64 {var a : A<Int32> = A<Int32>()return 0}这段程序会报
infinite instantiation的错误
这个报错的意思是, 泛型无限实例
但是我很好奇, 这个限制是只针对无限实例的, 还是针对多层泛型的
泛型实例化
一个泛型声明 在所有类型形参的取值 都确定之后, 形成一个 对应的非泛型语言结构 的过程称之为 泛型声明的实例化
即, 定义一个泛型之后, 使用泛型时 需要传入类型, 编译器会根据传入类型 进行类型取值, 并根据类型创建非泛型结构的过程, 就叫泛型的实例化
实例化的类型确实存在, 但代码中是看不到的
泛型函数的实例化
func show<T>(a: T) where T <: ToString {a.toString()}在给定
T = Int32的取值之后会形成这样的实例(这里假定show$Int32是编译器实例化后的内部表示, 后面也会使用类似的表示):func show$Int32(a: Int32) {a.toString()}
实例化泛型函数的限制
在仓颉语言中, 以下情形不能声明泛型函数:
接口与抽象类中的非静态抽象函数
类与抽象类中被
open关键字修饰的实例成员函数操作符重载函数
例如, 以下函数的声明与定义都是不合法的:
abstract class AbsClass {public func foo<T>(a: T): Unit // Error: 在抽象类中 声明抽象泛型函数public open func bar<T>(a: T) { // Error: 在抽象类中 定义open泛型函数...}}interface IF {func foo<T>(a: T): Unit // Error: 在接口中, 声明抽象泛型函数}open class Foo {public open func foo<T>(a: T): Unit { // Error: 在类中 定义open泛型函数...}}而以下的泛型函数是合法的:
class Foo {static func foo<T>(a: T) {...} // 在类中 定义静态泛型函数func bar<T>(a: T) {...} // 在类中 定义非open的泛型函数}abstract class Bar {func bar<T>(a: T) {...} // 在抽象类中 定义非open的泛型函数}struct R {func foo<T>(a: T) {...} // 在struct中 定义泛型函数}enum E {A | B | Cfunc e<T>(a: T) {...} // 在enum中 定义泛型函数}
类和接口的实例化
class Foo<T> <: IBar<T>{var a: Tinit(a: T) {this.a = a}static func foo(a: T) {...}public func bar(a: T, b: Int32): Unit {...}}interface IBar<T> {func bar(a: T, b: Int32): Unit}在给定
T=Int32时, 会生成以下实例的声明:class Foo$Int32 <: IBar$Int32 {var a: Int32static func foo(a: Int32) {...}func bar(a: Int32) {...}}interface IBar$Int32 {func bar(a: Int32, b: Int32)}
struct的实例化
结构体的实例化与类的实例化十分类似
struct Foo<T> {func foo(a: T) {...}}当给定
T=Int32时, 会生成以下实例的声明:struct Foo$Int32 {func foo(a: Int32) {...}}
Enum的实例化
enum Either<T, R> {Left(T)| Right(R)}当
Either被给定参数Int32与Bool时, 类型在被实例化后得到:enum Either$Int32$Bool {Left(Int32)| Right(Bool)}在使用一个泛型声明时, 例如调用泛型函数、构造泛型类型的值等, 在编译时 实际发生作用的都是确定类型形参后的实例, 也就是说 只有 当所有泛型参数都为具体类型 后才会发生实例化
说简单一点, 泛型的实例化, 实际就是拿传入的类型, 替换泛型变元
但, 必须要确定所有泛型变元的具体类型之后, 才会发生实例化
泛型函数重载
在仓颉编程语言中, 支持泛型函数之间的重载, 也支持泛型函数与非泛型函数之间的重载, 重载的定义详见 函数重载
函数调用时, 重载的处理过程如下:
构建函数调用的候选集, 最终进入候选集的函数均为通过类型检查可以被调用的函数, 详见[重载函数候选集]
在构建候选集时, 对于泛型函数有额外的规则, 下面会详细介绍;
根据作用域优先级规则(详见 [作用域优先级])和最匹配规则(详见 [最匹配规则])选择最匹配的函数, 如果无法确定唯一的最匹配函数, 则报无法决议的错误;
如果实参类型有多个, 根据最匹配函数确定实参类型, 如果不能确定唯一的实参类型, 则报错
关于函数调用的候选集, 应该类似记录重载函数的东西, 不在代码编写层面, 是由编译器构建的
构建函数调用的候选集时, 对于泛型函数需要注意以下几点:
在函数调用时, 对于泛型函数
f, 进入候选集的 可能是 部分实例化后的泛型函数 或是 完全实例化的函数具体是哪种形式进入候选集, 由调用表达式的形式决定:
调用表达式的形式为:
C<TA>.f(A), 即f为某个泛型类型的静态成员函数先对类型进行实例化, 再对 实例化后类型的静态成员函数 进行函数调用的类型检查, 如果能通过, 则进入候选集
假设
C的类型形参为X, 则进入候选集的函数是将f中X代换成TA之后的f':// 上下文包含以下类型: Base、Sub 和 Sub <: Baseclass F<X> {static func foo<Y>(a: X, b: Y) {} // foo1static func foo<Z>(a: Sub, b: Z) {} // foo2}/* 进入候选集的函数是 foo1 和部分实例化的 foo2: foo<Y>(a: Base, b: Y)和 foo<Z>(a: Sub, b: Z) */var f = F<Base>.foo(Sub(), Base()) // foo2调用表达式的形式为:
obj.f(A), 且obj为泛型类型实例化类型的实例, 即f为某个泛型类型的非静态成员函数在该表达式中
obj的类型需要先确定, 再根据obj的类型来获取候选集函数
obj的类型是实例化后的类型, 也是将 实例化后的类型的 非静态成员函数 进行函数调用的类型检查, 通过类型检查的进入候选集// 上下文包含以下类型: Base、Sub 和 Sub <: Baseclass C<T, U> {init (a: T, b: U) {}func foo(a: T, b: U) {} // foo1func foo(a: Base, b: U) {} // foo2}/** 推断 obj 的类型是 C<Sub, Rune>* 进入候选集的函数被实例化为 foo1、foo2:* foo(a:Sub, b:Rune) 和 foo(a: Base, b: Rune)*/main() {C(Sub(), 'a').foo(Sub(), 'a') // 选择 foo1return 0}
调用类的成员函数时, 都要先经过类型的实例化 或 通过类型实例 进行
如果函数调用时未提供类型实参, 也就是函数调用的形式为:
f(a), 则要求进入候选集的泛型函数f满足以下要求:
如果
f是泛型函数, 其形式为:f<X_1,...,X_m>(p1: T_1, ..., pn: T_n): R调用表达式中未提供类型实参, 形式为
f(a_1, ..., a_n)
f可以根据实参的类型(A1,...,An)推断出一组类型实参TA_1, ..., TA_m, 满足f的所有泛型约束, 且将f中的X_1, ..., X_m分别代换成TA_1, ..., TA_m, 能通过函数调用的类型检查, 检查规则如下:- 将推断出的类型实参`TA_1, ..., TA_m`代换到`f`的函数形参`(T1, ..., Tn)`后, 满足在调用表达式所在的上下文中`(A1, ..., An)`是代换后形参类型的子类型:$$σ=[X1↦TA1,...,Xm↦TAm]$$$$Δ⊢(A1,...,An)<:σ(T1,...,Tn)$$- 如果调用表达式中提供了返回类型`RA`, 则需要根据返回类型进行类型检查, 将f的返回类型`R`中的`X_1, ..., X_m`分别代换成`TA_1, ..., TA_m`后, 满足在调用表达式所在的上下文中代换后的返回类型是`RA`的子类型$$σ=[X1↦TA1,...,Xm↦TAm]$$$$Δ⊢σR<:RA$$
如果调用普通函数, 不指定类型形参, 编译器会根据 传入的实参类型, 推导并代换类型形参, 并作类型检查
如果函数调用时提供了类型实参, 也就是函数调用的形式为:
f<TA>(a), 则要求进入候选集的 f 满足以下要求:
f的类型形参与TA的数量相同, 且类型实参TA满足f的泛型约束, 且类型实参代入之后能通过函数调用的类型检查规则
如果调用普通函数, 指定类型形参, 编译器代换类型形参, 并作类型检查