仓颉编程(12)接口和抽象类
抽象类
1. 抽象类基础概念与定义
1.1 抽象类的概念与作用
抽象类是仓颉编程语言面向对象编程范式中的重要概念,它是一种特殊的类,不能直接实例化,主要用于作为其他类的基类。抽象类的主要作用是为了加强运行时多态,通过定义抽象成员约束子类行为。
抽象类的核心价值在于它能够提供一个通用的基类模板,既包含子类共性函数的具体定义,也包含需要子类各自实现的函数接口。这种设计使得抽象类成为一个 "半虚半实" 的结构,可以全部为虚,这时候就变成了接口。
在实际编程中,抽象类特别适合用于定义算法骨架(如数据处理流程)等场景,它允许你在基类中定义共通的行为和状态,同时强制子类提供具体的实现。通过这种方式,抽象类实现了代码复用与行为抽象的双重目标。
1.2 抽象类的定义方式与语法结构
在仓颉编程语言中,使用abstract关键字修饰的类为抽象类,与普通类不同的是,抽象类中可以声明抽象函数(没有函数体)。抽象类的基本定义语法如下:
abstract class 类名 {
// 成员变量
// 构造函数
// 成员函数
// 抽象函数
}
需要特别注意的是,抽象类定义时的open修饰符是可选的,因为抽象类总是可被继承的,它的open修饰符是隐含的。这意味着你可以直接定义抽象类而无需显式添加open关键字:
abstract class Shape {
public abstract func area(): Float64
var radius: Float64
}
此外,抽象类还可以被sealed修饰,表示被修饰的类定义只能在本定义所在的包内被其他类继承。sealed已经蕴含了public/open的语义,因此定义sealed abstract class时若提供public/open修饰符,编译器将会给出告警。
1.3 抽象类的实例化限制
抽象类最重要的特性之一是不能被实例化。抽象类是必须要被继承的,它属于大类型,一定会有子类型,需要通过绑定子类完成实例化。当尝试直接实例化抽象类时,会产生编译错误:
abstract class Father {
public abstract func abstractFunction(): Unit
}
main(): Int64 {
let f = Father() // 错误:抽象类不能实例化
return 0
}
编译错误信息会明确提示 "Cannot create instance of abstract class 'Father'"。虽然一些变量和值在编译时的类型可以是抽象的,但是这样的变量和值必须或者为null,或者含有对非抽象类的实例的引用(此非抽象类是从抽象类派生的)。
这种设计确保了抽象类只能作为其他类的基类使用,从而实现了面向对象设计中的抽象化目标。
2. 抽象类的成员定义与语法规则
2.1 抽象方法的声明与定义规则
抽象方法是没有方法体的函数,但一定要有返回值类型。抽象方法必须被重写,其访问修饰符只能为protected或public,不能是private。在抽象类中定义抽象方法的语法如下:
public abstract func 方法名(参数列表): 返回类型
抽象方法的声明必须满足以下规则:
- 访问修饰符限制:抽象方法不能声明为private,因为私有成员无法被子类访问和重写。这是仓颉语言的明确规定。
- 方法体要求:抽象方法没有方法体,直接以分号结束或者使用空的花括号。例如:
public abstract func area(): Float64
或者
public abstract func area(): Float64 {}
- 返回类型必须明确:抽象方法必须声明返回类型,不能省略。
- 参数列表规则:抽象方法可以有参数列表,参数的定义与普通函数相同。
2.2 具体方法在抽象类中的实现
抽象类中可以包含具体方法(有实现的方法),这些方法为子类提供了可直接使用的默认实现。抽象类的主要目的是为子类提供一个公共的模板或部分实现,同时强制子类去实现某些特定的方法(即抽象方法)。
具体方法的定义与普通类中的方法完全相同,可以包含方法体和实现逻辑。例如:
abstract class Shape {
public abstract func area(): Float64 // 抽象方法
public func name(): String { // 具体方法
return "Shape"
}
protected func calculatePerimeter(radius: Float64): Float64 { // 具体方法
return 2 * 3.14 * radius
}
}
在这个例子中,name()和calculatePerimeter()都是具体方法,它们提供了默认的实现逻辑。子类可以直接使用这些方法,也可以选择重写它们(如果需要的话)。
2.3 成员变量与构造函数
抽象类中可以定义成员变量,包括实例成员变量和静态成员变量。成员变量的定义方式与普通类完全相同:
abstract class Animal {
var name: String // 实例成员变量
static var count: Int64 = 0 // 静态成员变量
public init(name: String) { // 构造函数
this.name = name
Animal.count += 1
}
public abstract func speak(): Unit
}
关于抽象类的构造函数,需要注意以下几点:
- 构造函数的存在性:抽象类中可以有构造方法,包括主构造函数和辅助构造函数。
- 子类构造函数的依赖性:子类的构造函数一定依赖于父类的构造函数。当父类中主构造函数含有参数时,子类主构造函数需要显式地写出super(参数)。
- 抽象类构造函数的调用:虽然抽象类不能直接实例化,但在创建子类实例时会自动调用抽象类的构造函数。
2.4 抽象类中的访问修饰符规则
抽象类中的成员可以使用不同的访问修饰符,但有一些特殊的规则需要注意:
- 抽象方法的访问修饰符:只能是public或protected,不能是private。这是因为抽象方法需要被子类实现,而私有成员无法被子类访问。
- 具体方法的访问修饰符:可以是任意合法的访问修饰符(public、protected、private),遵循普通类的规则。
- 成员变量的访问修饰符:可以是任意合法的访问修饰符,没有特殊限制。
- 构造函数的访问修饰符:通常使用public,但也可以是protected(用于限制子类的创建方式)。
需要特别注意的是,父类中的抽象方法与子类中重写的方法需要用public或protected访问修饰符修饰,否则会报错。
3. 抽象类与接口的对比分析
3.1 定义方式与功能差异
抽象类和接口在定义方式上有显著的区别。抽象类使用abstract class关键字定义,而接口使用interface关键字定义。
从功能角度来看,两者的核心差异在于:
- 抽象类:提供部分实现的抽象模板,主要用于定义 "是什么"(部分共性的抽象实现),它更侧重于复用代码,同时也能强制子类实现某些方法。
- 接口:定义纯行为契约(无实现),主要用于定义 "能做什么"(行为契约),它主要用来定义一组方法的声明,任何类都可以实现接口。
在成员构成方面,两者也有明显差异:
|
特性 |
抽象类 |
接口 |
|
成员类型 |
可包含抽象 / 非抽象函数、变量 |
仅抽象函数、静态函数(可带默认实现) |
|
成员变量 |
支持实例变量、静态变量 |
不支持实例变量,仅支持静态常量 |
|
构造函数 |
支持(可被子类调用) |
不支持 |
|
静态方法 |
支持 |
支持(但有特殊规则) |
3.2 继承机制的区别
在继承机制方面,抽象类和接口有着根本性的不同:
- 抽象类:只支持单继承,每个类只能有一个父类。这是仓颉语言的规定,类只支持单继承。
- 接口:支持多继承(多实现),一个类可以实现多个接口,使用&分隔。例如:
class MyClass <: InterfaceA & InterfaceB {
// 实现InterfaceA和InterfaceB的所有成员
}
这种差异导致了不同的使用场景选择:
- 当需要实现 "是一个"(is-a)关系时,使用抽象类(如 "狗是一种动物")
- 当需要实现 "具有某种能力"(can-do)关系时,使用接口(如 "鸟能够飞")
3.3 使用场景与选择原则
在实际开发中,选择抽象类还是接口需要根据具体的应用场景:
推荐使用抽象类的场景:
- 当需要定义对象的共同属性和部分实现时
- 当存在 "is-a" 关系(继承关系)时
- 当需要为子类提供公共的默认实现时
- 当需要定义算法骨架(如数据处理流程)时
推荐使用接口的场景:
- 当需要定义行为契约(能力抽象)时,如通信、存储接口等
- 当需要实现多继承(多实现)时
- 当需要定义不相关类的共同行为时
- 当需要实现 "can-do" 关系(能力关系)时
选择的基本原则是:
- 优先使用接口抽象,延迟实现下沉
- 能通过接口定义的能力,尽量不放入抽象类,保持接口的纯粹性
- 抽象类仅实现跨子类的公共逻辑,避免包含领域特定代码
3.4 两者结合使用的实践
在实际开发中,抽象类和接口经常结合使用,以发挥各自的优势。常见的模式是:
- 抽象类实现接口:抽象类可以实现接口并提供默认实现,子类仅需覆盖差异部分,减少重复代码。
interface Loggable {
func log(message: String)
}
abstract class AbstractLogger <: Loggable {
public func log(message: String) {
let timestamp = getCurrentTime()
printLogWithFormat(timestamp: timestamp, message: message)
}
protected abstract func printLogWithFormat(timestamp: String, message: String)
}
class JSONLogger <: AbstractLogger {
protected override func printLogWithFormat(timestamp: String, message: String) {
println("{\"time\": \"\(timestamp)\", \"msg\": \"\(message)\"}")
}
}
- 多接口抽象类:抽象类可同时实现多个接口,为子类提供复合能力。
interface Communicable { func send(data: String) }
interface Loggable { func log(message: String) }
abstract class NetworkComponent <: Communicable, Loggable {
public func send(data: String) {
log(message: "发送数据:\(data)")
doSend(data)
}
protected abstract func doSend(data: String)
public func log(message: String) { /* 默认实现 */ }
}
4. 抽象类的继承与重写机制
4.1 继承抽象类的语法规则
在仓颉中,继承抽象类的语法与继承普通类相同,使用<:符号表示继承关系:
class 子类名 <: 抽象类名 {
// 实现抽象方法
// 可以添加新的成员
// 可以重写父类的具体方法
}
需要注意的是,抽象类总是可被继承的,因此继承抽象类时不需要考虑父类是否为open。
继承抽象类的子类必须满足以下要求:
- 必须实现父类中所有的抽象方法
- 可以选择重写父类的具体方法
- 构造函数必须调用父类的构造函数
4.2 抽象方法的重写要求
子类重写抽象方法时必须遵循严格的规则:
- 方法签名必须完全一致:包括方法名称、参数列表、返回类型都必须与父类的抽象方法相同。
- 访问修饰符要求:重写方法必须使用public或protected修饰符,不能使用private。
- 使用override关键字:虽然不是强制要求,但强烈建议使用override关键字标记重写方法,以提高代码的可读性和可维护性。
例如:
abstract class Shape {
public abstract func area(): Float64
public abstract func perimeter(): Float64
}
class Circle <: Shape {
var radius: Float64
public override func area(): Float64 {
return 3.14 * radius * radius
}
public func perimeter(): Float64 { // 没有使用override,但仍然是重写
return 2 * 3.14 * radius
}
}
4.3 构造函数的继承与调用
抽象类的构造函数在继承过程中遵循以下规则:
- 子类构造函数必须调用父类构造函数:当父类有构造函数时,子类必须在其构造函数中显式调用父类的构造函数。
- 主构造函数的调用:如果父类有主构造函数,子类的主构造函数需要使用super(参数)的形式调用:
abstract class Animal {
var name: String
public init(name: String) {
this.name = name
}
}
class Dog <: Animal {
var breed: String
public init(name: String, breed: String) {
super(name) // 必须显式调用父类构造函数
this.breed = breed
}
}
- 辅助构造函数的调用:如果子类定义了辅助构造函数,它必须先调用同一个类的其他构造函数(可以是主构造函数或其他辅助构造函数),最终间接调用父类的构造函数。
4.4 抽象类的多态实现
抽象类是实现多态的重要手段。通过抽象类,可以实现运行时多态:
abstract class Animal {
public abstract func speak(): Unit
}
class Dog <: Animal {
public override func speak(): Unit {
println("Woof!")
}
}
class Cat <: Animal {
public override func speak(): Unit {
println("Meow!")
}
}
func makeAnimalSpeak(animal: Animal) {
animal.speak()
}
main(): Int64 {
let dog = Dog()
let cat = Cat()
makeAnimalSpeak(animal: dog) // 输出:Woof!
makeAnimalSpeak(animal: cat) // 输出:Meow!
return 0
}
在这个例子中,makeAnimalSpeak函数接受一个Animal类型的参数,但在运行时会根据实际对象的类型(Dog或Cat)调用相应的speak方法。这就是多态的体现。
4. 与其他编程语言的对比
与 Java 抽象类的异同
仓颉的抽象类与 Java 的抽象类在很多方面相似,但也有一些差异:
相似之处:
- 都使用abstract关键字修饰抽象类
- 都不能直接实例化
- 都可以包含抽象方法和具体方法
- 都要求子类实现所有抽象方法
- 都支持单继承
不同之处:
- 修饰符差异:Java 中抽象类默认不是final的,而仓颉中类默认是final的(不可继承),但抽象类默认是open的。
- 构造函数:两者都支持构造函数,但在调用方式上略有不同。
- 访问修饰符:Java 中抽象方法可以是protected,而仓颉中抽象方法必须是public或protected。
- sealed修饰符:仓颉支持sealed修饰符限制继承范围,而 Java 不支持。
与 C++ 抽象类的对比
C++ 的抽象类与仓颉的抽象类在实现机制上有所不同:
主要差异:
- 抽象类的判定:C++ 中只要包含纯虚函数就是抽象类,而仓颉中必须使用abstract关键字声明。
- 继承机制:C++ 支持多继承,而仓颉只支持单继承。
- 虚函数机制:C++ 需要使用virtual关键字声明虚函数,而仓颉使用open和override关键字。
- 语法差异:C++ 使用=0表示纯虚函数,而仓颉使用abstract关键字。
仓颉抽象类的特点与优势
仓颉抽象类的设计具有以下特点和优势:
- 语法简洁:通过abstract关键字明确标识抽象类,语法清晰明了。
- 默认可继承:抽象类默认是open的,无需额外修饰符,简化了继承操作。
- 灵活的成员定义:可以包含各种类型的成员,包括抽象方法、具体方法、成员变量、构造函数等。
- 安全的抽象方法:禁止定义private的抽象方法,确保了抽象方法能够被正确实现。
- sealed修饰符:提供sealed修饰符限制继承范围,增强了封装性。
- 与接口的良好配合:抽象类可以很好地与接口配合使用,实现复杂的面向对象设计。
接口的定义
在仓颉编程语言的面向对象编程体系中,接口(Interface)是实现行为抽象、支持多态特性的核心机制之一。与抽象类侧重 “是什么” 的实体抽象不同,接口专注于 “能做什么” 的行为契约定义,是构建灵活、可扩展代码架构的关键工具。
一、接口的基础概念与核心价值
1.1 接口的本质:行为契约的抽象
接口在仓颉中是一种特殊的抽象类型,它仅定义行为契约,不包含数据存储,也不提供完整的方法实现(除默认方法外)。其核心本质是:
- 行为约定:规定实现类必须提供的方法签名,明确 “能做什么” 的功能边界
- 解耦工具:分离接口定义与实现逻辑,降低代码间的耦合度
- 多态载体:支持不同类通过实现同一接口,实现 “同行为不同实现” 的多态效果
- 能力组合:允许一个类实现多个接口,组合多种独立的行为能力
例如,定义一个Printable接口,约定 “可打印” 的行为:
interface Printable {
// 定义打印行为的契约
func printContent(): Unit
func getPrintFormat(): String
}
任何实现Printable接口的类,都必须提供这两个方法的具体实现,从而保证 “可打印” 能力的一致性。
1.2 接口与抽象类的核心区别
在学习接口定义前,需明确其与抽象类的本质差异,避免混淆使用场景:
|
特性维度 |
接口(Interface) |
抽象类(Abstract Class) |
|
数据存储 |
不支持实例变量,仅允许静态常量 |
支持实例变量与静态变量 |
|
方法实现 |
仅允许默认方法(带实现)和静态方法,普通方法需抽象 |
支持抽象方法与具体方法混合定义 |
|
构造函数 |
不支持(无法实例化) |
支持(供子类调用) |
|
继承机制 |
支持多实现(一个类可实现多个接口) |
仅支持单继承(一个类仅一个父抽象类) |
|
访问修饰符 |
成员默认 public,不可修改 |
成员支持 public/protected/private 修饰 |
|
核心用途 |
定义跨类的行为契约(can-do 关系) |
定义实体的抽象模板(is-a 关系) |
简单来说:接口是 “能力的集合”,抽象类是 “实体的模板”。例如,“鸟” 可以继承抽象类Animal(表示 “是动物”),同时实现Flyable接口(表示 “能飞”)。
1.3 接口的核心应用场景
在仓颉编程中,接口的典型应用场景包括:
- 跨模块行为约定:如定义DataStorage接口,统一数据库存储、文件存储的访问方法
- 多态能力实现:如通过ShapeDrawer接口,让圆形、矩形等不同图形类实现统一的绘制逻辑
- 解耦架构设计:如在分层架构中,用接口定义服务层与数据层的交互契约,便于替换实现
- 能力组合扩展:如一个SmartPhone类可同时实现CallAble、NetworkAble、PlayAble等多个接口
接口的实现
1. 接口定义基础语法
1.1 接口声明语法格式
在仓颉编程语言中,接口使用interface关键字声明,其后是接口的标识符和接口的成员。基本语法格式如下:
interface 接口名 {
// 接口成员定义
}
接口定义时,open修饰符是可选的,因为interface默认具有open语义。例如,一个简单的接口定义:
interface I {
func f(): Unit
}
接口还可以使用sealed修饰符,表示只能在接口定义所在的包内继承、实现或扩展该接口。需要注意的是,sealed已经蕴含了public/open的语义,因此定义sealed interface时若提供public/open修饰符,编译器将会给出告警。
1.2 接口成员类型与定义规则
接口的成员可以包含成员函数、操作符重载函数和成员属性,这些成员都是抽象的,要求实现类型必须拥有对应的成员实现。
1.2.1 成员函数定义
接口中的成员函数可以是实例函数或静态函数,定义方式与普通函数类似,但通常没有函数体(除非提供默认实现)。例如:
interface NamedType {
static func typename(): String // 静态成员函数
func getName(): String // 实例成员函数
}
接口成员函数的定义规则包括:
- 成员函数可以声明为open(可选),允许子类重写
- 接口成员默认被public修饰,不可以声明额外的访问控制修饰符
- 实现类型必须使用public实现这些函数
1.2.2 静态成员定义
接口中可以定义静态成员函数和静态成员属性。静态成员通过static关键字声明,可包含默认实现。例如:
interface MathOps {
static const PI: Float64 = 3.14159 // 静态常量
static func add(a: Int, b: Int): Int // 抽象静态函数
static func multiply(a: Int, b: Int) -> Int { // 带默认实现的静态函数
return a * b
}
}
静态成员函数和实例成员函数类似,都要求实现类型提供实现。当静态成员函数没有默认实现时,将无法通过接口类型名对其进行访问。
1.2.3 操作符重载函数
接口中还可以定义操作符重载函数。操作符重载通过在类型定义中添加operator修饰符,并定义与操作符同名的函数来实现。例如:
interface Addable {
operator func +(other: Int64): Int64
}
interface Subtractable {
operator func -(other: Int64): Int64
}
需要注意的是,参数数量需匹配操作符要求(一元操作符无参数,二元操作符一个参数)。
1.2.4 成员属性定义
接口中的成员属性使用var或let声明,可以指定get和set访问器。例如:
interface Shape {
var area: Float64 { get }
var color: String { get set }
}
属性可以是只读的(只有get)或读写的(有get和set)。实现类型必须提供属性的具体实现,包括存储 backing 变量或计算逻辑。
1.3 接口访问修饰符规则
接口的访问修饰符遵循以下规则:
- 接口本身可以使用public或sealed修饰,默认是public
- 接口成员默认被public修饰,不可以声明额外的访问控制修饰符
- 实现类型必须使用public实现接口成员,不能使用protected或private
- 接口中不允许定义private成员,因为接口成员需要被实现类型访问
例如:
interface I {
func f(): Unit // 隐式public
private func g(): Unit // 错误:接口成员不能是private
}
open class C <: I {
protected func f() {} // 编译错误,f需要是public语义
}
2. 接口继承机制
2.1 接口继承基本语法
接口可以继承一个或多个接口,但不能继承类。接口继承使用<:符号,多个父接口用&分隔。例如:
interface Addable {
func add(other: Int64): Int64
}
interface Subtractable {
func sub(other: Int64): Int64
}
interface Calculable <: Addable & Subtractable {
func mul(other: Int64): Int64
func div(other: Int64): Int64
}
在这个例子中,Calculable接口继承了Addable和Subtractable两个接口,并增加了乘除两种运算符重载。
2.2 多接口继承规则
接口继承具有以下规则:
- 多继承支持:一个接口可以继承多个父接口,用&分隔,没有顺序要求
- 成员合并:子接口自动继承父接口的所有成员,包括抽象成员和有默认实现的成员
- 新增成员:子接口可以添加新的接口成员,如上面例子中的mul和div函数
- 单继承限制:虽然接口支持多继承,但类只能有一个父类(单继承),可以实现多个接口
例如,一个类可以同时实现多个接口:
class MyInt <: Addable & Subtractable {
var value = 0
public func add(other: Int64): Int64 {
value + other
}
public func sub(other: Int64): Int64 {
value - other
}
}
2.3 接口继承中的默认实现处理
当接口继承父接口中有默认实现的函数或属性时,需要遵循特定规则:
- 子接口如果继承了父接口中有默认实现的函数或属性,则在子接口中不允许仅写此函数或属性的声明(即没有默认实现),而是必须要给出新的默认实现,并且函数定义前的override修饰符(或redef修饰符)是可选的
- 子接口如果继承了父接口中没有默认实现的函数或属性,则在子接口中允许仅写此函数或属性的声明(当然也允许定义默认实现),并且函数声明或定义前的override修饰符(或redef修饰符)是可选的
例如:
interface I1 {
func f(a: Int64) {
a
}
func f1(a: Int64): Unit
}
interface I2 <: I1 {
/*'override' is optional*/ func f(a: Int64) {
a + 1
}
/*'override' is optional*/ func f1(a: Int64): Unit {}
}
2.4 接口继承与类继承的区别
接口继承与类继承有以下主要区别:
|
特性 |
接口继承 |
类继承 |
|
继承数量 |
支持多继承(可继承多个接口) |
只支持单继承(只能有一个父类) |
|
成员类型 |
只能继承接口成员(函数、属性、操作符) |
可以继承所有成员(包括变量、方法等) |
|
默认实现 |
可以为成员提供默认实现 |
父类的方法和属性都有实现 |
|
访问修饰符 |
成员默认 public |
成员可以有不同的访问修饰符 |
仓颉语言只允许单继承,每个类只能有一个父类,但可以实现多个接口。这种设计既保证了继承体系的简单性,又通过接口实现了多态的灵活性。
3. 类实现接口的语法与约束
3.1 类实现单个接口的语法格式
类实现接口使用<:符号,基本语法格式为:
class 类名 <: 接口名 {
// 实现接口的所有成员
}
例如,实现前面定义的Flyable接口:
interface Flyable {
func fly(): Unit
}
class Bird <: Flyable {
public func fly(): Unit {
println("Bird flying")
}
}
当类Bird实现接口Flyable后,Bird类型的实例可以当作Flyable类型的实例使用。
3.2 类实现多个接口的语法规则
当类需要实现多个接口时,使用&分隔多个接口,语法格式为:
class 类名 <: 接口1 & 接口2 & ... {
// 实现所有接口的成员
}
接口之间没有顺序要求。例如,让MyInt同时实现Addable和Subtractable两个接口:
interface Addable {
func add(other: Int64): Int64
}
interface Subtractable {
func sub(other: Int64): Int64
}
class MyInt <: Addable & Subtractable {
var value = 0
public func add(other: Int64): Int64 {
value + other
}
public func sub(other: Int64): Int64 {
value - other
}
}
3.3 接口实现的约束条件
类实现接口时必须满足以下约束条件:
- 必须实现所有成员:实现类型必须实现接口中要求的所有成员
- 函数签名必须一致:对于成员函数和操作符重载函数,要求实现类型提供的函数实现与接口对应的函数名称相同、参数列表相同、返回类型相同
- 属性要求:对于成员属性,要求是否被mut修饰保持一致,并且属性的类型相同
- 访问权限要求:接口成员默认被public修饰,实现类型必须使用public实现,不能使用protected或private
例如,以下实现会导致编译错误:
interface I {
func f(): Unit
}
open class C <: I {
protected func f() {} // 编译错误,f需要是public语义
}
3.4 接口实现中的特殊规则
在接口实现中有一些特殊规则需要注意:
3.4.1 协变返回类型
如果接口中的成员函数或操作符重载函数的返回值类型是class类型,那么允许实现函数的返回类型是其子类型。例如:
open class Base {}
class Sub <: Base {}
interface I {
func f(): Base
}
class C <: I {
public func f(): Sub {
Sub()
}
}
3.4.2 默认实现的继承
接口的成员可以提供默认实现。当接口成员有默认实现时,类可以选择继承默认实现或提供自己的实现。例如:
interface SayHi {
func say() {
"hi"
}
}
class A <: SayHi {} // 继承默认实现
class B <: SayHi {
public func say() {
"hi, B"
}
}
3.4.3 多重继承冲突处理
当一个类实现多个接口时,如果多个接口中包含同一个成员的默认实现,会发生多重继承的冲突,语言无法选择最适合的实现,因此这时接口中的默认实现也会失效,需要实现类型提供自己的实现。例如:
interface SayHi {
func say() {
"hi"
}
}
interface SayHello {
func say() {
"hello"
}
}
class Foo <: SayHi & SayHello {
public func say() {
"Foo"
}
}
3.4.4 override 修饰符的使用
struct、enum和class在实现接口时,函数或属性定义前的override修饰符(或redef修饰符)是可选的,无论接口中的函数或属性是否存在默认实现。例如:
interface I {
func foo(): Int64 {
return 0
}
}
enum E <: I {
elem
public override func foo(): Int64 {
return 1
}
}
struct S <: I {
public override func foo(): Int64 {
return 1
}
}
3.5 接口实现的类型限制
仓颉中除Tuple、VArray和函数外的其它类型都可以实现接口。这意味着:
- 类(class):最常见的接口实现类型,可以实现任意数量的接口
- 结构体(struct):值类型,可以实现接口,但有一些特殊考虑(见下文)
- 枚举(enum):可以实现接口,通常用于定义有限的状态或选项
- 扩展(extend):通过扩展机制,可以为已有类型实现接口
- 不支持的类型:Tuple、VArray和函数类型不能实现接口
3.5.1 结构体实现接口的特殊规则
结构体实现接口时需要注意以下特殊规则:
- mut 函数要求:若接口中声明mut函数,struct实现时必须使用mut修饰符,而类实现时无需修饰
interface Mutable {
mut func update(value: Int64)
}
struct MutStruct : Mutable {
public mut func update(value: Int64) { /*...*/ } // struct必须添加mut
}
class MutClass : Mutable {
public func update(value: Int64) { /*...*/ } // 类无需mut修饰
}
- 值语义问题:当struct实例赋值给接口类型变量时,会发生值复制,接口变量持有struct的副本,修改不会影响原始实例
struct Counter : Mutable {
public var count: Int64 = 0
public mut func update(value: Int64) { count = value }
}
var counter = Counter()
var i: Mutable = counter // 复制实例,i持有副本
i.update(value: 10) // 修改副本的count值
print(counter.count) // 输出:0(原始实例未变更)
4. 接口实现的具体要求与注意事项
4.1 接口成员实现的具体要求
实现接口成员时,必须满足以下具体要求:
4.1.1 函数实现要求
- 函数签名完全匹配:函数名称、参数列表(参数个数、类型、顺序)、返回类型必须与接口定义完全一致
interface I {
func f(a: Int64, b: Float64): Bool
}
class C <: I {
// 正确实现
public func f(a: Int64, b: Float64): Bool {
return a > 0 && b > 0
}
// 错误:参数类型不匹配
public func f(a: Int64, b: Int64): Bool {
return a > 0 && b > 0
}
// 错误:返回类型不匹配
public func f(a: Int64, b: Float64): Int64 {
return 1
}
}
- 访问修饰符必须为 public:实现函数必须使用public修饰符,不能使用protected或private
- 默认实现的处理:如果接口成员有默认实现,类可以选择继承或重写。但如果多个接口有冲突的默认实现,必须重写
4.1.2 属性实现要求
- 属性类型必须一致:属性的类型必须与接口定义相同,包括是否为mut(可变)
interface Shape {
var area: Float64 { get }
var color: String { get set }
}
class Circle <: Shape {
// 正确:area是只读的
public var area: Float64 {
return 3.14 * radius * radius
}
// 正确:color是读写的
public var color: String = "red"
// 错误:area被声明为读写
public var area: Float64 {
get { return 3.14 * radius * radius }
set { /*...*/ }
}
}
- 存储属性 vs 计算属性:实现时可以使用存储属性或计算属性,只要满足接口的get/set要求
4.1.3 操作符重载实现要求
- 操作符名称必须正确:函数名必须与要重载的操作符完全相同
- 参数数量必须匹配:一元操作符无参数,二元操作符有一个参数
- operator 修饰符:必须使用operator修饰符
interface Addable {
operator func +(other: Int64): Int64
}
class MyInt <: Addable {
var value: Int64
public operator func +(other: Int64): Int64 {
return value + other
}
}
4.2 实现多个接口时的冲突处理
当实现多个接口时,可能遇到以下冲突情况:
4.2.1 同名成员冲突
当多个接口定义了同名的成员(函数、属性或操作符)时,需要在实现类中统一处理:
- 如果同名成员的签名相同(参数列表和返回类型相同),直接在类中实现一次即可
- 如果同名成员的签名不同,需要都实现(构成重载)
- 如果同名成员有不同的默认实现,必须在类中重写并提供自己的实现
4.2.2 继承链中的冲突
当接口之间存在继承关系时,需要注意:
- 子接口继承的成员可能与其他接口的成员冲突
- 父接口的默认实现可能被子接口的声明覆盖
- 需要仔细处理继承链中的所有成员
例如:
interface A {
func f(): Unit
}
interface B <: A {
func f(): Unit // 继承A的f,并可以提供新的默认实现
}
interface C {
func f(): Unit
}
class D <: B & C {
// 必须实现f(),因为B和C都要求实现f()
public func f(): Unit {
// 实现逻辑
}
}
4.3 接口实现的最佳实践
基于接口的特性和约束,以下是一些最佳实践:
- 优先使用接口抽象:能通过接口定义的能力,尽量不放入抽象类,保持接口的纯粹性
- 接口职责单一:每个接口应该专注于定义一类行为,避免接口过于庞大
- 合理使用默认实现:在接口中提供实用的默认实现,可以减少实现类的代码量
- 避免循环依赖:接口之间的继承应该形成有向无环图,避免循环依赖
- 文档说明:为接口和接口成员添加清晰的文档注释,说明接口的用途和成员的含义
4.4 接口实现的常见错误与解决方案
在实现接口时,常见的错误包括:
- 遗漏接口成员:忘记实现接口中的某个成员
-
- 错误示例:
interface I {
func f(): Unit
func g(): Unit
}
class C <: I {
public func f(): Unit {
// 实现f
}
// 遗漏了g()的实现
}
-
- 解决方案:仔细检查接口定义,确保实现所有成员
- 函数签名不匹配:参数类型、返回类型或参数顺序与接口定义不一致
-
- 错误示例:
interface I {
func f(a: Int64): String
}
class C <: I {
public func f(a: Int32): String { // 参数类型错误
return "a = \(a)"
}
}
-
- 解决方案:仔细比对接口定义,确保签名完全一致
- 访问修饰符错误:使用了protected或private修饰接口实现
-
- 错误示例:
interface I {
func f(): Unit
}
class C <: I {
protected func f(): Unit { // 错误:必须是public
// 实现
}
}
-
- 解决方案:始终使用public修饰接口实现
- 默认实现冲突:多个接口有相同的默认实现
-
- 错误示例:
interface A {
func f() { print("A") }
}
interface B {
func f() { print("B") }
}
class C <: A & B {
// 没有实现f(),导致冲突
}
-
- 解决方案:必须在类中重写f(),提供自己的实现
- 值类型的特殊问题(针对struct):
-
- 错误示例:
interface Mutable {
mut func update(): Unit
}
struct S <: Mutable {
var x: Int64
public func update(): Unit { // 错误:缺少mut修饰符
x += 1
}
}
-
- 解决方案:为struct的mut函数添加mut修饰符
接口与抽象类练习题
一,模拟编写操作系统类
编写一个操作系统类,它的成员包括:名称、运行和关机,在编写一个鸿蒙类,专属成员包括:自由流转,在编写一个Windows类,它没有专属成员(使用抽象类来实现代码)
package demo
abstract class System {
// 抽象类构造函数改为protected(仅允许子类调用)
protected System(private var _name: String) {}
// 公开的name属性(供子类和外部访问)
public mut prop name: String {
get() {
return this._name
}
set(_name) {
this._name = _name
}
}
// 抽象方法声明返回String
public func start(): Unit
public func stop(): Unit
}
class HarmonyOS <: System {
// 子类构造函数调用父类protected构造函数
public HarmonyOS(name: String) {
super(name)
}
// 重写start(),返回String(与父类一致)
public override func start() {
println("${this.name}开机了")
}
// 重写stop(),返回String(与父类一致)
public override func stop() {
println("${this.name}关机了")
}
public func free() {
println("${this.name}自由流转中")
}
}
class Windows <: System {
// 子类构造函数调用父类protected构造函数
public Windows(name: String) {
super(name)
}
// 重写start(),返回String(与父类一致)
public override func start() {
println("${this.name}开机了")
}
// 重写stop(),返回String(与父类一致)
public override func stop() {
println("${this.name}关机了")
}
public func free() {
println("${this.name}自由流转中")
}
}
main() {
let hw = HarmonyOS("鸿蒙 next")
hw.start()
hw.free()
hw.stop()
let win = Windows("Windows xp")
win.start()
win.stop()
}
二,模拟编写计算机USB接口
编写一个计算机的接口USB接口,它的成员包括:名称、启动和停止,在编写一个U盘类和打印机类(使用接口来实现代码)
package demo
interface USB {
mut prop name:String
func start():Unit
func stop():Unit {
println("${this.name}:关闭")
}
}
class U <: USB {
public U(private var _name:String) {}
public mut prop name:String {
get() {
return this._name
}
set(_name) {
this._name =_name
}
}
public override func start() {
println("${this._name}:开启")
}
}
class Printer <: USB {
public Printer(private var _name:String) {}
public mut prop name:String {
get() {
return this._name
}
set(_name) {
this._name =_name
}
}
public override func start() {
println("${this._name}:开始打印")
}
public override func stop() {
println("${this._name}:停止打印")
}
}
main() {
let u = U("work document")
u.start()
u.stop()
print("\n")
let printer:USB = Printer("打印机")
printer.start()
printer.stop()
}
更多推荐


所有评论(0)