目录

1、类和接口

特殊符号解释:

2、函数作为一等公民

为什么称为 "一等公民"?

特殊符号解释:

3、代数数据类型和模式匹配

特殊符号解释:

4、泛型

特殊符号解释:

核心概念总结

关键符号速查表


1、类和接口

核心概念

  • 单继承,多实现:每个类只能有 1 个父类,但可实现多个接口。
  • open 修饰符:允许类被继承或方法被重写(默认不可继承 / 重写)。
  • 动态派遣:根据对象实际类型调用方法(而非变量声明类型)。

        仓颉支持使用传统的类(class)和接口(interface)来实现面向对象范式编程。仓颉语言只允许单继承,每个类只能有一个父类,但可以实现多个接口。每个类都是 Object 的子类(直接子类或者间接子类)。此外,所有的仓颉类型(包括 Object)都隐式地实现 Any 接口。         

        仓颉提供 open 修饰符,来控制一个类能不能被继承,或者一个对象成员函数能不能被子类重写(override)。

        在下面的例子中,类 B 继承了类 A,且同时实现了接口 I1 和 I2。为了让 A 能够被继承,它的声明需要被 open 修饰。类 A 中的函数 f 也被 open 修饰,因此可以在 B 中被重写。对函数 f 的调用会根据对象具体的类型来决定执行哪个版本,即动态派遣

特殊符号解释:
  • <: 符号表示继承实现关系:
    • class B <: A 表示 B 继承自 A
    • class B <: I1 & I2 表示 B 实现了接口 I1 和 I2
  • & 符号用于多接口实现多接口继承
    • class B <: A & I1 & I2 表示 B 继承 A 并实现 I1 和 I2
    • interface I3 <: I1 & I2 表示 I3 继承自 I1 和 I2
public open class A {
    let x: Int = 1  // 不可变属性 x
    var y: Int = 2  // 可变属性 y

    public open func f(): Unit {  // open 允许子类重写
        println("function f in A")
    }

    public func g(): Unit {  // 未标记 open,不可重写
        println("function g in A")
    }
}

public interface I1 {
    func h1(): Unit  // 接口方法,无需实现
}

public interface I2 {
    func h2(): Unit
}

public class B <: A & I1 & I2 {  // B 继承 A 并实现 I1、I2
    public override func f(): Unit {  // 重写 A 中的 f()
        println("function f in B")
    }

    public func h1(): Unit {  // 实现 I1 中的 h1()
        println("function h1 in B")
    }

    public func h2(): Unit {  // 实现 I2 中的 h2()
        println("function h2 in B")
    }
}

main() {
    let o1: I1 = B()  // o1 是 I1 类型,但实际指向 B 实例
    let o2: A = A()   // o2 是 A 类型,指向 A 实例
    let o3: A = B()   // o3 是 A 类型,但实际指向 B 实例

    o1.h1()  // 调用 B 中的 h1()
    o2.f()   // 调用 A 中的 f()
    o3.f()   // 动态派遣:调用 B 中的 f()(因为实际是 B 实例)
    o3.g()   // 调用 A 中的 g()(B 未重写 g())
}

仓颉的 interface 之间也可以继承,并且不受单继承的约束,即一个 interface 也可以继承多个父 interface。如下示例,I3 可以同时继承 I1 和 I2。因此,若要实现 I3,需要提供对 fg 和 h 三个函数的实现。

interface I1 {
    func f(x: Int): Unit
}

interface I2 {
    func g(x: Int): Int
}

interface I3 <: I1 & I2 {
    func h(): Unit
}

2、函数作为一等公民

什么是“一等公民”:

在编程语言中,"函数作为一等公民"(First-Class Functions) 是指函数被视为与其他数据类型(如整数、字符串、对象等)具有同等地位的语言特性。具体来说,一等公民函数可以:

  1. 作为变量赋值:可以将函数赋值给变量,就像存储其他数据一样。
  2. 作为参数传递:可以将函数作为参数传递给其他函数。
  3. 作为返回值:函数可以返回另一个函数。
  4. 存储在数据结构中:可以将函数存储在数组、字典等数据结构中。
  5. 支持匿名函数:可以创建无需命名的函数(如 Lambda 表达式)。
为什么称为 "一等公民"?

这一概念源于数学中的 **"一等对象"(First-Class Object)**,即具有完整权利的对象。在编程语言中,"一等公民" 意味着函数可以参与所有其他数据类型允许的操作,而无需特殊语法或限制。

核心概念

  • 函数可作为变量、参数、返回值,或存储在数据结构中。
  • Lambda 表达式{参数 => 表达式} 语法创建匿名函数。

        仓颉中函数可以作为普通表达式使用,可以作为参数传递,作为函数返回值,被保存在其他数据结构中,或者赋值给一个变量使用。

        在下面列子中的全局函数,对象或结构体等数据类型的成员函数都可以作为一等公民使用。

特殊符号解释:
  • => 符号用于Lambda 表达式模式匹配
    • {x: Int => x * x} 是一个 Lambda 表达式,表示接受 x 返回 x*x
    • case Node(v, l, r) => ... 是模式匹配中的分支,匹配后执行右侧代码
func f(x: Int) {  // 普通函数
    return x
}

let a = f  // 将函数 f 赋值给变量 a

let square = {x: Int => x * x}  // Lambda 表达式,等价于:
// func square(x: Int) { return x * x }

// 函数嵌套与返回函数
func g(x: Int) {
    func h() {  // 内部函数
        return f(square(x))  // 调用外部的 f 和 square
    }
    return h  // 返回内部函数
}

func h(f: ()->Int) {  // 接受函数作为参数
    f()  // 调用传入的函数
}

let b = h(g(100))  // 等价于:h(g(100))()


// 成员函数作为一等公民
class C {
    var x = 100
    func resetX(n: Int) {
        x = n
        return x
    }
}

main() {
    let o = C()
    let f = o.resetX  // 将成员函数赋值给变量
    f(200)  // 等价于:o.resetX(200)
    print(o.x)  // 输出 200
}

3、代数数据类型和模式匹配

核心概念

  • 枚举(enum):定义带参数的类型构造器(如二叉树节点)。
  • 模式匹配(match):根据数据结构提取值,类似 switch 但更强大。

        代数数据类型是一种复合类型,指由其它数据类型组合而成的类型。两类常见的代数类型是积类型(如 struct、tuple 等)与和类型(如 tagged union)。 在此我们着重介绍仓颉的和类型 enum,以及对应的模式匹配能力。

        在下面的例子中,enum 类型 BinaryTree 具有两个构造器,Node 和 Empty。其中 Empty 不带参数,对应于只有一个空节点的二叉树,而 Node 需要三个参数来构造出一个具有一个值和左右子树的二叉树。

特殊符号解释:
  • | 符号用于枚举类型的构造器分隔
    • enum BinaryTree { | Node(...) | Empty } 表示两种构造器
  • match 关键字用于模式匹配,类似其他语言的 switch 但更强大
  • _ 是通配符模式,匹配任何值
// 定义二叉树枚举类型
enum BinaryTree {
    | Node(value: Int, left: BinaryTree, right: BinaryTree)  // 节点构造器
    | Empty  // 空树构造器
}

访问这些 enum 实例的值需要使用模式匹配进行解析。模式匹配是一种测试表达式是否具有特定特征的方法,在仓颉中主要提供了 match 表达式来完成这个目标。对于给定的 enum 类型的表达式,我们使用 match 表达式来判断它是用哪个构造器构造的,并提取相应构造器的参数。下面的例子中,递归函数 sumBinaryTree 实现对二叉树节点中保存的整数求和。

// 递归计算树中所有节点值的和
func sumBinaryTree(bt: BinaryTree) {
    match (bt) {
        case Node(v, l, r) =>  // 匹配 Node 构造器,绑定 v, l, r
            v + sumBinaryTree(l) + sumBinaryTree(r)
        case Empty => 0  // 匹配 Empty 构造器
    }
}

除此 enum 模式以外,仓颉也提供了其它各种模式,如常量模式、绑定模式、类型模式等,以及各种模式的嵌套使用。在下面的例子中,我们给出了对应模式的使用:

  • 常量模式:可以使用多种字面量值进行判等比较,如整数、字符串等。
  • 绑定模式:可以将指定位置的成员绑定到新的变量,多用于解构 enum 或 tuple。上面的 sumBinaryTree 例子中就用到了绑定模式,将 Node 节点中实际的参数与三个新声明的变量 vl 和 r 分别绑定。
  • 类型模式:可以用于匹配是否目标类型,多用于向下转型。
  • tuple 模式:用于比较或者解构 tuple。
  • 通配符模式:用于匹配任何值。

未来还可能引入的模式,如序列(sequence)模式、record 模式等。

// 其他模式匹配示例
func f1(x: String) {
    match (x) {
        case "abc" => ()  // 常量模式:匹配字符串 "abc"
        case "def" => ()  // 常量模式:匹配字符串 "def"
        case _ => ()      // 通配符模式:匹配任何字符串
    }
}

func f2(x: (Int, Int)) {
    match (x) {
        case (_, 0) => 0  // 匹配第二个元素为 0 的元组
        case (i, j) => i / j  // 绑定模式:将元组元素绑定到 i 和 j
    }
}

func f3(x: ParentClass) {
    match (x) {
        case y: ChildClass1 => ...  // 类型模式:匹配 ChildClass1 类型
        case y: ChildClass2 => ...  // 类型模式:匹配 ChildClass2 类型
        case _ => ...               // 通配符模式
    }
}

4、泛型

核心概念

  • 类型参数化:用 <T> 定义通用函数 / 类,提高代码复用。
  • 类型约束:通过 where 子句限制泛型类型(如要求 T 实现 Equatable)。
  • 非协变:即使 Apple 是 Fruit 的子类,Array<Apple> 与 Array<Fruit> 不可互转。
特殊符号解释:
  • <T> 表示泛型类型参数T 是类型变量
  • where T <: Equatable<T> 是类型约束,要求 T 实现 Equatable<T> 接口

        在现代软件开发中,泛型编程已成为提高代码质量、复用性和灵活性的关键技术。泛型作为一种参数化多态技术,允许开发者在定义类型或函数时使用类型作为参数,从而创建可适用于多种数据类型的通用代码结构。泛型带来的好处包括:

  • 代码复用:能够定义可操作多种类型的通用算法和数据结构,减少代码冗余。
  • 类型安全:支持更多的编译时的类型检查,避免了运行时类型错误,增强了程序的稳定性。
  • 性能提升:由于避免了不必要的类型转换,泛型还可以提高程序执行效率。

        仓颉支持泛型编程,诸如函数、struct、class、interface、extend 都可以引入泛型变元以实现功能的泛型化。数组类型在仓颉中就是典型的泛型类型应用,其语法表示为 Array<T>,其中 T 表示了元素的类型,可以被实例化为任何一个具体的类型,例如 Array<Int> 或 Array<String>,甚至可以是嵌套数组 Array<Array<Int>>,从而可以轻易地构造各种不同元素类型的数组。

        除了类型外,我们还可以定义泛型函数。例如我们可以为使用泛型函数来实现任意两个同类型数组的 concat 操作。如下代码所示,我们定义了一个泛型函数 concat,并且它支持任意两个 Array<T> 类型的数组参数,经过处理后返回了一个拼接后的新数组。这样定义的 concat 函数可以应用在 Array<Int>Array<String>Array<Array<Int>> 以及其它任意类型的数组上,实现了功能的通用化。

// 泛型函数:拼接两个同类型数组
func concat<T>(lhs: Array<T>, rhs: Array<T>): Array<T> {
    let defaultValue = if (lhs.size > 0) {
        lhs[0]  // 如果 lhs 非空,取第一个元素作为默认值
    } else if (rhs.size > 0) {
        rhs[0]  // 如果 rhs 非空,取第一个元素作为默认值
    } else {
        return []  // 两个数组都为空,直接返回空数组
    }
    
    // 创建新数组,大小为两个数组之和,初始值为 defaultValue
    let newArr = Array<T>(lhs.size + rhs.size, item: defaultValue)
    
    // 数组切片赋值
    newArr[0..lhs.size] = lhs  // 将 lhs 复制到新数组前半部分
    newArr[lhs.size..lhs.size+rhs.size] = rhs  // 将 rhs 复制到新数组后半部分
    
    return newArr
}

泛型和接口以及子类型结合使用,还可以让我们对泛型中的类型变元给出具体的约束,从而对可以实例化该类型变元的实际类型做出限制。下面的例子中,我们希望在数组 arr 查找元素 element。虽然我们并不关心数组及其元素的具体类型,但元素类型 T 必须能够支持判等操作,让我们能够比较数组中的元素与给定元素是否相等。因此,在 lookup 函数中的 where 子句中,我们要求 T <: Equatable<T>,即类型 T 必须实现了接口 Equatable<T>

// 带类型约束的泛型函数
func lookup<T>(element: T, arr: Array<T>): Bool where T <: Equatable<T> {
    for (e in arr) {  // 遍历数组
        if (element == e) {  // 由于 T 实现了 Equatable,可以使用 == 比较
            return true
        }
    }
    return false
}

仓颉的泛型类型不支持协变。以数组为例,不同元素类型的数组是完全不相同的类型,它们之间不能互相赋值,哪怕元素类型之间具有父子类型关系也是禁止的。这避免了数组协变导致的类型不安全问题。 如下示例所示,Apple 是 Fruit 的子类,但是变量 a 和变量 b 之间是不能互相赋值的,Array<Fruit> 和 Array<Apple> 之间没有子类型关系。

// 泛型不支持协变示例
open class Fruit {}
class Apple <: Fruit {}

main() {
    var a: Array<Fruit> = []
    var b: Array<Apple> = []
    a = b  // 编译错误:Array<Apple> 不是 Array<Fruit> 的子类型
    b = a  // 编译错误:Array<Fruit> 不是 Array<Apple> 的子类型
}

核心概念总结

概念 关键语法 / 符号 作用说明
类与接口 open <: & - 单继承(<: 继承类),多接口实现(<: 后跟 & 分隔的接口)
open 允许类被继承或方法被重写
函数一等公民 => {...}(Lambda) - 函数可赋值给变量、作为参数传递、返回值或存入数据结构
- Lambda 表达式简化匿名函数定义
枚举与模式匹配 enum ` match_` enum 定义带参数的构造器(如 `Node Empty)<br>- match 解析枚举值,_` 匹配任意值
泛型编程 <T> where - 用 <T> 定义通用类型 / 函数
where 约束类型(如要求 T 实现接口)
- 不支持协变,类型严格独立

关键符号速查表

符号 含义
<: 继承类或实现接口(class B <: A & I1:B 继承 A 并实现 I1)
& 组合多个接口(interface I3 <: I1 & I2:I3 继承 I1 和 I2)
open 允许类被继承或方法被重写(open class A / open func f()
=> Lambda 表达式分隔符({x => x*2})或模式匹配分支(case Node(v) => ...
` 枚举构造器分隔符(`enum E {A B}`)
<T> 泛型类型参数(func f<T>(x: T)
where 泛型类型约束(where T <: Equatable<T>
_ 通配符(匹配任意值,如 case _ => ...
Logo

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

更多推荐