抽象类

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 方法名(参数列表): 返回类型

抽象方法的声明必须满足以下规则:

  1. 访问修饰符限制:抽象方法不能声明为private,因为私有成员无法被子类访问和重写。这是仓颉语言的明确规定。
  2. 方法体要求:抽象方法没有方法体,直接以分号结束或者使用空的花括号。例如:
public abstract func area(): Float64

或者

public abstract func area(): Float64 {}
  1. 返回类型必须明确:抽象方法必须声明返回类型,不能省略。
  2. 参数列表规则:抽象方法可以有参数列表,参数的定义与普通函数相同。

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
}

关于抽象类的构造函数,需要注意以下几点:

  1. 构造函数的存在性:抽象类中可以有构造方法,包括主构造函数和辅助构造函数。
  2. 子类构造函数的依赖性:子类的构造函数一定依赖于父类的构造函数。当父类中主构造函数含有参数时,子类主构造函数需要显式地写出super(参数)。
  3. 抽象类构造函数的调用:虽然抽象类不能直接实例化,但在创建子类实例时会自动调用抽象类的构造函数。

2.4 抽象类中的访问修饰符规则

抽象类中的成员可以使用不同的访问修饰符,但有一些特殊的规则需要注意:

  1. 抽象方法的访问修饰符:只能是public或protected,不能是private。这是因为抽象方法需要被子类实现,而私有成员无法被子类访问。
  2. 具体方法的访问修饰符:可以是任意合法的访问修饰符(public、protected、private),遵循普通类的规则。
  3. 成员变量的访问修饰符:可以是任意合法的访问修饰符,没有特殊限制。
  4. 构造函数的访问修饰符:通常使用public,但也可以是protected(用于限制子类的创建方式)。

需要特别注意的是,父类中的抽象方法与子类中重写的方法需要用public或protected访问修饰符修饰,否则会报错。

3. 抽象类与接口的对比分析

3.1 定义方式与功能差异

抽象类和接口在定义方式上有显著的区别。抽象类使用abstract class关键字定义,而接口使用interface关键字定义。

从功能角度来看,两者的核心差异在于:

  • 抽象类:提供部分实现的抽象模板,主要用于定义 "是什么"(部分共性的抽象实现),它更侧重于复用代码,同时也能强制子类实现某些方法。
  • 接口:定义纯行为契约(无实现),主要用于定义 "能做什么"(行为契约),它主要用来定义一组方法的声明,任何类都可以实现接口。

在成员构成方面,两者也有明显差异:

特性

抽象类

接口

成员类型

可包含抽象 / 非抽象函数、变量

仅抽象函数、静态函数(可带默认实现)

成员变量

支持实例变量、静态变量

不支持实例变量,仅支持静态常量

构造函数

支持(可被子类调用)

不支持

静态方法

支持

支持(但有特殊规则)

3.2 继承机制的区别

在继承机制方面,抽象类和接口有着根本性的不同:

  1. 抽象类:只支持单继承,每个类只能有一个父类。这是仓颉语言的规定,类只支持单继承。
  2. 接口:支持多继承(多实现),一个类可以实现多个接口,使用&分隔。例如:
class MyClass <: InterfaceA & InterfaceB {
    // 实现InterfaceA和InterfaceB的所有成员
}

这种差异导致了不同的使用场景选择:

  • 当需要实现 "是一个"(is-a)关系时,使用抽象类(如 "狗是一种动物")
  • 当需要实现 "具有某种能力"(can-do)关系时,使用接口(如 "鸟能够飞")

3.3 使用场景与选择原则

在实际开发中,选择抽象类还是接口需要根据具体的应用场景:

推荐使用抽象类的场景

  1. 当需要定义对象的共同属性和部分实现时
  2. 当存在 "is-a" 关系(继承关系)时
  3. 当需要为子类提供公共的默认实现时
  4. 当需要定义算法骨架(如数据处理流程)时

推荐使用接口的场景

  1. 当需要定义行为契约(能力抽象)时,如通信、存储接口等
  2. 当需要实现多继承(多实现)时
  3. 当需要定义不相关类的共同行为时
  4. 当需要实现 "can-do" 关系(能力关系)时

选择的基本原则是:

  • 优先使用接口抽象,延迟实现下沉
  • 能通过接口定义的能力,尽量不放入抽象类,保持接口的纯粹性
  • 抽象类仅实现跨子类的公共逻辑,避免包含领域特定代码

3.4 两者结合使用的实践

在实际开发中,抽象类和接口经常结合使用,以发挥各自的优势。常见的模式是:

  1. 抽象类实现接口:抽象类可以实现接口并提供默认实现,子类仅需覆盖差异部分,减少重复代码。
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)\"}")
    }
}
  1. 多接口抽象类:抽象类可同时实现多个接口,为子类提供复合能力。
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。

继承抽象类的子类必须满足以下要求:

  1. 必须实现父类中所有的抽象方法
  2. 可以选择重写父类的具体方法
  3. 构造函数必须调用父类的构造函数

4.2 抽象方法的重写要求

子类重写抽象方法时必须遵循严格的规则:

  1. 方法签名必须完全一致:包括方法名称、参数列表、返回类型都必须与父类的抽象方法相同。
  2. 访问修饰符要求:重写方法必须使用public或protected修饰符,不能使用private。
  3. 使用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 构造函数的继承与调用

抽象类的构造函数在继承过程中遵循以下规则:

  1. 子类构造函数必须调用父类构造函数:当父类有构造函数时,子类必须在其构造函数中显式调用父类的构造函数。
  2. 主构造函数的调用:如果父类有主构造函数,子类的主构造函数需要使用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
    }
}
  1. 辅助构造函数的调用:如果子类定义了辅助构造函数,它必须先调用同一个类的其他构造函数(可以是主构造函数或其他辅助构造函数),最终间接调用父类的构造函数。

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 的抽象类在很多方面相似,但也有一些差异:

相似之处

  1. 都使用abstract关键字修饰抽象类
  2. 都不能直接实例化
  3. 都可以包含抽象方法和具体方法
  4. 都要求子类实现所有抽象方法
  5. 都支持单继承

不同之处

  1. 修饰符差异:Java 中抽象类默认不是final的,而仓颉中类默认是final的(不可继承),但抽象类默认是open的。
  2. 构造函数:两者都支持构造函数,但在调用方式上略有不同。
  3. 访问修饰符:Java 中抽象方法可以是protected,而仓颉中抽象方法必须是public或protected。
  4. sealed修饰符:仓颉支持sealed修饰符限制继承范围,而 Java 不支持。

与 C++ 抽象类的对比

C++ 的抽象类与仓颉的抽象类在实现机制上有所不同:

主要差异

  1. 抽象类的判定:C++ 中只要包含纯虚函数就是抽象类,而仓颉中必须使用abstract关键字声明。
  2. 继承机制:C++ 支持多继承,而仓颉只支持单继承。
  3. 虚函数机制:C++ 需要使用virtual关键字声明虚函数,而仓颉使用open和override关键字。
  4. 语法差异:C++ 使用=0表示纯虚函数,而仓颉使用abstract关键字。

仓颉抽象类的特点与优势

仓颉抽象类的设计具有以下特点和优势:

  1. 语法简洁:通过abstract关键字明确标识抽象类,语法清晰明了。
  2. 默认可继承:抽象类默认是open的,无需额外修饰符,简化了继承操作。
  3. 灵活的成员定义:可以包含各种类型的成员,包括抽象方法、具体方法、成员变量、构造函数等。
  4. 安全的抽象方法:禁止定义private的抽象方法,确保了抽象方法能够被正确实现。
  5. sealed修饰符:提供sealed修饰符限制继承范围,增强了封装性。
  6. 与接口的良好配合:抽象类可以很好地与接口配合使用,实现复杂的面向对象设计。

接口的定义

在仓颉编程语言的面向对象编程体系中,接口(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 接口的核心应用场景

在仓颉编程中,接口的典型应用场景包括:

  1. 跨模块行为约定:如定义DataStorage接口,统一数据库存储、文件存储的访问方法
  2. 多态能力实现:如通过ShapeDrawer接口,让圆形、矩形等不同图形类实现统一的绘制逻辑
  3. 解耦架构设计:如在分层架构中,用接口定义服务层与数据层的交互契约,便于替换实现
  4. 能力组合扩展:如一个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 接口访问修饰符规则

接口的访问修饰符遵循以下规则:

  1. 接口本身可以使用public或sealed修饰,默认是public
  2. 接口成员默认被public修饰,不可以声明额外的访问控制修饰符
  3. 实现类型必须使用public实现接口成员,不能使用protected或private
  4. 接口中不允许定义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 多接口继承规则

接口继承具有以下规则:

  1. 多继承支持:一个接口可以继承多个父接口,用&分隔,没有顺序要求
  2. 成员合并:子接口自动继承父接口的所有成员,包括抽象成员和有默认实现的成员
  3. 新增成员:子接口可以添加新的接口成员,如上面例子中的mul和div函数
  4. 单继承限制:虽然接口支持多继承,但类只能有一个父类(单继承),可以实现多个接口

例如,一个类可以同时实现多个接口:

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 接口继承中的默认实现处理

当接口继承父接口中有默认实现的函数或属性时,需要遵循特定规则:

  1. 子接口如果继承了父接口中有默认实现的函数或属性,则在子接口中不允许仅写此函数或属性的声明(即没有默认实现),而是必须要给出新的默认实现,并且函数定义前的override修饰符(或redef修饰符)是可选的
  2. 子接口如果继承了父接口中没有默认实现的函数或属性,则在子接口中允许仅写此函数或属性的声明(当然也允许定义默认实现),并且函数声明或定义前的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 接口实现的约束条件

类实现接口时必须满足以下约束条件:

  1. 必须实现所有成员:实现类型必须实现接口中要求的所有成员
  2. 函数签名必须一致:对于成员函数和操作符重载函数,要求实现类型提供的函数实现与接口对应的函数名称相同、参数列表相同、返回类型相同
  3. 属性要求:对于成员属性,要求是否被mut修饰保持一致,并且属性的类型相同
  4. 访问权限要求:接口成员默认被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和函数外的其它类型都可以实现接口。这意味着:

  1. 类(class):最常见的接口实现类型,可以实现任意数量的接口
  2. 结构体(struct):值类型,可以实现接口,但有一些特殊考虑(见下文)
  3. 枚举(enum):可以实现接口,通常用于定义有限的状态或选项
  4. 扩展(extend):通过扩展机制,可以为已有类型实现接口
  5. 不支持的类型:Tuple、VArray和函数类型不能实现接口
3.5.1 结构体实现接口的特殊规则

结构体实现接口时需要注意以下特殊规则:

  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修饰
}
  1. 值语义问题:当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 函数实现要求
  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
    }
}
  1. 访问修饰符必须为 public:实现函数必须使用public修饰符,不能使用protected或private
  2. 默认实现的处理:如果接口成员有默认实现,类可以选择继承或重写。但如果多个接口有冲突的默认实现,必须重写
4.1.2 属性实现要求
  1. 属性类型必须一致:属性的类型必须与接口定义相同,包括是否为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 { /*...*/ }
    }
}
  1. 存储属性 vs 计算属性:实现时可以使用存储属性或计算属性,只要满足接口的get/set要求
4.1.3 操作符重载实现要求
  1. 操作符名称必须正确:函数名必须与要重载的操作符完全相同
  2. 参数数量必须匹配:一元操作符无参数,二元操作符有一个参数
  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 同名成员冲突

当多个接口定义了同名的成员(函数、属性或操作符)时,需要在实现类中统一处理:

  1. 如果同名成员的签名相同(参数列表和返回类型相同),直接在类中实现一次即可
  2. 如果同名成员的签名不同,需要都实现(构成重载)
  3. 如果同名成员有不同的默认实现,必须在类中重写并提供自己的实现
4.2.2 继承链中的冲突

当接口之间存在继承关系时,需要注意:

  1. 子接口继承的成员可能与其他接口的成员冲突
  2. 父接口的默认实现可能被子接口的声明覆盖
  3. 需要仔细处理继承链中的所有成员

例如:

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 接口实现的最佳实践

基于接口的特性和约束,以下是一些最佳实践:

  1. 优先使用接口抽象:能通过接口定义的能力,尽量不放入抽象类,保持接口的纯粹性
  2. 接口职责单一:每个接口应该专注于定义一类行为,避免接口过于庞大
  3. 合理使用默认实现:在接口中提供实用的默认实现,可以减少实现类的代码量
  4. 避免循环依赖:接口之间的继承应该形成有向无环图,避免循环依赖
  5. 文档说明:为接口和接口成员添加清晰的文档注释,说明接口的用途和成员的含义

4.4 接口实现的常见错误与解决方案

在实现接口时,常见的错误包括:

  1. 遗漏接口成员:忘记实现接口中的某个成员
    • 错误示例:
interface I {
    func f(): Unit
    func g(): Unit
}
class C <: I {
    public func f(): Unit {
        // 实现f
    }
    // 遗漏了g()的实现
}
    • 解决方案:仔细检查接口定义,确保实现所有成员
  1. 函数签名不匹配:参数类型、返回类型或参数顺序与接口定义不一致
    • 错误示例:
interface I {
    func f(a: Int64): String
}
class C <: I {
    public func f(a: Int32): String {  // 参数类型错误
        return "a = \(a)"
    }
}
    • 解决方案:仔细比对接口定义,确保签名完全一致
  1. 访问修饰符错误:使用了protected或private修饰接口实现
    • 错误示例:
interface I {
    func f(): Unit
}
class C <: I {
    protected func f(): Unit {  // 错误:必须是public
        // 实现
    }
}
    • 解决方案:始终使用public修饰接口实现
  1. 默认实现冲突:多个接口有相同的默认实现
    • 错误示例:
interface A {
    func f() { print("A") }
}
interface B {
    func f() { print("B") }
}
class C <: A & B {
    // 没有实现f(),导致冲突
}
    • 解决方案:必须在类中重写f(),提供自己的实现
  1. 值类型的特殊问题(针对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()
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐