首页 / 资讯中心 / 文章详情

仓颉语言泛型类完全指南:从类型形参、约束设计到静态成员限制

仓颉语言泛型类完全指南:从类型形参、约束设计到静态成员限制 ★ FEATURED ARTICLE
文档编程语言【免费下载链接】cangjie_docs仓颉编程语言开发者文档。项目地址https://gitcode.com/Cangjie/cangjie_docs点击查看免费下载泛型类是仓颉编程语言中参数化类型体系的核心载体标准库中Map的键值对Node、HashMapK, V等类型均由泛型类实现。本文以NodeK, V为贯穿示例系统讲解泛型类的声明语法、类型形参约束where子句、静态成员与类型参数之间的禁区并结合泛型接口、泛型结构体、泛型枚举与子类型关系给出可复制、可运行的完整代码与编译期规则帮助你在实际项目中正确设计和使用泛型类。一、泛型类前置知识四个核心术语在深入泛型类之前先明确仓颉中关于泛型的一组术语详见 泛型概述类型形参Type Parameter类型或函数声明中需要在使用处被指定的类型声明时给定标识符以便在声明体中引用。类型变元Type Variable通过标识符引用这些类型形参时这些标识符被称为类型变元。类型实参Type Argument使用泛型声明时指定的具体类型。类型构造器Type Constructor需要零个、一个或多个类型作为实参的类型如ListListInt64就是通过Int64实参构造出的一个具体列表类型。例如class ListT { var elem: OptionT None var tail: OptionListT None } func sumInt(a: ListInt64) {}其中ListT中的T是类型形参elem: OptionT与tail: OptionListT中对T的引用是类型变元sumInt参数中ListInt64的Int64是类型实参List本身是类型构造器。在仓颉中function、class、interface、struct与enum的声明都可以声明类型形参也就是说它们都可以是泛型的。泛型类就是其中的一种声明了类型形参的class类型。二、定义泛型类以 Map 键值对 Node 为例泛型接口一节介绍了泛型接口的定义与使用泛型类的语法与其一脉相承。标准库Map中的键值对Node类型就是一个典型的泛型类其定义如下该代码可正常编译运行open class NodeK, V where K : Hashable EquatableK { var key: OptionK OptionK.None var value: OptionV OptionV.None init() {} init(key: K, value: V) { this.key OptionK.Some(key) this.value OptionV.Some(value) } }这段定义包含三个值得深入解析的设计点1. 为什么需要两个类型形参K与V一个键值对中键与值的类型有可能不相同且可以为任意满足条件的类型因此Node需要两个类型形参K表示键类型V表示值类型。如果只有单一形参TNode就无法同时表达键与值类型不同的场景。2. 形参约束where K : Hashable EquatableK的含义K : Hashable EquatableK是对于键类型K的约束意为K必须同时实现Hashable与EquatableK两个接口。其中:表示子类型/实现关系连接同一个类型变元需要同时满足的多个约束关键细节Equatable本身是泛型接口当它作为约束使用时必须带上类型实参写作EquatableK而非裸的Equatable。这种设计保证了K既能被哈希Hashable又能与自己比较相等EquatableK这正是哈希表类容器对键类型的基本要求。关于泛型约束的完整规则详见 泛型约束 章节。3. 成员类型使用OptionT包装key与value的声明类型是OptionK与OptionV。OptionT是仓颉标准库std.core中定义的泛型枚举用于表示可能有值、也可能为空的计算结果详见 Option 类型 与 泛型枚举package std.core // Option is defined in std.core. public enum OptionT { Some(T) | None public func getOrThrow(): T { match (this) { case Some(v) v case None throw NoneValueException() } } // ... }因此OptionK.None表示键尚未设置OptionK.Some(key)表示键已被赋予值。两个构造函数init()与init(key: K, value: V)分别对应空节点与已填充键值对的节点。三、泛型约束详解用where划定能力边界泛型约束的作用是在声明时明确泛型形参所具备的操作与能力只有声明了约束才能调用相应的成员函数。语法上约束使用where关键字声明在函数或类型的声明体之前。1. 基本语法:、,与对于声明的泛型形参T1, T2可以写作where T1 : Interface, T2 : Class规则要点,用于分隔不同类型形参的约束用于连接同一个类型形参需要同时满足的多个约束。例如下面的声明表示A与B都需要实现ToString接口open class NodeA, B where A : ToString, B : ToString {}易错点如果写成where A : ToString B : ToString会被当作同一个类型形参A的复合约束处理并不表示分别约束A与B。2. 接口约束实战让泛型变量可被打印core包中定义了ToString接口package std.core // ToString is defined in core. public interface ToString { func toString(): String }有了这个约束就可以定义泛型打印函数genericPrintfunc genericPrintT(a: T) where T : ToString { println(a) } main() { genericPrintInt64(10) }输出10如果传入的类型实参没有实现ToString接口编译器会直接报错。例如传入一个函数作为参数时func genericPrintT(a: T) where T : ToString { println(a) } main() { genericPrint(Int64) - Int64({i 0}) }编译该文件编译器会抛出泛型类型参数不满足约束的错误因为(Int64) - Int64 : ToString不成立。3. class 类型约束实战动物园 Zoo除了接口约束还可以使用具体 class 类型约束泛型变元。例如声明一个动物园类型ZooT要求T是动物类型Animal的子类型import std.collection.ArrayList 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 ZooT where T : Animal { var animals: ArrayListAnimal ArrayListAnimal() public func addAnimal(a: T) { animals.add(a) } public func allAnimalRuns() { for (a in animals) { println(a.run()) } } } main() { var zoo: ZooAnimal ZooAnimal() zoo.addAnimal(Dog()) zoo.addAnimal(Fox()) zoo.allAnimalRuns() }程序的输出为dog run fox run正是因为在ZooT的声明处约束了T : Animal才能在allAnimalRuns中对animals中的元素安全调用run()成员函数。注意泛型变元的约束只能是具体的 class 类型或 interface且变元如果存在多个 class 类型的上界时它们必须在同一继承链路上。四、泛型类的静态成员禁区泛型类的静态成员有严格的限制这是原文档重点强调的规则也是最容易踩坑的地方。1. 规则本质静态成员的内存是共享的由于泛型类的静态成员变量的内存是共享的因此静态成员变量或属性的类型声明和表达式中不能引用类型参数也不能包含未实例化的泛型类型表达式静态变量或属性初始化表达式中不能调用泛型类的静态成员函数或属性。为什么静态成员只存在一份不随类型实参不同而不同如果允许AT出现在静态成员中那么BInt32与BString会共享同一个静态成员它究竟应该以Int32还是String为实参实例化A就无从确定因此编译器直接禁止这种依赖。2. 错误示例逐行解析以下代码展示了各类非法用法!-- compile.error --标记编译会报错class AT {} class BT { static func foo() { 1 } static var err1: AT AT() // Error, static member cannot depend on generic parameter Generics-T static prop err2: AT { // Error, static member cannot depend on generic parameter Generics-T get() { AT() // Error, static member cannot depend on generic parameter Generics-T } } static var vfoo foo() // Error, its equal to static var vfoo BT.foo(), implicit reference to generic T. static var ok: Int64 1 } main() { BInt32.ok 2 println(BInt64.ok) // 2 }逐行解读声明错误原因static var err1: AT静态成员的类型声明引用了类型参数T报错static member cannot depend on generic parameter Generics-Tstatic prop err2: AT及其get()中的AT()静态属性的类型声明与初始化表达式均引用了类型参数T同样报错static var vfoo foo()该表达式等价于static var vfoo BT.foo()隐式引用了泛型参数T报错static var ok: Int64 1类型与初始化表达式均与T无关合法3. 静态成员跨实例化共享的验证main函数中的两行代码揭示了静态成员的真实行为BInt32.ok 2 println(BInt64.ok) // 2对BInt32.ok赋值2之后BInt64.ok读取到的值也是2。这正印证了泛型类的静态成员变量的内存是共享的——无论泛型类以何种类型实参实例化其静态成员都指向同一份内存。这一点与普通类的静态成员语义一致也是泛型类设计中必须时刻牢记的约束来源。五、泛型类的实例化、继承与使用1. 通过类型实参实例化与所有泛型声明一样泛型类在使用时需要显式指定类型实参。以NodeK, V为例let node NodeString, Int64(key: age, value: 18) let emptyNode NodeString, Int64()NodeString, Int64是一个键为String、值为Int64的具体类型而NodeInt64, String则是另一个完全不同的类型。在仓颉中元素类型不相同的泛型实例化类型是不相同的类型不能互相赋值。2. 泛型类的继承语义Node声明为open class并非偶然。根据 class 的继承 中的规则仓颉中class是引用类型支持单继承但非抽象的类要可被继承定义时必须使用open修饰。open class NodeK, V表明它允许被其他类继承。子类继承父类时将继承父类中除private成员和构造函数以外的所有成员。此外需要特别留意当实现或重定义的函数为泛型函数时子类型函数的类型变元约束需要比父类型中对应函数更宽松或相同更严格的约束会导致编译错误。六、与其他泛型声明的横向对比理解了泛型类之后可以顺势对比其他几类泛型声明它们共享同一套类型形参语法。1. 泛型接口Iterable / Iterator / Collection标准库中Iterable是典型的泛型接口详见 泛型接口public interface IterableE { func iterator(): IteratorE } public interface IteratorE : IterableE { func next(): OptionE } public interface CollectionT : IterableT { prop size: Int64 func isEmpty(): Bool }Iterator的next()需要返回一个使用时才指定的元素类型因此必须声明类型形参E。这与泛型类NodeK, V中的OptionK/OptionV用法互相呼应。2. 泛型结构体PairT, Ustruct类型的泛型与class类似详见 泛型结构体struct PairT, U { let x: T let y: U public init(a: T, b: U) { x a y b } public func first(): T { return x } public func second(): U { return y } } main() { var a: PairString, Int64 PairString, Int64(hello, 0) println(a.first()) println(a.second()) }输出hello 0泛型类与泛型结构体最根本的区别继承自class与struct的差异class是引用类型赋值或传参时共享同一对象struct是值类型赋值或传参时发生复制且class之间可以继承struct之间不能继承。3. 泛型枚举OptionOptionT已在第二节中给出。它用Some(T) | None两个构造器表达有值 / 空两种状态泛型形参T使其可以包装任意类型的返回值。七、泛型类的子类型关系不型变实例化后的泛型类型之间也存在子类型关系但仓颉对用户自定义的泛型类型有一条重要限制详见 泛型类型的子类型关系open class C {} class D : C {} interface IX {}即使D : C成立ID : IC也不成立。这是因为在仓颉语言中用户定义的类型构造器在其类型参数处是**不型变invariant**的。其精确定义为若T(A) : T(B)当且仅当A B则T是不型变的若T(A) : T(B)当且仅当A : B则T在X处是协变的若T(A) : T(B)当且仅当B : A则T在X处是逆变的。现阶段仓颉中所有用户自定义的泛型类型在所有类型变元处都是不变的内建类型除外内建元组类型对每个元素类型协变内建函数类型在入参类型处逆变、在返回类型处协变。因此对于泛型类NodeK, VNodeDog, X与NodeAnimal, X之间不存在子类型关系即便Dog : Animal。注意class以外的类型实现接口时该类型和该接口之间的子类型关系不能作为协变和逆变的依据。这一限制虽削弱了部分表达能力但避免了协变数组运行时抛异常这类安全隐患。八、实战泛型类如何支撑 HashMap回到泛型类最典型的生产场景。仓颉标准库使用HashMapK, V表示哈希表详见 HashMap 使用指南其键类型K必须实现Hashable和EquatableK接口例如数值或String值类型V可以是任意类型import std.collection.* var a: HashMapInt64, Int64 HashMapInt64, Int64() // 键为 Int64、值为 Int64 的 HashMap var b: HashMapString, Int64 HashMapString, Int64() // 键为 String、值为 Int64 的 HashMap对比可以发现NodeK, V where K : Hashable EquatableK中的约束与HashMapK, V对键类型K的要求完全一致。这正是Map的键值对就是使用泛型类来定义的这一论断的底层逻辑——标准库中的HashMap/Map正是通过约束K : Hashable EquatableK的泛型节点来存储键值对的。在 基础 Collection 类型概述 中可以看到HashMapK, V的键保证唯一、支持增删元素、元素无顺序这些特性都由其底层泛型节点与哈希约束共同支撑。实际使用时构造与遍历 HashMap 的典型写法如下import std.collection.HashMap main() { let map HashMapString, Int64([(a, 0), (b, 1), (c, 2)]) for ((k, v) in map) { println(The key is ${k}, the value is ${v}) } }补充泛型别名如果泛型类型名过长可以用类型别名简写详见 类型别名struct RecordDataT { var a: T public init(x: T) { a x } } type RDT RecordDataT main(): Int64 { var struct1: RDInt32 RecordDataInt32(2) return 1 }此后即可用RDInt32代指RecordDataInt32。注意类型别名可以声明类型形参但不能对其形参使用where声明约束。小结本文围绕仓颉编程语言的泛型类展开以NodeK, V为贯穿示例覆盖了从四个泛型术语、双类型形参设计、where约束语法:、,、的组合、接口约束与 class 约束到泛型类静态成员与类型参数之间的严格限制静态成员内存共享导致的禁止引用类型参数规则。同时横向对比了泛型接口、泛型结构体、泛型枚举与泛型类的异同阐明了用户自定义泛型类型不型变的子类型关系并结合HashMapK, V验证了约束设计在生产容器中的实际应用。掌握这些规则你就能在设计诸如键值对容器节点带约束的领域模型泛型工具类等场景时写出既能通过编译、又符合仓颉类型系统设计意图的泛型类代码。赞分享文档编程语言【免费下载链接】cangjie_docs仓颉编程语言开发者文档。项目地址https://gitcode.com/Cangjie/cangjie_docs点击查看免费下载相关推荐仓颉泛型类详解类型参数、泛型约束与静态成员限制仓颉泛型类详解类型参数、泛型约束与静态成员限制 本篇围绕仓颉Cangjie语言中的泛型类Generic Class展开结合标准库中 HashMap文档编程语言仓颉语言泛型体系全解参数化类型、泛型约束与型变规则仓颉语言泛型体系全解参数化类型、泛型约束与型变规则 本文以仓颉编程语言Cangjie的泛型机制为核心系统讲解参数化类型的定义、类型形参/类型变元/类型实文档编程语言大麦自动抢票项目实战指南3 步跑通第一个抢票任务大麦自动抢票项目实战指南3 步跑通第一个抢票任务 你可能经历过开售前 10 秒守在页面狂刷、票档按钮却全部变灰的时刻。ticket purchase 是一个开GUI 自动化RPA创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站