仓颉编程(21)扩展
扩展
一、扩展机制概述
1.1 什么是扩展
在仓颉编程语言中,扩展(Extension)是一种为现有类型添加新功能而不修改原始类型定义的机制。这个概念可能对你来说有些抽象,让我用一个生活中的例子来解释。
假设你有一辆自行车,它原本只有两个轮子和一个车架。现在你想要给它添加一个铃铛、一个车灯或者一个载物筐,但你不想改变自行车本身的结构。扩展就像是这些额外的配件,它们可以增强自行车的功能,但不会改变自行车的本质。
在编程中,类型就像是自行车的设计图,而扩展就是我们为这个设计图添加的额外功能。比如,如果你想要为字符串类型(String)添加一个计算长度的功能,或者为数字类型添加一个判断是否为质数的方法,都可以通过扩展来实现。
1.2 扩展的基本概念
根据华为官方文档的定义,扩展可以为在当前 package 可见的类型(除函数、元组、接口)添加新功能。这意味着你可以对类、结构体等类型进行扩展,但不能对函数、元组和接口本身进行扩展。
扩展可以添加的功能包括:
- 添加成员函数:为类型增加新的方法
- 添加操作符重载函数:为类型提供自定义的操作符行为
- 添加成员属性:为类型增加新的属性
- 实现接口:使类型符合新的接口要求
需要特别注意的是,扩展不能增加成员变量。这是因为扩展的设计理念是 "只能扩展行为,不能扩展存储"。也就是说,扩展只能为类型添加新的功能方法和属性,但不能为类型添加新的数据存储。
1.3 扩展的设计理念
仓颉语言的扩展机制体现了几个重要的设计理念:
非侵入性设计是扩展机制最核心的理念。扩展允许我们在不修改原始类型代码的情况下添加功能,这保持了类型的封装性。这种设计有很多好处,比如:
- 不会影响原有代码的稳定性
- 可以安全地为第三方库添加功能
- 支持模块化开发和代码复用
灵活性和可组合性是扩展的另一个重要特性。扩展支持泛型约束和接口实现,多个扩展可以协同工作。这意味着你可以根据不同的需求,为同一个类型定义多个扩展,每个扩展专注于特定的功能领域。
类型安全性是仓颉语言的一贯追求。扩展遵循仓颉的类型系统规则,确保了类型安全。这意味着在编译阶段就能发现类型不匹配的错误,而不是在运行时才发现问题。
1.4 扩展与继承的区别
作为初学者,你可能会问:扩展和继承有什么区别?为什么不直接使用继承来实现功能扩展?
在仓颉语言中,类默认是 "关闭的",也就是不可被继承,需要使用open修饰符才能够被继承。这种设计理念表明,仓颉更希望通过组合的方式来实现类的扩展,而不是继承。
扩展与继承的主要区别在于:
- 继承是一种 "是一种"(is-a)的关系,子类继承父类的所有成员
- 扩展是一种 "添加功能" 的关系,不影响类型的继承体系
举个例子,如果你有一个Animal类,然后创建了一个Dog类继承自Animal,那么Dog就是一种Animal。但如果你为Animal类添加了一个扩展,那么这个扩展只是为Animal类增加了新的功能,并不会创建新的类型。
1.5 扩展的优势
使用扩展机制有很多实际的好处:
提高代码复用性:你可以为标准库类型或第三方库类型添加通用功能,而不需要修改这些库的源代码。比如,你可以为String类型添加一个toCamelCase方法,让所有字符串都能方便地转换为驼峰命名格式。
增强代码可维护性:扩展的功能可以独立维护和测试,不会影响原始类型的功能。这使得代码的修改和维护更加安全和高效。
支持领域特定语言(DSL)构建:结合仓颉的元编程能力和扩展机制,可以轻松构建领域专用语言。比如,你可以为数学计算领域定义特定的操作符和方法。
实现设计模式:扩展可以方便地实现装饰器、策略等设计模式,使代码结构更加清晰和灵活。
二、直接扩展详解
2.1 直接扩展的定义和语法
直接扩展是指为现有类型添加新功能,而不涉及新的接口实现。这是扩展机制中最简单、最常用的形式。
直接扩展的基本语法结构如下:
extend 类型名 {// 扩展的成员(函数、属性、操作符重载)
}让我们来看一个简单的例子。假设我们想要为String类型添加一个printSize方法,用于打印字符串的长度,代码可以这样写:
extend String {public func printSize() {println("字符串的长度是: ${this.size}")}
}在这个例子中,我们使用extend关键字声明了一个扩展,后面跟着被扩展的类型String,然后是一对大括号,里面定义了扩展的具体内容。这里我们添加了一个公共的(public)函数printSize,它使用this关键字来访问当前字符串实例的size属性。
2.2 直接扩展的实现原理
作为初学者,你可能会好奇:扩展是如何在不修改原始类型的情况下为其添加功能的?
在仓颉语言中,扩展是在编译时处理的。编译器会将扩展的成员 "合并" 到被扩展的类型中,使得这些新功能在使用时就像是类型本身的成员一样。
需要特别注意的是,扩展不能访问被扩展类型的private成员。这是因为扩展必须遵守类型的封装性原则,不能破坏原有的访问控制。同时,扩展中的成员不能使用super关键字,因为扩展不是继承关系。
扩展的另一个重要限制是,不能遮盖(override)被扩展类型的任何成员。这意味着如果你在扩展中定义了一个与原类型同名的方法,编译器会报错。这个限制保证了扩展不会意外地改变原有功能的行为。
2.3 直接扩展的修饰符使用
在直接扩展中,你可以为成员添加各种修饰符,以控制它们的访问权限和行为:
- public:公共成员,在任何地方都可以访问
- protected:受保护成员,在本包内和包外子类中可见(仅类类型适用)
- private:私有成员,仅在本扩展内可见
- static:静态成员,只能通过类型名访问,不能通过实例对象访问
- mut:可变函数,用于struct类型,可以修改被扩展类型的成员
下面是一个展示各种修饰符用法的例子:
extend String {public func publicMethod() {println("这是一个公共方法")}protected func protectedMethod() {println("这是一个受保护方法")}private func privateMethod() {println("这是一个私有方法")}static func staticMethod() {println("这是一个静态方法")}mut func mutableMethod() {// 这里可以修改String的可变成员println("这是一个可变方法")}
}2.4 泛型类型的直接扩展
直接扩展也支持泛型类型,仓颉提供了两种扩展泛型类型的方式:
第一种:特定泛型实例化类型的扩展
这种方式用于为特定的泛型实例化类型添加功能,语法如下:
extend 泛型类型<具体类型> {// 扩展的成员
}例如,我们有一个泛型容器类Container<T>,现在想要为Container<Int64>添加一些特定功能:
class Container<T> {var data: Tpublic init(value: T) {data = value}
}
extend Container<Int64> {public func doubleValue() {data = data * 2}public func isEven() -> Bool {return data % 2 == 0}
}在这个例子中,我们为Container<Int64>添加了doubleValue方法(将值翻倍)和isEven方法(判断是否为偶数)。
第二种:泛型形参的泛型扩展
这种方式用于为泛型类型本身添加功能,语法如下:
extend<泛型参数> 泛型类型<泛型参数> {// 扩展的成员
}例如,我们想要为Container<T>添加一些通用功能:
extend<T> Container<T> {public func getValue() -> T {return data}public func setValue(newValue: T) {data = newValue}
}这个扩展为所有类型的Container都添加了getValue和setValue方法。
你还可以为泛型扩展添加约束,以实现特定条件下的功能:
interface Comparable<T> {func equals(other: T) -> Bool
}
extend<T> Container<T> where T <: Comparable<T> {public func equals(other: Container<T>) -> Bool {return this.data.equals(other.data)}
}这个扩展为实现了Comparable接口的类型的Container添加了equals方法。
2.5 直接扩展的使用场景
直接扩展在实际开发中有很多实用的场景,让我为你介绍几个常见的应用:
场景一:为内置类型添加便捷方法
标准库中的类型可能缺少一些常用的功能,我们可以通过扩展来添加。例如,为String类型添加一些文本处理功能:
extend String {// 反转字符串public func reverse() -> String {let rs = this.clone().toRuneArray()rs.reverse()return String.fromRuneArray(rs)}// 统计指定字符出现的次数public func countChar(char: Rune) -> Int64 {var count = 0for c in this.toRuneArray() {if c == char {count += 1}}return count}// 转换为驼峰命名public func toCamelCase() -> String {var result = ""var capitalizeNext = falsefor char in this {if char == '_' {capitalizeNext = true} else {if capitalizeNext {result.append(char.toUpperCase())capitalizeNext = false} else {result.append(char)}}}return result}
}使用这些扩展的例子:
let text = "hello_world"
println("原字符串: $text")
println("反转: $text.reverse()")
println("字符'l'出现次数: $text.countChar(r'l')")
println("驼峰命名: $text.toCamelCase()")场景二:为第三方库类型添加功能
当你使用第三方库时,如果发现某个类型缺少你需要的功能,可以通过扩展来添加,而不需要修改第三方库的代码。
例如,假设你使用了一个第三方的Date库,但它缺少一些日期计算功能:
extend Date {public func addDays(days: Int64) -> Date {// 实现添加天数的逻辑}public func addWeeks(weeks: Int64) -> Date {return self.addDays(days: weeks * 7)}
}场景三:实现操作符重载
你可以通过扩展为类型添加自定义的操作符行为。例如,为Vector2类型实现加法操作:
class Vector2 {var x: Float64var y: Float64public init(x: Float64, y: Float64) {this.x = xthis.y = y}
}
extend Vector2 {public operator func +(other: Vector2) -> Vector2 {return Vector2(x: this.x + other.x, y: this.y + other.y)}
}这样你就可以使用+操作符来相加两个向量:
let v1 = Vector2(x: 1.0, y: 2.0)
let v2 = Vector2(x: 3.0, y: 4.0)
let v3 = v1 + v2 // 等价于 v1.+(v2)场景四:添加计算属性
你可以为类型添加计算属性,这些属性的值是根据其他属性计算得出的:
extend Int64 {public prop isPrime: Bool {get() {if this <= 1 {return false}for i in 2..=sqrt(this) {if this % i == 0 {return false}}return true}}public prop factorial: Int64 {get() {if this <= 1 {return 1}return this * (this - 1).factorial}}
}使用这些计算属性:
let number = 17
println("$number 是质数: $number.isPrime")
println("$number 的阶乘: $number.factorial")2.6 直接扩展的最佳实践
作为初学者,在使用直接扩展时应该遵循一些最佳实践:
1. 单一职责原则
每个扩展应该专注于一个特定的功能领域。例如,不要在一个扩展中既添加数学计算功能,又添加字符串处理功能。
2. 命名一致性
扩展的命名和风格应该与被扩展类型保持一致。如果原类型的方法使用驼峰命名,那么扩展的方法也应该使用驼峰命名。
3. 避免过度扩展
不要为一个类型添加过多的功能,这会使代码变得难以理解和维护。如果某个扩展变得过于复杂,应该考虑将其拆分成多个扩展。
4. 性能考虑
在扩展中实现方法时,要考虑性能影响。避免在扩展中实现低效的算法,特别是对于经常使用的方法。
5. 文档化
为扩展提供清晰的文档和示例,这有助于其他开发者理解和使用你的扩展。
三、接口扩展详解
3.1 接口扩展的定义和语法
接口扩展是指包含接口实现的扩展,它可以为现有类型添加新功能并实现接口,增强抽象灵活性。这是扩展机制中更加强大的功能,它允许我们为已有的类型赋予新的接口能力。
接口扩展的基本语法结构如下:
extend 类型名 <: 接口名 {// 实现接口的成员
}与直接扩展的区别在于,接口扩展使用<:符号来指定要实现的接口。如果要实现多个接口,可以使用&符号分隔:
extend 类型名 <: 接口名1 & 接口名2 & 接口名3 {// 实现接口的成员
}3.2 接口扩展的实现原理
接口扩展的实现原理与直接扩展类似,都是在编译时将扩展的成员 "合并" 到被扩展的类型中。但接口扩展有一个重要的特点:它实际上是在为类型添加一个新的接口实现。
让我们通过一个例子来理解。假设我们有一个Shape接口:
interface Shape {func area() -> Float64func perimeter() -> Float64
}现在,我们想要为Circle类实现这个接口:
class Circle {var radius: Float64public init(radius: Float64) {this.radius = radius}
}
extend Circle <: Shape {public func area() -> Float64 {return 3.14159 * radius * radius}public func perimeter() -> Float64 {return 2 * 3.14159 * radius}
}在这个例子中,Circle原本并没有实现Shape接口,但通过接口扩展,我们为它添加了area和perimeter方法,使它成为了Shape接口的一个实现。
3.3 接口扩展与直接扩展的区别
作为初学者,理解接口扩展与直接扩展的区别非常重要。让我通过对比表格来清晰地展示它们的差异:
| 特性 | 直接扩展 | 接口扩展 | 
| 语法 | extend 类型名 { ... } | extend 类型名 <: 接口名 { ... } | 
| 功能 | 为类型添加成员(函数、属性、操作符) | 为类型添加接口实现 | 
| 接口实现 | 不涉及接口 | 必须实现接口的所有抽象成员 | 
| 类型关系 | 不改变类型的接口实现 | 使类型成为接口的子类型 | 
| 使用场景 | 为类型添加额外功能 | 为类型赋予新的抽象能力 | 
| 扩展性 | 功能扩展 | 接口能力扩展 | 
举个简单的例子,如果我们想要为String类型添加一个print方法,使用直接扩展就可以了:
extend String {public func print() {println(this)}
}但如果我们想要让String实现一个Printable接口,就需要使用接口扩展:
interface Printable {func print() -> Unit
}
extend String <: Printable {public func print() {println(this)}
}3.4 多接口实现
接口扩展的一个强大特性是可以同时实现多个接口。例如,我们可以定义多个接口:
interface Drawable {func draw() -> Unit
}
interface Movable {func move(x: Int64, y: Int64) -> Unit
}
interface Resizable {func resize(width: Int64, height: Int64) -> Unit
}然后为Rectangle类同时实现这三个接口:
class Rectangle {var x: Int64 = 0var y: Int64 = 0var width: Int64 = 100var height: Int64 = 50
}
extend Rectangle <: Drawable & Movable & Resizable {public func draw() {println("绘制矩形: 位置($x, $y), 尺寸($width x $height)")}public func move(x: Int64, y: Int64) {this.x = xthis.y = yprintln("矩形移动到: ($x, $y)")}public func resize(width: Int64, height: Int64) {this.width = widththis.height = heightprintln("矩形尺寸调整为: $width x $height")}
}这样,Rectangle就具有了可绘制、可移动和可调整大小的能力。我们可以通过接口类型来使用这些功能:
let rect = Rectangle()
let drawable: Drawable = rect
let movable: Movable = rect
let resizable: Resizable = rect
drawable.draw()
movable.move(x: 10, y: 20)
resizable.resize(width: 200, height: 100)3.5 带约束的接口扩展
接口扩展也支持泛型约束,这使得我们可以为特定类型的实例实现接口。例如:
interface Serializable {func serialize() -> String
}
interface Comparable<T> {func equals(other: T) -> Bool
}
class Pair<T1, T2> {let first: T1let second: T2public init(a: T1, b: T2) {first = asecond = b}
}
// 为实现了Serializable接口的类型对添加序列化功能
extend<T1, T2> Pair<T1, T2> <: Serializable where T1 <: Serializable, T2 <: Serializable {public func serialize() -> String {return "($(first.serialize()), $(second.serialize()))"}
}
// 为实现了Comparable接口的类型对添加比较功能
extend<T1, T2> Pair<T1, T2> <: Comparable<Pair<T1, T2>> where T1 <: Comparable<T1>, T2 <: Comparable<T2> {public func equals(other: Pair<T1, T2>) -> Bool {return first.equals(other.first) && second.equals(other.second)}
}3.6 接口扩展的使用场景
接口扩展在实际开发中有很多重要的应用场景:
场景一:为现有类型添加新的接口能力
假设我们有一个第三方的Logger类,但它没有实现我们需要的Loggable接口。我们可以通过接口扩展来解决这个问题:
interface Loggable {func log(message: String) -> Unit
}
// 假设这是一个第三方库的类
class ThirdPartyLogger {public func writeLog(message: String) {println("日志: $message")}
}
// 通过接口扩展使其实现Loggable接口
extend ThirdPartyLogger <: Loggable {public func log(message: String) {this.writeLog(message: message)}
}这样,ThirdPartyLogger就可以在需要Loggable接口的地方使用了。
场景二:实现接口继承体系
接口扩展可以为现有的类型添加全新的接口继承体系。例如,我们可以为所有整数类型实现Integer接口:
sealed interface Integer {}
extend Int8 <: Integer {}
extend Int16 <: Integer {}
extend Int32 <: Integer {}
extend Int64 <: Integer {}
let a: Integer = 123 // 合法,因为Int64实现了Integer接口场景三:为集合类型添加统一的接口
我们可以为不同的集合类型实现统一的接口,使它们具有一致的行为:
interface Collection {func size() -> Int64func isEmpty() -> Bool
}
// 为Array实现Collection接口
extend<T> Array<T> <: Collection {public func size() -> Int64 {return this.length}public func isEmpty() -> Bool {return this.length == 0}
}
// 为List实现Collection接口
extend<T> List<T> <: Collection {public func size() -> Int64 {return this.count}public func isEmpty() -> Bool {return this.count == 0}
}这样,Array和List都具有了Collection接口的能力,可以在需要集合的地方统一使用。
场景四:实现设计模式
接口扩展可以方便地实现各种设计模式。例如,我们可以通过接口扩展实现装饰器模式:
interface Component {func operation() -> String
}
// 具体组件
class ConcreteComponent <: Component {public func operation() -> String {return "具体组件的操作"}
}
// 装饰器接口
interface Decorator <: Component {var component: Component
}
// 通过扩展为ConcreteComponent添加装饰器功能
extend ConcreteComponent <: Decorator {public var component: Component = ConcreteComponent()public func operation() -> String {return "装饰器: " + component.operation()}
}扩展导入导出
1. 扩展导入导出的基本概念和特殊性
1.1 扩展导入导出的基本概念
在仓颉编程语言中,扩展(Extension)是一种为现有类型添加新功能而不修改原始类型定义的机制。与其他编程语言的扩展机制类似,仓颉的扩展可以为类、结构体等类型添加成员函数、操作符重载函数、成员属性以及实现接口,但不能增加成员变量或访问原类型的私有成员。
扩展导入导出是指将扩展的功能在不同包之间进行共享和使用的机制。与普通的包成员(如类、函数等)不同,扩展本身不能使用public修饰符,这使得扩展的导入导出具有独特的规则体系。
1.2 扩展的隐式导入特性
仓颉扩展机制的一个重要特性是隐式导入。扩展的导入不需要显式地使用import语句,只需要导入被扩展的类型和相关接口,就可以使用可访问的所有扩展功能。
这种隐式导入机制大大简化了扩展的使用。例如,在包b中,只需要导入Foo就可以使用Foo对应的扩展中的函数f。而对于接口扩展,需要同时导入被扩展的类型和扩展的接口才能使用。
隐式导入的原理在于,扩展的功能会随着类型的导入而自动生效。这意味着当你在代码中导入了某个类型后,该类型的所有可访问扩展都会被自动加载,无需额外的导入操作。
1.3 扩展与普通包成员在导入导出上的差异
扩展与普通包成员在导入导出方面存在显著差异:
修饰符限制:
- 普通包成员(如类、函数等)可以使用public、protected、private等修饰符来控制可见性
- 扩展本身不能使用任何修饰符,包括public
导出规则:
- 普通包成员的导出主要由其自身的访问修饰符决定
- 扩展的导出有特殊规则,需要同时考虑扩展与被扩展类型的包关系、访问修饰符等多个因素
导入方式:
- 普通包成员需要显式使用import语句导入
- 扩展采用隐式导入,随类型导入自动生效
作用范围:
- 普通包成员的作用范围由其定义位置和修饰符决定
- 扩展的作用范围受到包结构的严格限制,特别是接口扩展必须遵循 "孤儿规则"
1.4 扩展导入导出的基本规则体系
仓颉扩展的导入导出规则体系主要包括以下几个方面:
包关系要求:
- 直接扩展必须与被扩展类型在同一个包中才能被导出
- 接口扩展必须与被扩展类型或接口在同一个包中(孤儿规则)
访问修饰符要求:
- 直接扩展的导出需要被扩展类型和扩展成员都为public或protected修饰
- 接口扩展的导出分两种情况:与被扩展类型同包时需被扩展类型为public;与接口同包时需接口为public
导入要求:
- 直接扩展只需导入被扩展类型
- 接口扩展需要同时导入被扩展类型和接口
泛型约束影响:
- 泛型扩展的导出还受到泛型约束中类型的访问修饰符影响
2. 直接扩展的导入导出规则
2.1 直接扩展导出的基本规则
直接扩展是指为现有类型添加新功能而不涉及接口实现的扩展形式。直接扩展的导出规则非常严格,只有当扩展与被扩展的类型在同一个包中,并且被扩展的类型和扩展中添加的成员都使用public或protected修饰时,扩展的功能才会被导出。
这一规则可以分解为三个必要条件:
- 包关系条件:扩展与被扩展类型必须在同一个包中
- 类型修饰符条件:被扩展类型必须为public或protected修饰
- 成员修饰符条件:扩展中添加的成员必须为public或protected修饰
否则,直接扩展均不能被导出,只能在当前包使用。
例如:
// 包 a
public class Foo {}
extend Foo {public func f() {}  // 导出:满足所有条件
}
// 包 b
import a.*
extend Foo {public func g() {}  // 不能导出:不在同一包
}
// 包 c
import a.*
import b.*
main() {let a = Foo()a.f()  // OKa.g()  // Error
}2.2 直接扩展导入的规则
直接扩展的导入规则相对简单:只需要导入被扩展的类型即可,扩展会随类型的导入而隐式导入。
例如:
// 包 a
public class Foo {}
extend Foo {public func f() {}
}
// 包 b
import a.Foo  // 只需要导入 Foo 类型
main() {let a = Foo()a.f()  // 可以直接使用扩展的 f() 方法
}这种隐式导入机制的优势在于:
- 简化了代码,不需要显式导入每个扩展
- 保证了扩展功能的自动可用
- 遵循了 "扩展是类型功能的自然延伸" 这一设计理念
2.3 泛型直接扩展的导入导出规则
当直接扩展涉及泛型类型时,导入导出规则变得更加复杂,需要考虑泛型约束的影响。
泛型扩展的导出规则:
当扩展与被扩展的类型在同一个包中时,扩展是否导出由被扩展类型与泛型约束(如果有)的访问修饰符同时决定。
具体规则如下:
- 当所有的泛型约束都是导出类型(即这些类型使用public或protected修饰)时,该扩展将被导出
- 如果有任何一个泛型约束不是导出类型(例如使用private或internal修饰),则扩展不能被导出
例如:
// 包 a
public interface MyInterface {}
public class MyGenericClass<T> {}
// 扩展要求 T 实现 MyInterface
extend<T> MyGenericClass<T> where T <: MyInterface {public func myMethod() {}  // 可以导出
}
// 包 b
private interface PrivateInterface {}
public class MyGenericClass<T> {}
// 扩展要求 T 实现 PrivateInterface
extend<T> MyGenericClass<T> where T <: PrivateInterface {public func myMethod() {}  // 不能导出
}泛型扩展的导入规则与非泛型扩展相同,只需要导入被扩展的泛型类型即可。
2.4 同一包内直接扩展的可见性规则
在同一个包内,对同一类型可以定义多个直接扩展,这些扩展之间的可见性遵循以下规则:
扩展间可见性:
- 在同一个包内对同一类型可以扩展多次
- 扩展之间可以直接调用其他扩展中非private修饰的函数
例如:
class Foo {}
extend Foo {  // OKprivate func f() {}func g() {}
}
extend Foo {  // OKfunc h() {g()  // OKf()  // Error:不能访问 private 函数}
}泛型扩展的特殊规则:
当扩展涉及泛型类型时,扩展之间的可见性还取决于泛型约束:
- 如果两个扩展的约束相同,则两个扩展相互可见
- 如果两个扩展的约束不同且有包含关系,约束更宽松的扩展对约束更严格的扩展可见,反之不可见
- 当两个扩展的约束不同且不存在包含关系时,两个扩展互相不可见
例如:
open class A {}
class B <: A {}
class E<X> {}
extend<X> E<X> where X <: B {  // 扩展1:约束更严格public func f1() {f2()  // OK:可以调用扩展2的 f2()}
}
extend<X> E<X> where X <: A {  // 扩展2:约束更宽松public func f2() {f1()  // Error:不能调用扩展1的 f1()}
}2.5 不同包之间直接扩展的限制
直接扩展在不同包之间存在严格的限制:
跨包扩展的限制:
- 直接扩展只能在与被扩展类型相同的包中定义
- 不能在其他包中为其他包的类型定义直接扩展
包结构的影响:
- 子包被视为与父包不同的包
- 即使在同一个模块内,如果扩展与被扩展类型不在同一个包中,扩展也不能被导出
例如:
// 包 a
public class Foo {}
// 包 a.sub(子包)
import a.*
extend Foo {public func f() {}  // 可以导出,因为在同一个模块
}
// 包 b(不同模块)
import a.Foo
extend Foo {public func g() {}  // 不能导出,跨模块
}3. 接口扩展的导入导出规则
3.1 接口扩展导出的双重规则
接口扩展是指包含接口实现的扩展,它可以为现有类型添加新功能并实现接口,增强抽象灵活性。接口扩展的导出规则比直接扩展更加复杂,分为两种情况:
情况一:接口扩展和被扩展类型在同一个包中
- 如果接口是来自导入的,只有当被扩展类型使用public修饰时,扩展的功能才会被导出
情况二:接口扩展与接口在同一个包中
- 只有当接口是使用public修饰时,扩展的功能才会被导出
例如:
// 包 a
public class Foo {}
public interface I {func g(): Unit
}
extend Foo <: I {public func g(): Unit {}  // 可以导出
}
// 包 b
import a.I
public class Bar {}
extend Bar <: I {public func g(): Unit {}  // 可以导出,因为 I 是 public
}3.2 接口扩展导入的规则
接口扩展的导入规则比直接扩展复杂,需要同时导入多个元素:
导入要求:
需要同时导入被扩展的类型、扩展的接口和泛型约束(如果有)才能使用。
例如:
// 包 a
public class Foo {}
// 包 b
import a.Foo
public interface I {func g(): Unit
}
extend Foo <: I {public func g() {this.f()  // OK}
}
// 包 c
import a.Foo  // 导入被扩展的类型
import b.I    // 导入接口
import b.*    // 或者直接导入包 b
func test() {let a = Foo()a.f()  // OKa.g()  // OK
}这种多重导入要求的原因在于:
- 被扩展类型提供了实例对象
- 接口定义了扩展的功能契约
- 泛型约束确保了类型安全性
3.3 泛型接口扩展的导入导出规则
泛型接口扩展的导入导出规则更加复杂,需要考虑多个因素:
导出规则:
当接口扩展与被扩展类型在不同的包时,接口扩展是否导出由接口类型以及泛型约束(如果有)里用到的类型中最小的访问级别决定。
具体规则:
- 需要考虑接口本身的访问修饰符
- 需要考虑所有泛型约束的访问修饰符
- 扩展的导出级别由这些访问级别中最低的那个决定
例如:
// 包 a
public interface GenericInterface<T> {func genericMethod(): T
}
public class GenericClass {}
// 包 b
import a.GenericInterface
import a.GenericClass
// 为GenericClass实现GenericInterface<Int64>
extend GenericClass <: GenericInterface<Int64> {public func genericMethod(): Int64 {return 42}
}
// 包 c
import b.*
import a.GenericClass
main() {let obj: GenericInterface<Int64> = GenericClass()println(obj.genericMethod())  // 可以调用
}导入规则:
其他包必须导入被扩展类型、相应的接口以及约束用到的类型(如果有),才能访问对应接口包含的扩展成员。
3.4 接口扩展的 "孤儿规则"
孤儿规则是接口扩展中一个非常重要的限制,用于防止类型被意外实现不合适的接口。
规则定义:
仓颉不允许定义孤儿扩展,即既不与接口(包含接口继承链上的所有接口)定义在同一个包中,也不与被扩展类型定义在同一个包中的接口扩展。
目的:
为了防止一个类型被意外实现不合适的接口。
例如,下面的代码是错误的:
// 包 a
public class Foo {}
// 包 b
public interface Bar {}
// 包 c
import a.Foo
import b.Bar
extend Foo <: Bar {}  // 错误:这是一个孤儿扩展正确的做法是将扩展定义在包a或包b中:
// 包 a
import b.Bar
extend Foo <: Bar {}  // 合法
// 包 b
import a.Foo
extend Foo <: Bar {}  // 合法3.5 同一包内接口扩展的可见性规则
同一包内的接口扩展之间的可见性规则与直接扩展类似:
扩展间可见性:
- 在同一个包内可以对同一类型定义多个接口扩展
- 扩展之间可以直接调用其他扩展中非private修饰的函数
泛型约束的影响:
接口扩展的可见性同样受到泛型约束的影响,规则与直接扩展相同。
3.6 不同包之间接口扩展的限制
接口扩展在不同包之间的限制主要体现在 "孤儿规则" 上:
包关系要求:
- 接口扩展必须与被扩展类型或接口在同一个包中
- 不能在第三方包中为其他包的类型实现接口
导出限制:
即使满足孤儿规则,接口扩展的导出还需要满足相应的访问修饰符要求。
4. 不同包结构下扩展的导入导出行为
4.1 同一包内扩展的导入导出行为
在同一包内,扩展的导入导出行为相对简单直接:
导出条件:
- 直接扩展:只要被扩展类型和扩展成员都为public或protected,即可导出
- 接口扩展:需要满足相应的接口或类型的public要求
导入方式:
- 直接扩展:只需要使用被扩展类型,无需额外导入
- 接口扩展:需要导入相关接口
可见性范围:
- 同一包内的所有位置都可以访问扩展功能
- 子包需要通过导入才能访问父包的扩展
例如:
// 包 a
public class MyClass {}
// 直接扩展
extend MyClass {public func method1() {}
}
// 接口扩展
interface MyInterface {}
extend MyClass <: MyInterface {public func method2() {}
}
// 包 a 内其他文件可以直接使用
main() {let obj = MyClass()obj.method1()  // OKobj.method2()  // OK
}4.2 子包中扩展的导入导出行为
子包中的扩展行为需要特别注意包的层级关系:
包声明要求:
子包的包声明必须反映其相对于源码根目录的完整路径。例如:
// 源码结构
src/
└─ pkg1/└─ sub1/└─ file.cj
// file.cj 的包声明
package pkg1.sub1扩展的导出:
- 如果扩展与被扩展类型在同一个子包中,导出规则与普通包相同
- 子包中的扩展可以访问父包中的类型,但需要通过导入
- 父包不能直接访问子包中的扩展,需要通过导入子包
示例:
// 包 pkg1
public class Base {}
// 包 pkg1.sub1
import pkg1.*
// 为 Base 类添加扩展
extend Base {public func subMethod() {}  // 可以在 pkg1.sub1 包内使用
}
// 包 pkg1.sub2
import pkg1.*
import pkg1.sub1.*  // 需要显式导入子包
main() {let obj = Base()obj.subMethod()  // 需要先导入 pkg1.sub1
}4.3 跨包扩展的导入导出行为
跨包扩展的导入导出行为受到严格限制:
直接扩展的跨包限制:
- 直接扩展必须与被扩展类型在同一个包中,因此不能跨包定义直接扩展
- 试图在其他包中为其他包的类型定义直接扩展会导致编译错误
接口扩展的跨包规则:
接口扩展可以跨包,但必须遵循 "孤儿规则":
- 扩展必须与被扩展类型在同一个包中
- 或者扩展必须与接口在同一个包中
- 不能同时脱离两者(孤儿扩展)
跨包导入的要求:
- 需要导入被扩展类型所在的包
- 需要导入接口所在的包(如果是接口扩展)
- 需要遵循访问修饰符的限制
4.4 扩展与被扩展类型在不同包中的交互规则
当扩展与被扩展类型位于不同包时,交互规则变得复杂:
直接扩展的限制:
- 直接扩展不能与被扩展类型在不同包中
- 这种情况是不允许的,会导致编译错误
接口扩展的特殊情况:
接口扩展可以在不同包中,但必须满足以下条件之一:
- 扩展与被扩展类型在同一个包中(此时接口可以来自其他包)
- 扩展与接口在同一个包中(此时被扩展类型可以来自其他包)
示例:
// 包 a
public class Foo {}
// 包 b
import a.Foo
public interface I {}
// 包 b 中为 Foo 实现 I 接口
extend Foo <: I {public func method() {}  // 可以导出,因为扩展与接口在同一个包
}
// 包 c
import a.Foo
import b.I
main() {let obj: I = Foo()obj.method()  // OK
}4.5 模块内不同包之间扩展的可见性
在同一个模块内,不同包之间的扩展可见性遵循以下规则:
包关系的影响:
- 同一模块内的不同包可以相互导入
- 父包和子包之间通过包声明建立层级关系
- 扩展的可见性由其导出规则和包的导入关系共同决定
访问修饰符的作用:
- public修饰的扩展可以在模块内任何位置访问
- protected修饰的扩展只能在当前包及其子包中访问
- internal修饰的扩展(默认)只能在当前包内访问
示例:
// 模块 mymodule
// 包 mymodule.a
public class MyType {}
// 包 mymodule.b
import mymodule.a.*
extend MyType {public func publicMethod() {}  // 可以在整个模块中访问protected func protectedMethod() {}  // 只能在 mymodule.b 及其子包中访问
}
// 包 mymodule.c
import mymodule.a.*
import mymodule.b.*
main() {let obj = MyType()obj.publicMethod()  // OK// obj.protectedMethod()  // 错误:不能访问 protected 方法
}5. 扩展导入导出规则的实际应用示例
5.1 为标准库类型添加扩展
为标准库类型添加扩展是扩展机制的常见应用场景。由于标准库类型(如String、Int等)在core包中定义,并且是public的,我们可以在自己的包中为它们添加扩展。
示例:为 String 类型添加扩展
在自己的包中为String类型添加扩展:
// 包 mypackage.utils
// 为 String 类型添加扩展
extend String {public func reverse(): String {let rs = this.toRuneArray()rs.reverse()return String.fromRuneArray(rs)}public func countChar(char: Rune): Int64 {var count = 0for c in this.toRuneArray() {if c == char {count += 1}}return count}
}
// 使用示例
main() {let text = "hello world"println("原字符串: $text")println("反转: $text.reverse()")println("字符'l'出现次数: $text.countChar(r'l')")
}规则分析:
- String是core包中的public类型
- 扩展定义在mypackage.utils包中
- 扩展方法使用public修饰
- 限制:这个扩展只能在mypackage.utils包中使用,不能导出到其他包
- 原因:扩展与被扩展类型(String)不在同一个包中
5.2 跨包使用接口扩展
接口扩展允许我们为现有类型赋予新的接口能力,这在跨包场景中特别有用。
场景描述:
定义一个Logger接口,为第三方FileLogger类实现该接口,使其能够在需要Logger的地方使用。
实现代码:
// 包 logging.interfaces
public interface Logger {func log(message: String): Unit
}
// 包 logging.impl
import logging.interfaces.Logger
// 假设这是一个第三方库的类
class FileLogger {public func writeToFile(message: String) {println("写入文件: $message")}
}
// 为 FileLogger 实现 Logger 接口
extend FileLogger <: Logger {public func log(message: String) {this.writeToFile(message: message)}
}
// 包 app
import logging.interfaces.Logger
import logging.impl.FileLogger
func doLogging(logger: Logger) {logger.log("应用启动")
}
main() {let logger = FileLogger()doLogging(logger: logger)  // 可以正常使用
}规则分析:
- Logger接口在logging.interfaces包中定义,是public的
- FileLogger在logging.impl包中定义
- 接口扩展在logging.impl包中,与FileLogger在同一包
- 由于接口Logger是public的,扩展可以被导出
- 包app只需要导入Logger接口和FileLogger类即可使用
5.3 泛型扩展的导入导出应用
泛型扩展在实际开发中常用于为容器类或算法提供通用功能。
示例:为 Pair 类型实现序列化功能
定义一个Serializable接口,为Pair类型实现序列化功能,要求类型参数也必须是可序列化的:
// 包 serialization
public interface Serializable {func serialize(): String
}
// 包 data_structures.pair
import serialization.Serializable
public class Pair<T1, T2> {let first: T1let second: T2public init(a: T1, b: T2) {first = asecond = b}
}
// 为 Pair 实现 Serializable 接口
extend<T1, T2> Pair<T1, T2> <: Serializable where T1 <: Serializable, T2 <: Serializable {public func serialize(): String {return "($(first.serialize()), $(second.serialize()))"}
}
// 包 examples
import data_structures.pair.Pair
import serialization.Serializable
class Person <: Serializable {let name: Stringpublic init(name: String) {this.name = name}public func serialize(): String {return "Person($name)"}
}
main() {let p1 = Person(name: "Alice")let p2 = Person(name: "Bob")let pair = Pair(p1, p2)println(pair.serialize())  // 输出:(Person(Alice), Person(Bob))
}规则分析:
- Serializable接口是public的
- Pair类是public的泛型类
- 接口扩展要求T1和T2都必须实现Serializable接口
- 由于Person类实现了Serializable,因此Pair<Person, Person>可以被序列化
- 扩展的导出取决于T1和T2的类型,如果它们都是public,则扩展可以被导出
5.4 扩展在子包和父包之间的传递
扩展在子包和父包之间的传递需要特别注意包的层级关系和导入规则。
项目结构:
src
└─ myapp├── main.cj          # 主程序├── utils/│   ├── string_ext.cj  # String扩展│   └── __init__.cj    # 包声明└── logging/├── interfaces.cj  # Logger接口├── impl/│   ├── file.cj    # FileLogger类│   └── __init__.cj└── __init__.cj实现代码:
- myapp.utils.string_ext:
// package myapp.utils
extend String {public func reverse(): String {let rs = this.toRuneArray()rs.reverse()return String.fromRuneArray(rs)}
}- myapp.logging.impl.file:
// package myapp.logging.impl
import myapp.logging.Logger
class FileLogger {public func write(message: String) {println("文件日志: $message")}
}
extend FileLogger <: Logger {public func log(message: String) {this.write(message)}
}- myapp.main:
// package myapp
import myapp.utils.String  // 正确:导入String类型
import myapp.logging.Logger
import myapp.logging.impl.FileLogger
main() {let s = "hello".reverse()  // 可以调用,因为String扩展在同一模块println(s)  // 输出:ollehlet logger = FileLogger()logger.log("主程序启动")  // 可以调用,因为扩展被导出
}规则分析:
- String扩展定义在myapp.utils包中,可以在整个myapp模块中使用
- Logger接口在myapp.logging包中是public的
- FileLogger的接口扩展在myapp.logging.impl包中,与接口在同一模块
- 主程序可以直接使用String的扩展方法
- 需要导入Logger接口和FileLogger类才能使用日志功能
5.5 扩展的重导出应用
扩展的重导出允许我们将扩展功能集中暴露,简化其他包的使用。
示例:扩展的重导出
在包a中定义扩展,并在包b中重导出:
// 包 a.utils
extend String {public func upperFirst(): String {if this.isEmpty() {return ""}return this[0].toUpperCase() + this[1..]}
}
// 包 a
public import a.utils.String  // 重导出 String 类型
public import a.utils.*       // 重导出所有扩展
// 包 b
import a.*  // 导入包 a
main() {let s = "hello".upperFirst()  // 可以直接使用println(s)  // 输出:Hello
}规则分析:
- 在包a中,通过public import重导出了String类型和所有扩展
- 包b导入包a后,可以直接使用String的扩展方法
- 这种方式避免了在每个包中都要导入String类型
- 注意:只能重导出具体的声明,不能重导出整个包
5.6 扩展与访问修饰符的组合使用
不同的访问修饰符组合会影响扩展的可见性范围:
示例:不同修饰符组合的扩展
// 包 myapp
// 1. 公开类型 + 公开成员
public class PublicType {}
extend PublicType {public func publicMethod() {}  // ✓ 可以导出protected func protectedMethod() {}  // ✓ 模块内可访问
}
// 2. 受保护类型 + 公开成员
protected class ProtectedType {}
extend ProtectedType {public func publicMethod() {}  // ✓ 模块内可访问private func privateMethod() {}  // ✗ 不能导出
}
// 3. 内部类型 + 受保护成员
internal class InternalType {}
extend InternalType {protected func protectedMethod() {}  // ✗ 不能导出(类型是internal)
}
// 4. 公开类型 + 私有成员
public class AnotherType {}
extend AnotherType {private func privateMethod() {}  // ✗ 不能导出
}
// 使用示例
main() {let publicObj = PublicType()publicObj.publicMethod()  // 可以调用// publicObj.protectedMethod()  // 错误:protected方法只能在模块内访问let protectedObj = ProtectedType()protectedObj.publicMethod()  // 可以调用(模块内)
}规则总结:
- 只有当类型和成员都为public或protected时,扩展才能被导出
- protected成员在模块内可见,但在模块外不可见
- private成员无论如何都不能被导出
- internal类型的扩展只能在当前包中使用
