仓颉编程语言青少年基础教程:泛型(Generic)和区间(Range)类型
仓颉编程语言青少年基础教程:泛型(Generic)和区间(Range)类型
本文介绍仓颉编程语言中的泛型(Generic)和区间(Range)类型。泛型允许我们编写更加通用、类型安全的代码。通过泛型,可以在定义函数、类、接口等时使用类型参数,实现代码复用。区间(Range )类型是一个泛型,用于表示一个拥有固定步长的序列。
泛型(Generic)
泛型是一种语言特性、一种编程范式,它允许你编写出参数化的类型和函数。你可以把它想象成一个可以生产具体数据类型的模板 / 机制。
泛型是仓颉编程语言中的核心特性,对于想要熟练使用仓颉进行开发的程序员来说,深入学习泛型是必不可少的。泛型作为一种增强代码复用性和类型安全性的高级特性,需要建立在理解类型系统、函数、类等基础概念之上。
在仓颉编程语言中,泛型指的是参数化类型,参数化类型是一个在声明时未知并且需要在使用时指定的类型。
先给出一个直观示例:
// 交换两个值的泛型函数
func swap<T>(a: T, b: T) {return (b, a)
}// 查找最大值的泛型函数
func maxV<T>(a: T, b: T): T where T <: Comparable<T> {return if (a > b) {a} else {b}
}main(): Int64 {// 1. 整数var x = 10var y = 20(x, y) = swap(x, y) // 解包重新赋值println("x = ${x}, y = ${y}") // x = 20, y = 10// 2. 浮点var a = 10.30var b = 20.20(a, b) = swap(a, b)println("a = ${a}, b = ${b}") // a = 20.200000, b = 10.300000// 3. 最大值println("最大值: ${maxV(a, b)}") // 20.200000return 0
}
这个示例中的Comparable<T> 是std.core包中的一个接口表示比较运算,是等于、小于、大于、小于等于、大于等于接口的集合体。core 包不需要显式导入,默认导入(“隐式导入”)。
max<T>(T, T, Array<T>) where T <: Comparable<T> 获取一组数据中的最大值。
编译运行截图:
泛型是仓颉语言中处理参数化类型的机制:定义时声明类型形参,使用时传入类型实参,在保持类型安全的前提下提高代码复用性。
简单地说,“泛型 = 参数化类型:声明时未知,使用时由调用者填入具体类型”。
几个常用的术语:
• 类型形参(Type parameter):一个类型或者函数声明可能有一个或者多个需要在使用处被指定的类型,这些类型就被称为类型形参。在声明形参时,需要给定一个标识符,以便在声明体中引用。
• 类型变元(Type variable):在声明类型形参后,当通过标识符来引用这些类型时,这些标识符被称为类型变元。
• 类型实参(Type argument):当在使用泛型声明的类型或函数时指定了泛型参数,这些参数被称为类型实参。
• 类型构造器(Type constructor):一个需要零个、一个或者多个类型作为实参的类型称为类型构造器。
在仓颉中,仓颉支持的泛型有 5种:function、class、interface、struct 与 enum 的声明都可以声明类型形参,也就是说它们都可以是泛型的。
声明种类 | 泛型头位置 | 示例 |
函数 | 函数名之后 | func id<T>(x: T): T { x } |
类/接口/结构体/枚举 | 类型名之后 | class Box<T> { var v: T = _ } |
类型别名 | 别名名之后 | type Result<T> = Option<T> |
其中,T 是 “类型形参”或称为“占位符”,代表 任意某个具体类型,但具体是哪一个类型,要等到调用/使用时才确定。具体的说:
• 定义时:T是惯用名字,可以给它起任何名字(U、Key、Value…)。
• 使用时:你把这张空白名片填上真实类型(Int64、String、User…),编译器就帮你生成一份专属于这个类型的代码。
使用时,把 <T> 替成具体类型:有两种方式:
- 显式实参(可省,编译器可推断),如:
let x = id<Int64>(42)
- 隐式实参,如:
let x = id(42) // 推断为 Int64
约束分为接口约束与 class 类型约束。语法为在函数、类型的声明体之前使用 where 关键字来声明,对于声明的泛型形参 T1, T2,可以使用 where T1 <: Interface, T2 <: Class 这样的方式来声明泛型约束,同一个类型变元的多个约束可以使用 & 连接。例如:where T1 <: Interface1 & Interface2。
仓颉语言的where 约束规则:
• 只能落在函数/类/接口/结构体/枚举的 尾部——名字 + 泛型头 + 形参列表 + 返回类型)尾部左大括号 { 之前;
• 多个约束用 & 连接;
• 类约束要求所有类必须在同一继承链上;
• 接口约束可任意组合;
• 类型别名不能写 where,约束要放到使用处。
下面结合示例展开介绍。
泛型函数
泛型函数在函数名后用<>声明类型形参,可在参数、返回值和函数体中使用。
示例:泛型函数
//简单的泛型函数:返回输入值本身
func id<T>(a: T): T {return a
}// 泛型函数组合:将两个函数串联执行
func compose<T1, T2, T3>(f: (T1) -> T2, g: (T2) -> T3): (T1) -> T3 {return { x: T1 => g(f(x)) }
}// 使用组合函数
func add2(a: Int64): Int64 {return a + 2
}func multiply3(a: Int64): Int64 {return a * 3
}main() {// 直接使用id函数println(id<Int64>(10)) // 输出:10println(id<String>("hello")) // 输出:hello// 使用组合函数:先加2再乘3let add2ThenMultiply3 = compose<Int64, Int64, Int64>(add2, multiply3)println(add2ThenMultiply3(5)) // 输出:21 (5+2=7, 7*3=21)return 0
}
编译运行截图:
示例2:成员泛型函数
类、结构体等的成员函数也可声明为泛型,源码:
class Printer {// 泛型成员函数:只能打印实现了ToString接口的类型func print<T>(data: T) where T <: ToString {println(data.toString())}
}main() {let p = Printer()p.print<Int64>(123) // 输出:123p.print<String>("test") // 输出:testreturn 0
}
类、结构体、枚举中使用泛型
示例1:类使用泛型:
import std.collection.* // ArrayList<T>// 泛型栈
class Stack<T> {private var items: ArrayList<T> = ArrayList<T>() // 空列表public func push(item: T): Unit {items.add(item) // List 的 add 尾插}public func pop(): Option<T> {if (items.isEmpty()) {return None}let lastIdx = items.size - 1let val = items[lastIdx]items.remove(at:lastIdx) // 删除最后一个return Some(val)}public func peek(): Option<T> {return if (items.isEmpty()) {None} else {Some(items[items.size - 1])}}public func isEmpty(): Bool {return items.isEmpty()}
}main(): Int64 {// 整数栈let intStack = Stack<Int64>()intStack.push(1)intStack.push(2)intStack.push(3)while (!intStack.isEmpty()) {match (intStack.pop()) {case Some(value) => println("弹出: ${value}")case None => ()}}// 字符串栈let strStack = Stack<String>()strStack.push("Hello")strStack.push("World")match (strStack.peek()) {case Some(top) => println("栈顶元素: ${top}")case None => ()}return 0
}
编译运行输出:
弹出: 3
弹出: 2
弹出: 1
栈顶元素: World
示例2:结构体使用泛型:
// 1. 约束写在结构体泛型参数上
struct Pair<T, U> where T <: ToString, U <: ToString {var first: T // 需要可变时用 varvar second: Upublic init(first: T, second: U) {this.first = firstthis.second = second}// 2. 普通成员函数,不再写 wherepublic func toString(): String {return "(${first}, ${second})"}
}// 泛型可选值容器
struct Box<T> {private var value: Option<T> = Nonepublic init() {} // 空盒public init(value: T) {this.value = Some(value)}public func get(): Option<T> {return value}public mut func set(newValue: T): Unit { // mut 关键字value = Some(newValue)}
}main(): Int64 {let pair = Pair("Age", 25)println(pair.toString()) // (Age, 25)let box = Box<Int64>(42)match (box.get()) {case Some(v) => println("Box contains: ${v}") // Box contains: 42case None => ()}return 0
}
编译运行输出:
(Age, 25)
Box contains: 42
泛型接口
接口定义中使用泛型类型参数。示例:
import std.collection.* // ArrayList<T>// 1. 接口方法
interface Container<T> {func add(item: T): Unitfunc remove(): Option<T>func size(): Int64func isEmpty(): Bool
}// 2. 实现接口时同样加 public
class Queue<T> <: Container<T> {private var items: ArrayList<T> = ArrayList<T>()public func add(item: T): Unit {items.add(item) // ArrayList 的尾插}public func remove(): Option<T> {if (items.isEmpty()) {return None}let head = items[0]items.remove(at: 0) // 删除头元素return Some(head)}public func size(): Int64 {return items.size}public func isEmpty(): Bool {return items.isEmpty()}
}main(): Int64 {let queue: Container<String> = Queue<String>()queue.add("First")queue.add("Second")queue.add("Third")while (!queue.isEmpty()) {match (queue.remove()) {case Some(item) => println("处理: ${item}")case None => ()}}return 0
}
编译运行输出:
处理: First
处理: Second
处理: Third
泛型约束
通过where关键字限制类型形参必须满足的条件(如实现特定接口或继承特定类)。
示例1:
import std.collection.* //ArrayList<T>用到// 定义"可计算面积"接口
interface AreaCalculable {func getArea(): Float64
}// 圆形(实现接口)
class Circle <: AreaCalculable {var radius: Float64public init(r: Float64) {radius = r}public func getArea(): Float64 {return 3.14 * radius * radius}
}// 矩形(实现接口)
class Rectangle <: AreaCalculable {var width: Float64var height: Float64public init(w: Float64, h: Float64) {width = wheight = h}public func getArea(): Float64 {return width * height}
}// 泛型工具类:计算面积总和(约束T必须实现AreaCalculable)
class AreaTool<T> where T <: AreaCalculable {public static func sumAreas(shapes: ArrayList<T>): Float64 {var total: Float64 = 0.0for (shape in shapes) {total += shape.getArea()}return total}
}main() {let shapes = ArrayList<AreaCalculable>()shapes.add(Circle(2.0)) // 面积≈12.56shapes.add(Rectangle(3.0, 4.0)) // 面积=12.0let total = AreaTool<AreaCalculable>.sumAreas(shapes)println("总面积:${total}") // 输出:总面积:24.560000return 0
}
示例2:
import std.collection.*// 要求类型参数必须实现Comparable接口
func sort<T>(items: ArrayList<T>) where T <: Comparable<T> {let n = items.sizefor (i in 0..n) {for (j in 0..n-i-1) {if (items[j] > items[j+1]) {let temp = items[j]items[j] = items[j+1] // ArrayList 支持下标赋值items[j+1] = temp}}}
}// 多个约束条件:ToString + Comparable
func process<T>(value: T) where T <: ToString & Comparable<T> {println("Processing: ${value}")// 可以使用Printable和Comparable的方法
}//简单 Collection 接口
interface Collection<T> {func add(item: T): Unitfunc get(index: Int64): Option<T>
}main() {// ArrayList 字面量写法let numbers = ArrayList<Int64>([3, 1, 4, 1, 5, 9, 2])sort(numbers)// 打印结果print("排序后: [")for (i in 0..numbers.size) {print("${numbers[i]}")if (i < numbers.size - 1) {print(", ")}}println("]") // [1, 1, 2, 3, 4, 5, 9]
}
类型别名与泛型别名
类型不够直观时,可以使用type关键字定义别名,提高代码可读性。类型别名的定义以关键字 type 开头,接着是类型的别名,然后是等号 =,最后是原类型。
当一个泛型类型的名称过长时,可以使用类型别名来为其声明一个更短的别名。
示例1:
import std.collection.*// 1. 基础类型别名
type IntArray = ArrayList<Int64>
type StringMap = HashMap<String, String>// 2. 泛型别名
type Result<T> = Option<T>
type Callback<T> = (T) -> Unit// 3. 复杂泛型别名
type Graph<T> = HashMap<T, ArrayList<T>>main(): Int64 {// 4. 整数列表let numbers: IntArray = IntArray([1, 2, 3, 4, 5])// 5. 字符串映射let config: StringMap = StringMap()config["name"] = "仓颉"config["version"] = "1.0.1"// 6. 图结构let graph: Graph<String> = Graph()graph["A"] = ArrayList(["B", "C"])graph["B"] = ArrayList(["D"])println("配置: ${config}")println("图结构: ${graph}")return 0
}
编译运行输出:
配置: [(name, 仓颉), (version, 1.0.1)]
图结构: [(A, [B, C]), (B, [D])]
示例2:
// 简单类型别名
type I64 = Int64
type Str = String// 泛型类型别名(为泛型链表定义短别名)
class LongNameGenericClass<T> {var data: Tpublic init(data: T) {this.data = data}
}
type ShortGen<T> = LongNameGenericClass<T>main() {// 使用简单别名let a: I64 = 100let b: Str = "别名测试"println("${a}, ${b}") // 输出:100 别名测试// 使用泛型别名let obj = ShortGen<Bool>(true)println(obj.data) // 输出:truereturn 0
}
泛型小结:
仓颉的泛型系统提供了强大而灵活的类型参数化能力:
• 类型安全:在编译时检查类型,避免运行时错误
• 代码复用:一份代码适用于多种类型
• 性能优化:编译器可以针对具体类型生成优化代码
• 约束机制:通过where子句限制类型参数,确保必要的操作可用
泛型是现代编程语言的重要特性,掌握泛型编程能够帮助我们写出更加优雅、高效的代码。
最后给出一个泛型应用示例:缓存系统
import std.collection.*class Cache<K, V> where K <: Hashable & Equatable<K> {private var storage: HashMap<K, V> = HashMap<K, V>()private var maxSize: Int64private var accessOrder: ArrayList<K> = ArrayList<K>()public init(maxSize: Int64) { this.maxSize = maxSize }public init() { this.maxSize = 100 }public func put(key: K, value: V): Unit {if (storage.size >= maxSize && !storage.contains(key)) {if (accessOrder.size > 0) {let oldestKey = accessOrder[0]accessOrder.remove(at: 0)storage.remove(oldestKey)}}storage[key] = valueupdateAccessOrder(key)}public func get(key: K): Option<V> {if (storage.contains(key)) {updateAccessOrder(key)return Some(storage[key])}return None}private func updateAccessOrder(key: K): Unit {var i = 0while (i < accessOrder.size) {if (accessOrder[i] == key) { accessOrder.remove(at: i); continue }i += 1}accessOrder.add(key)}public func clear(): Unit { storage.clear(); accessOrder.clear() }public func size(): Int64 { return storage.size }
}main(): Int64 {let cache = Cache<String, String>(3)cache.put("user1", "Alice")cache.put("user2", "Bob")cache.put("user3", "Charlie")println("获取user1: ${cache.get("user1")}") // Some("Alice")cache.put("user4", "David") // 挤出 user2println("获取user2: ${cache.get("user2")}") // Noneprintln("获取user4: ${cache.get("user4")}") // Some("David")println("缓存大小: ${cache.size()}") // 3return 0
}
编译运行输出:
获取user1: Some(Alice)
获取user2: None
获取user4: Some(David)
缓存大小: 3
区间(Range)类型
Range 类型——区间类型是一个泛型,使用 Range<T> 表示——用于表示一个拥有固定步长的序列,并且是一个不可变(immutable)类型。
Range<T>是仓颉内置的泛型类型。区间类型 Range<T> 表示一个序列,支持步长设置。字面量形式为 start..end : step(左闭右开)和 start..=end : step(左闭右闭)。
每个 Range类型的实例都会包含 start、end 和 step 值。其中,start 和 end 分别表示序列的起始值和终止值,step 表示序列中前后两个元素之间的差值。
区间字面量有两种形式:“左闭右开”区间和“左闭右闭”区间,区别在于是否包含end值:
• “左闭右开”区间的格式是 start..end : step,它表示一个从 start 开始,以 step 为步长,到 end(不包含 end)为止的区间;
• “左闭右闭”区间的格式是 start..=end : step,它表示一个从 start 开始,以 step 为步长,到 end(包含 end)为止的区间。
区间字面量中,可以不写 step,此时 step 默认等于 1,但是step 的值不能等于 0(步长为 0 会导致序列无法推进,无意义)。另外,区间也有可能是空的(即不包含任何元素的空序列)。
Range 类型支持使用 == 进行判等(使用 != 进行判不等),两个相同类型的 Range 实例相等,当且仅当它们同时为“左闭右开”或“左闭右闭”,并且它们的 start 值、end 值和 step 值均对应相等。
示例:
// 区间类型示例程序
main() {// 1. 左闭右开区间(start..end[:step])let range1 = 1..5; // 步长默认1,不包含5let range2 = 2..10:2; // 步长2,不包含10let range3 = 10..3:-2; // 负步长(递减),不包含3// 2. 左闭右闭区间(start..=end[:step])let range4 = 1..=5; // 步长默认1,包含5let range5 = 2..=10:2; // 步长2,包含10let range6 = 10..=3:-2; // 负步长(递减),包含3// 打印区间元素(遍历区间)println("左闭右开区间 1..5 的元素:");for (num in range1) {print("${num}, ");}println(); // 输出:1, 2, 3, 4,println("左闭右开区间 2..10:2 的元素:");for (num in range2) {print("${num}, ");}println(); // 输出:2, 4, 6, 8,println("左闭右开区间 10..3:-2 的元素:");for (num in range3) {print("${num}, ");}println(); // 输出:10, 8, 6, 4,println("左闭右闭区间 1..=5 的元素:");for (num in range4) {print("${num}, ");}println(); // 输出:1, 2, 3, 4, 5,println("左闭右闭区间 2..=10:2 的元素:");for (num in range5) {print("${num}, ");}println(); // 输出:2, 4, 6, 8, 10,println("左闭右闭区间 10..=3:-2 的元素:");for (num in range6) {print("${num}, ");}println(); // 输出:10, 8, 6, 4,// 3. 区间判等(== 和 !=)let a = 1..5;let b = 1..5;let c = 1..=5;let d = 1..5:2;println("a == b:${a == b}"); // 输出:true(同类型且参数相同)println("a == c:${a == c}"); // 输出:false(类型不同)println("a == d:${a == d}"); // 输出:false(步长不同)// 4. 空区间判断(通过遍历是否有元素)let empty1 = 5..1; // 正步长但start > endlet empty2 = 1..5:-1; // 负步长但start < endlet empty3 = 3..3; // 左闭右开,start == endprintln("判断空区间:");println("empty1 是否为空:${is_empty(empty1)}"); // 输出:trueprintln("empty2 是否为空:${is_empty(empty2)}"); // 输出:trueprintln("empty3 是否为空:${is_empty(empty3)}"); // 输出:true
}// 判断区间是否为空的函数
func is_empty(range:Range<Int64>) {for (num in range) {return false; // 如果有元素,返回 false}return true; // 如果没有元素,返回 true
}
编译运行输出:
左闭右开区间 1..5 的元素:
1, 2, 3, 4,
左闭右开区间 2..10:2 的元素:
2, 4, 6, 8,
左闭右开区间 10..3:-2 的元素:
10, 8, 6, 4,
左闭右闭区间 1..=5 的元素:
1, 2, 3, 4, 5,
左闭右闭区间 2..=10:2 的元素:
2, 4, 6, 8, 10,
左闭右闭区间 10..=3:-2 的元素:
10, 8, 6, 4,
a == b:true
a == c:false
a == d:false
判断空区间:
empty1 是否为空:true
empty2 是否为空:true
empty3 是否为空:true