尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

【仓颉语言入门 · 第19课】

发布时间:2026/9/29 12:37:16

资讯中心
01
ARTICLE

【仓颉语言入门 · 第19课】

【仓颉语言入门 · 第19课】
【仓颉语言入门 · 第19课】泛型编程一份逻辑任意类型复用第 18 课的MyOptionT、MyResultT, E已经提前用了一个新玩意儿定义类型时不写死它装什么而是留一个占位符T使用时再决定。本课就把它讲透——泛型generics把类型也变成参数让同一份函数/结构体/类的逻辑能对任意类型复用。第 8 课天天用的ArrayListT、第 14 课的IteratorT背后都是它。本文所有代码与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。目录系列导航整套路线共7 个模块、30 课模块课次内容一、环境与入门0105环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环二、常用类型与数据组织0610字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理三、函数与函数式1114函数、Lambda 与高阶函数、闭包、迭代器与惰性序列四、面向对象与类型系统1520struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展五、工程化与标准库2125cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试六、并发编程2628线程、Channel 通道与同步原语、并发实战七、项目实战2930命令行小工具、GeoJSON 数据处理实战环境搭建与第一个仓颉程序变量与常量let / var 与基本数据类型运算符与标准输入输出分支结构if 与 match 表达式循环结构while / for / Range字符串详解与字符串插值数组 Array 与区间 Range集合框架ArrayList、HashMap、HashSet可空类型?与 Option错误处理异常机制与 Result函数定义、参数与返回值Lambda 与高阶函数闭包、作用域与函数类型迭代器 Iterator 与 Sequence结构体 struct 与类 class构造函数、属性与方法接口 interface 与实现枚举 enum、代数数据类型与 match 模式匹配泛型编程本文扩展、类型别名与可见性控制cjpm 包管理与多文件项目组织文件与目录 IOJSON 处理结合 stdx 扩展库网络编程入门单元测试并发基础线程的创建与等待Channel 通道与同步原语并发实战多线程任务处理实战一带文件持久化的命令行小工具实战二GeoJSON 数据处理程序一、为什么需要泛型从复制粘贴说起假设要写一个原样返回参数的函数。为了兼容整数和字符串没有泛型时你只能复制粘贴两份func echoInt(value: Int64): Int64 { return value } func echoString(value: String): String { return value }逻辑一模一样只有参数类型不同。要是再想支持Float64、Bool、UInt8……就得继续复制第三份、第四份。第 8 课的ArrayList也面临同样的问题总不能为每种元素都写一个IntArrayList、StringArrayList。泛型的思路是函数里的类型先不写死用一个占位符代替调用时再告诉它这次用什么类型。占位符通常写作TType 的首字母你也可以用任何合法标识符。二、泛型函数把类型也变成参数2.1 第一个泛型函数在函数名后面加一对尖括号T就声明了一个类型参数func echoT(value: T): T { return value } main(): Int64 { println(echo(42)) // T 被推断为 Int64 println(echo(你好)) // T 被推断为 String println(echo(true)) // T 被推断为 Bool return 0 }运行结果42 你好 true一个函数三种类型逻辑只写了一遍。2.2 类型实参通常自动推断调用echo(42)时编译器从实参42推断出T Int64不需要你手写。绝大多数场景靠推断就够了。2.3 也可以显式指定类型实参推断不出来或想写得更明确时可以在函数名后用类型显式给出main(): Int64 { println(echoInt64(42)) println(echoString(显式类型实参)) return 0 }运行结果42 显式类型实参2.4 多个类型参数K, V类型参数可以有多个用逗号分隔。下面三个例子分别演示取数组末元素、组装键值对、交换数组中两个位置func getLastT(values: ArrayT): T { return values[values.size - 1] } // K、V 是两个互相独立的类型参数 func makePairK, V(key: K, value: V): (K, V) { return (key, value) } func swapAtT(values: ArrayT, i: Int64, j: Int64): Unit { let tmp values[i] values[i] values[j] values[j] tmp } main(): Int64 { println(getLast([1, 2, 3, 4, 5])) println(getLast([Cangjie, Java, Python])) let p makePair(年龄, 18) println(${p[0]}${p[1]}) let nums [10, 20, 30] swapAt(nums, 0, 2) println(${nums[0]}, ${nums[1]}, ${nums[2]}) return 0 }运行结果5 Python 年龄18 30, 20, 10注意getLast里ArrayT的元素类型、下标返回值、函数返回值三处的T是同一个——这正是泛型能保证类型对得上的原因传入ArrayInt64返回的就一定是Int64不会把整数当字符串用。三、无约束的 T能做什么不能做什么T只是个占位符在加任何约束之前编译器对它长什么样一无所知。3.1 能做的事不依赖类型内部结构的操作都可以前面的例子已经证明整体赋值、作为参数传递、作为返回值、放进同类型的容器ArrayT、ArrayListT。3.2 不能做的事一对两个 T 做运算直接写a bfunc addTwiceT(a: T, b: T): T { return a b }编译报错error: invalid binary operator on type Generics-T and Generics-T # note: you may want to implement operator func (right: Generics-T) for type Generics-T编译器的意思很直白我不知道T有没有实现你要不用不了。报错里的Generics-T就是泛型参数 T在编译器内部的叫法。3.3 不能做的事二直接打印 Tfunc printItT(value: T): Unit { println(value) }编译报错error: mismatched types expected Struct-String, found Generics-T因为println需要的是String而无约束的T不保证能转成字符串。想让上面两段代码合法就得给T加约束——下一节。四、泛型约束where约束用关键字where写在参数列表后面形式为where T : 接口读作T 必须实现这个接口。有了约束函数体里就能用该接口提供的能力。4.1 约束 T : ToString可以打印了func printItT(value: T): Unit where T : ToString { println(值是${value}) } main(): Int64 { printIt(42) printIt(仓颉) return 0 }运行结果值是42 值是仓颉4.2 约束 T : Comparable可以比大小了第 18 课讲过实现ComparableT就自动拥有、、等比较运算。泛型求最大值func maxOfT(a: T, b: T): T where T : ComparableT { if (a b) { return a } return b } main(): Int64 { println(maxOf(3, 9)) println(maxOf(2.5, 1.8)) println(maxOf(apple, banana)) return 0 }运行结果9 2.500000 banana整数、浮点数、字符串都能比较一份函数全搞定。4.3 多个约束用 合并一个类型参数可以同时要求多个接口用连接。下例要求T既能比较、又能打印func reportMaxT(a: T, b: T): Unit where T : ComparableT ToString { let m maxOf(a, b) println(较大的是${m}) } main(): Int64 { reportMax(10, 20) reportMax(cat, dog) return 0 }运行结果较大的是20 较大的是dog4.4 约束不满足时编译期直接拦下拿一个没有实现ComparableT的普通类硬塞进要求该约束的泛型类class ComparableBoxT where T : ComparableT { init() {} } class PlainPoint { let x: Int64 let y: Int64 init(x: Int64, y: Int64) { this.x x this.y y } } main(): Int64 { let box ComparableBoxPlainPoint() return 0 }编译报错error: generics type arguments do not match the constraint of Class-ComparableBoxGenerics-T note: Class-PlainPoint is not a subtype of Interface-ComparableClass-PlainPoint错误在编译期就暴露而不是等运行到比较那一步才崩溃——这就是约束的价值。4.5 常用约束速查约束接口保证你能做什么备注ToStringtoString()可println、可字符串插值非泛型接口EquatableT!自动获得泛型接口ComparableTcompare返回Ordering见第 18 课Hashable可做HashMap的键、HashSet的元素1.2.0 里是非泛型接口写HashableT会报错见 FAQ Q4五、泛型结构体Box结构体第 15 课同样可以带类型参数struct BoxT { let value: T init(value: T) { this.value value } func peek(): T { return this.value } } main(): Int64 { let b1 BoxInt64(42) let b2 BoxString(你好) println(${b1.peek()}, ${b2.peek()}) return 0 }运行结果42, 你好⚠️ 注意和某些语言不同仓颉的 struct不会因为你写了字段就自动生成构造函数。如果删掉上面的init编译会报error: the uninitialized member variable value is not initialized in the constructor of class or struct所以泛型结构体也要像第 16 课讲的那样显式写init。六、泛型接口与泛型类写一个 Stack6.1 泛型接口接口名后也能加类型参数。先定义能存取元素的容器接口interface ContainerT { func add(item: T): Unit func take(): OptionT func size(): Int64 }6.2 泛型类实现泛型接口下面用第 8 课的ArrayListT实现一个后进先出的栈记得import std.collection.*class StackT : ContainerT { private let items: ArrayListT ArrayListT() public init() {} public func add(item: T): Unit { this.items.add(item) } public func take(): OptionT { if (this.items.isEmpty()) { return None } let last this.items.size - 1 let v this.items[last] this.items.remove(last..last 1) return Some(v) } public func size(): Int64 { return this.items.size } } main(): Int64 { let s StackString() s.add(第一个) s.add(第二个) s.add(第三个) println(栈大小${s.size()}) while (let Some(v) - s.take()) { println(弹出${v}) } println(空栈再弹${s.take()}) return 0 }运行结果栈大小3 弹出第三个 弹出第二个 弹出第一个 空栈再弹None两个要点class StackT : ContainerT实现接口时把接口的类型参数也填成同一个T。弹出元素用的是第 8 课的删单个写法remove(last..last 1)左闭右开栈空时take()返回None而不是抛异常。配合第 18 课的 while-let取空自然结束。七、带约束的泛型类与泛型继承7.1 泛型类也能加约束类的类型参数同样用where约束。这个有序盒子要求元素可比较于是类内可以求最大元素open class OrderedBoxT where T : ComparableT { protected let items: ArrayListT ArrayListT() public init() {} public func add(item: T): Unit { this.items.add(item) } public func largest(): OptionT { if (this.items.isEmpty()) { return None } var best this.items[0] for (x in this.items) { if (x best) { best x } } return Some(best) } }注意类被标成了open第 15 课因为下面要继承它。7.2 子类写死类型实参继承时可以把父类的类型参数固定成具体类型这样子类只服务这一种元素还能按该类型加专用方法class NumberBox : OrderedBoxInt64 { public init() { super() } func sum(): Int64 { var total 0 for (x in this.items) { total x } return total } } main(): Int64 { let nb NumberBox() nb.add(3) nb.add(9) nb.add(5) println(总和${nb.sum()}) println(最大${nb.largest()}) return 0 }运行结果总和17 最大Some(9)7.3 子类保留类型参数子类也可以继续保留泛型此时父类的约束要原样重写一遍class CountingBoxT : OrderedBoxT where T : ComparableT { public init() { super() } func count(): Int64 { return this.items.size } } main(): Int64 { let cb CountingBoxString() cb.add(apple) cb.add(banana) cb.add(cherry) println(个数${cb.count()}) println(最大${cb.largest()}) return 0 }运行结果个数3 最大Some(cherry)八、实战泛型仓库 RepositoryK, V把本课知识点串起来一个通用的数据仓库用HashMap存储键、值的类型都由使用方决定查询统一返回Option。package cjprobe19 import std.collection.* // K键、V值键必须可哈希、可判等才能做 HashMap 的键 class RepositoryK, V where K : Hashable EquatableK { private let store: HashMapK, V HashMapK, V() public init() {} func save(key: K, value: V): Unit { this.store.add(key, value) } // 用 get 安全读取键不存在返回 None绝不抛异常 func find(key: K): OptionV { return this.store.get(key) } func delete(key: K): Bool { match (this.store.remove(key)) { case Some(_) true case None false } } func count(): Int64 { return this.store.size } } // 泛型函数找不到时返回调用方给的默认值 func findOrElseK, V(repo: RepositoryK, V, key: K, def: V): V where K : Hashable EquatableK { match (repo.find(key)) { case Some(v) v case None def } } main(): Int64 { // 用户仓库编号 - 姓名 let users RepositoryInt64, String() users.save(1, 张三) users.save(2, 李四) println(用户数${users.count()}) println(找1号${users.find(1)}) println(找9号${findOrElse(users, 9, 未知用户)}) println(删1号${users.delete(1)}再删1号${users.delete(1)}) // 库存仓库商品名 - 数量 let stock RepositoryString, Int64() stock.save(键盘, 50) stock.save(鼠标, 0) println(键盘库存${stock.find(键盘)}) println(耳机库存${findOrElse(stock, 耳机, -1)}) return 0 }运行结果用户数2 找1号Some(张三) 找9号未知用户 删1号true再删1号false 键盘库存Some(50) 耳机库存-1值得体会的设计同一套Repository代码实例化成了两个完全不同类型的仓库——RepositoryInt64, String和RepositoryString, Int64键值类型刚好对调却一行新逻辑都不用写。查询走第 8 课强调过的get返回Option缺键不抛异常findOrElse用 match 处理两种结果调用方拿到的永远是一个确定的值。九、常见问题 FAQQ1泛型和第 17 课的接口多态什么时候用哪个泛型在编译期就确定了具体类型编译器为每个类型实参生成对应代码保留装的是什么类型这一信息StackInt64里一定是整数接口多态在运行期分派适合把一批不同实现混装、统一处理ArrayDrawable里放各种图形。经验法则写对类型不挑剔、但要保留类型的容器和算法用泛型写一组可互换的实现用接口。两者经常配合比如本课的StackT : ContainerT。Q2为什么我的泛型函数里不能直接println(value)、不能a b因为无约束的T对编译器来说是未知类型不保证有这些能力。按需要加约束打印用where T : ToString比大小用where T : ComparableT。Q3用泛型会有性能开销吗泛型在编译期按类型实参生成具体代码调用没有运行期分派的额外开销。真正可能带来装箱开销的是用接口类型接收值那是接口多态的代价不是泛型的。Q4为什么键约束写HashableT编译报错实测在 SDK 1.2.0 中Hashable是非泛型接口直接写K : Hashable即可写HashableK会报type arguments number does not match type parameters number。而EquatableT、ComparableT是泛型接口必须带类型参数。Q5一个类型参数最多能加几个约束没有只能一个的限制需要几个能力就用连几个如T : ComparableT ToString。约束越多函数体内能安全使用的操作越多但能传进来的类型也越少——按需添加即可。Q6泛型类可以被继承吗可以但父类要先标open。两种形式本课都实测过子类把类型实参写死NumberBox : OrderedBoxInt64或子类继续保留泛型并把约束原样重写CountingBoxT : OrderedBoxT where T : ComparableT。Q7标准库的OptionT和第 18 课手写的MyOptionT是什么关系它们是同一种泛型枚举形态enum OptionT { Some(T) | None }。你完全可以把本课的泛型知识当作看懂标准库类型签名的钥匙——以后见到ArrayListT、IteratorT、HashMapK, V尖括号里的字母意思都一样使用时才确定的类型占位符。十、课后练习写泛型函数firstT(values: ArrayT): OptionT空数组返回None再写containsT(values: ArrayT, target: T): Bool想想它需要T满足什么约束。写泛型函数reverseT(values: ArrayT): Unit原地把数组反转利用本课的swapAt思路用[1, 2, 3, 4, 5]和字符串数组分别测试。给第六节的StackT增加两个方法peek(): OptionT只看栈顶不弹出和contains(item: T): Bool注意给T加必要约束。定义泛型结构体PairA, B两个字段first、second实现ToString让它能直接println——先想清楚约束该怎么写输出形如(键, 值)。扩展第八节的RepositoryK, V增加update(key: K, value: V): Bool只有键已存在时才更新不存在返回 false和keys(): ArrayListK各写一个RepositoryInt64, String和RepositoryString, Int64验证全部方法。下节预告泛型让你能复用逻辑但有时候你只是想给一个已经存在、又不能改源码的类型比如标准库的String补几个顺手的方法——既不想继承也不想写一堆工具函数。第 20 课扩展、类型别名与可见性控制将讲解仓颉的扩展机制不加子类也能给类型加方法、typealias类型别名以及public/private可见性控制把面向对象这一模块收尾。系列说明本系列基于 Windows 平台 CIDE 仓颉 SDK1.2.0编写所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入以你本地版本为准欢迎评论区交流。 遇到问题扫码联系作者跟着课程练习时如果在 SDK 安装、环境变量配置、编译报错或调试上卡住欢迎扫码加作者企业微信直接咨询请备注仓颉课程离线环境下图片可能加载不出来也可以在 CIDE 菜单Help ▸ 联系作者 / Contact中查看同一张二维码应用内置兜底图无需联网。 工具下载本系列全程使用的仓颉 IDE ——CIDE免费开源、社区版GitCode 仓库 / 安装包下载https://gitcode.com/wp_upala/cide打开页面后进入发行版Releases两种包任选其一安装版下载CIDE-版本-x64-Setup.exe双击安装适合日常长期使用免安装版Portable下载CIDE-版本-x64-Portable.zip解压到任意目录即用不写注册表、不留安装痕迹拷到 U 盘也能在别的电脑直接运行包内附《使用说明.txt》。适合先试用、或在受限电脑上学习本系列课程。仓颉 SDK 请前往仓颉编程语言官网下载https://cangjie-lang.cn
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。