match表达式

条件分支语句是构建复杂程序逻辑的核心工具。传统的switch语句虽然在处理简单的离散值匹配时表现良好,但随着编程需求的日益复杂化,其局限性也逐渐显现。仓颉编程引入了强大的match 表达式来替代传统的switch语句,为开发者提供了更加灵活、安全和高效的模式匹配机制。

match 表达式不仅在语法上更加简洁优雅,更重要的是它支持丰富的模式匹配能力,能够处理枚举类型、元组、复杂数据结构等多种场景。通过穷尽性检查机制,match 表达式能够在编译期就发现所有可能的遗漏情况,极大地提高了代码的安全性和可靠性。

一、match 表达式的基础语法与结构

1.1 两种形式的 match 表达式

仓颉支持两种形式的 match 表达式,每种形式都有其特定的应用场景和语法规则。

第一种:包含待匹配值的 match 表达式

这种形式是最常见的 match 表达式,它以关键字match开头,后跟要匹配的值(可以是任意表达式),接着是定义在一对花括号内的若干 case 分支。其基本语法结构如下:

match (待匹配表达式) {
    case 模式1 | 模式2 where 条件 => { 代码块 }
    case 模式3 => { 代码块 }
    ...
    case _ => { 默认操作 }
}

在这个语法结构中,待匹配表达式可以是任何有效的表达式,包括变量、函数调用、计算表达式等。每个case 分支以关键字case开头,后面跟着一个或多个由|连接的模式,模式后面可以跟where子句作为模式守卫条件,最后是=>运算符和相应的代码块。

下面是一个简单的例子,展示了如何使用包含待匹配值的 match 表达式:

main() {
    let x = 0
    match (x) {
        case 1 => let r1 = "x = 1"
                  print(r1)
        case 0 => let r2 = "x = 0"   // Matched.
                  print(r2)
        case _ => let r3 = "x != 1 and x != 0"
                  print(r3)
    }
}

在这个例子中,变量x的值为 0,因此会匹配到第二个 case 分支,输出结果为 "x = 0"。

第二种:不含待匹配值的 match 表达式

这种形式的 match 表达式与第一种相比,关键字match之后没有待匹配的表达式,并且case之后不再是模式,而是类型为Bool的表达式或者_(表示 true)。其语法结构如下:

match {
    case 条件表达式1 => { 代码块 }
    case 条件表达式2 => { 代码块 }
    ...
    case _ => { 默认操作 } // 等价于 case true => ...
}

这种形式的 match 表达式本质上是一个模式化的条件分支结构,它会依次判断每个 case 分支中的条件表达式,直到遇到值为true的条件分支并执行相应的代码块。

下面的例子展示了不含待匹配值的 match 表达式的用法:

main() {
    let x = -1
    match {
        case x > 0 => print("x > 0")
        case x < 0 => print("x < 0")   // Matched.
        case _ => print("x = 0")
    }
}

在这个例子中,变量x的值为 - 1,因此会匹配到第二个 case 分支(x < 0),输出结果为 "x < 0"。

1.2 case 分支的执行逻辑

match 表达式的执行逻辑具有以下特点:

顺序匹配:match 表达式执行时会自上而下依次将待匹配表达式与每个 case 中的模式进行匹配。一旦匹配成功(如果有 pattern guard,也需要 where 之后的表达式的值为 true),则执行=>之后的代码然后立即退出 match 表达式的执行,不会再去匹配它之后的 case 分支。

穷尽性要求:match 表达式要求所有匹配必须是 穷尽(exhaustive) 的,意味着待匹配表达式的所有可能取值都应该被考虑到。当 match 表达式非穷尽,或者编译器判断不出是否穷尽时,均会编译报错。这是一个非常重要的特性,它确保了代码的完整性和安全性。

匹配规则:对于包含多个由|连接的模式的 case 分支,只要待匹配值和其中一个模式匹配则认为匹配成功。例如:

case 10 | 9 => "优秀"

这个 case 分支会匹配值为 10 或 9 的情况。

1.3 支持的模式类型

仓颉 match 表达式支持多种模式类型,每种模式都有其特定的匹配规则和应用场景。

常量模式(Constant Pattern)

常量模式可以是整数字面量、浮点数字面量、字符字面量、布尔字面量、字符串字面量(不支持字符串插值)、Unit 字面量。在包含匹配值的 match 表达式中使用常量模式时,要求常量模式表示的值的类型与待匹配值的类型相同,匹配成功的条件是待匹配的值与常量模式表示的值相等。

下面的例子展示了如何使用常量模式:

main() {
    let score = 90
    let level = match (score) {
        case 0 | 10 | 20 | 30 | 40 | 50 => "D"
        case 60 => "C"
        case 70 | 80 => "B"
        case 90 | 100 => "A"   // Matched.
        case _ => "Not a valid score"
    }
    println(level)
}

在这个例子中,score 的值为 90,因此会匹配到 "90 | 100" 这个 case 分支,输出结果为 "A"。

通配符模式(Wildcard Pattern)

通配符模式使用下划线_表示,可以匹配任意值。通配符模式通常作为最后一个 case 中的模式,用来匹配其他 case 未覆盖到的情况。例如:

case _ => "Other"

这个 case 分支会匹配所有其他未被前面 case 分支覆盖的情况。

绑定模式(Binding Pattern)

绑定模式使用标识符表示,与通配符模式相比,绑定模式同样可以匹配任意值,但绑定模式会将匹配到的值与标识符进行绑定,在=>之后可以通过标识符访问其绑定的值。

下面的例子展示了绑定模式的使用:

main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => "x is not zero and x = ${n}"   // Matched.
    }
    println(y)
}

在这个例子中,变量x的值为 - 10,会匹配到第二个 case 分支,其中n被绑定为 - 10,输出结果为 "x is not zero and x = -10"。

需要注意的是,使用|连接多个模式时不能使用绑定模式,也不可嵌套出现在其它模式中,否则会报错。

元组模式(Tuple Pattern)

元组模式用于匹配元组类型的值,它的定义和元组字面量类似:(p_1, p_2, ..., p_n),区别在于这里的p_1到p_n(n 大于等于 2)是模式而不是表达式。

给定一个元组值tv和一个元组模式tp,当且仅当tv每个位置处的值均能与tp中对应位置处的模式相匹配,才称tp能匹配tv。

下面的例子展示了元组模式的使用:

main() {
    let tv = ("Alice", 24)
    let s = match (tv) {
        case ("Bob", age) => "Bob is ${age} years old"
        case ("Alice", age) => "Alice is ${age} years old"   // Matched.
        case (name, 100) => "${name} is 100 years old"
        case (_, _) => "someone"
    }
    println(s)
}

在这个例子中,元组tv的值为 ("Alice", 24),会匹配到第二个 case 分支,其中age被绑定为 24,输出结果为 "Alice is 24 years old"。

类型模式(Type Pattern)

类型模式用于判断一个值的运行时类型是否是某个类型的子类型。类型模式有两种形式:_: Type(嵌套一个通配符模式_)和id: Type(嵌套一个绑定模式 id),它们的差别是后者会发生变量绑定,而前者并不会。

下面的例子展示了类型模式的使用:

open class Base {
    var a: Int64
    public init() {
        a = 10
    }
}
class Derived <: Base {
    public init() {
        a = 20
    }
}
main() {
    var d = Derived()
    var r = match (d) {
        case b: Base => b.a   // Matched.
        case _ => 0
    }
    println("r = ${r}")
}

在这个例子中,变量d是 Derived 类型的实例,它是 Base 的子类,因此会匹配到第一个 case 分支,输出结果为 20。

enum 模式(Enum Pattern)

enum 模式用于匹配 enum 类型的实例,它的定义和 enum 的构造器类似:无参构造器C或有参构造器C(p_1, p_2, ..., p_n),构造器的类型前缀可以省略,区别在于这里的p_1到p_n(n 大于等于 1)是模式。

给定一个 enum 实例ev和一个 enum 模式ep,当且仅当ev的构造器名字和ep的构造器名字相同,且ev参数列表中每个位置处的值均能与ep中对应位置处的模式相匹配,才称ep能匹配ev。

下面的例子展示了 enum 模式的使用:

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}
main() {
    let x = Year(2)
    let s = match (x) {
        case Year(n) => "x has ${n * 12} months"   // Matched.
        case TimeUnit.Month(n) => "x has ${n} months"
    }
    println(s)
}

在这个例子中,变量x的构造器是 Year,因此会匹配到第一个 case 分支,其中n被绑定为 2,输出结果为 "x has 24 months"。

1.4 模式守卫(Pattern Guard)的使用

在 case 分支的模式之后,可以使用 模式守卫(Pattern Guard) 进一步对匹配出来的结果进行判断。模式守卫使用where cond表示,要求表达式cond的类型为Bool。

模式守卫的执行顺序是:首先进行模式匹配,如果匹配成功,则计算where子句中的条件表达式,如果条件为true,则执行相应的代码块;如果条件为false,则继续匹配下一个 case 分支。

下面的例子展示了模式守卫的使用:

enum RGBColor {
    | Red(Int16) | Green(Int16) | Blue(Int16)
}
main() {
    let c = RGBColor.Green(-100)
    let cs = match (c) {
        case Red(r) where r < 0 => "Red = 0"
        case Red(r) => "Red = ${r}"
        case Green(g) where g < 0 => "Green = 0"   // Matched.
        case Green(g) => "Green = ${g}"
        case Blue(b) where b < 0 => "Blue = 0"
        case Blue(b) => "Blue = ${b}"
    }
    print(cs)
}

在这个例子中,变量c的值是 Green (-100),会匹配到第三个 case 分支(Green (g) where g < 0),因为 g 的值为 - 100 小于 0,所以条件成立,输出结果为 "Green = 0"。

1.5 模式的嵌套组合

Tuple 模式和 enum 模式可以嵌套任意模式,形成复杂的匹配结构。下面的代码展示了不同模式嵌套组合使用的例子:

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}
enum Command {
    | SetTimeUnit(TimeUnit)
    | GetTimeUnit
    | Quit
}
main() {
    let command = SetTimeUnit(Year(2022))
    match (command) {
        case SetTimeUnit(Year(year)) => println("Set year ${year}")
        case SetTimeUnit(Month(month)) => println("Set month ${month}")
        case _ => ()
    }
}

在这个例子中,SetTimeUnit(Year(year))是一个嵌套的 enum 模式,首先匹配 SetTimeUnit 构造器,然后在其参数中进一步匹配 Year 构造器,并将年份值绑定到year变量,输出结果为 "Set year 2022"。

二、match 表达式与传统 switch 语句的比较

2.1 语法结构的差异

传统 switch 语句和仓颉 match 表达式在语法结构上存在显著差异,这些差异直接影响了它们的表达能力和使用方式。

传统 switch 语句的语法结构

传统 switch 语句的基本语法结构如下:

switch (变量) {
    case 值1:
        // 执行代码
        break;
    case 值2:
        // 执行代码
        break;
    default:
        // 默认代码
}

在传统 switch 语句中,case后面必须跟常量值,不能是变量或表达式。每个case分支必须以break语句结尾,否则会发生 穿透(fall-through) 现象。

match 表达式的语法优势

相比之下,match 表达式在语法上具有以下优势:

  1. 更简洁的语法:match 表达式使用case 模式 => 代码块的形式,不需要使用break语句,避免了穿透问题。
  2. 模式匹配能力:match 表达式支持多种模式类型,包括常量模式、通配符模式、绑定模式、元组模式、类型模式和 enum 模式,而传统 switch 语句只能匹配常量值。
  3. 表达式能力:match 表达式本身就是一个表达式,可以直接返回值,而传统 switch 语句是一个语句,不能直接返回值。

下面通过一个简单的例子对比两者的语法差异:

使用 switch 语句判断星期几:

int day = 3;
String result;
switch (day) {
    case 1:
        result = "星期一";
        break;
    case 2:
        result = "星期二";
        break;
    case 3:
        result = "星期三";
        break;
    default:
        result = "未知日期";
}
System.out.println(result);

使用 match 表达式判断星期几:

main() {
    let day = 3
    let result = match (day) {
        case 1 => "星期一"
        case 2 => "星期二"
        case 3 => "星期三"
        case _ => "未知日期"
    }
    println(result)
}

从这个对比可以看出,match 表达式的代码更加简洁,不需要使用break语句,也不需要额外的变量来存储结果。

2.2 功能特性的差异

传统 switch 语句和 match 表达式在功能特性上的差异主要体现在以下几个方面:

数据类型支持的差异

传统 switch 语句在数据类型支持方面存在很大限制。以 Java 为例,传统 switch 语句仅支持以下数据类型:

  • int 及其包装类(Integer)
  • char
  • String(Java 7 及以上)
  • 枚举类型

不支持 long、float、double 或复杂对象。

而 match 表达式支持的数据类型更加广泛:

  • 基本数据类型(整数、浮点数、布尔值等)
  • 字符串
  • 枚举类型
  • 元组类型
  • 自定义对象类型
  • 任何可以进行模式匹配的数据类型

匹配能力的差异

传统 switch 语句只能进行精确匹配,即case后面必须跟一个编译期可确定的常量值,不能进行范围匹配、模式匹配或复杂条件判断。

match 表达式则具有强大的模式匹配能力

  • 支持常量匹配、范围匹配(通过模式守卫)
  • 支持解构匹配(如元组模式、enum 模式)
  • 支持类型匹配(类型模式)
  • 支持条件匹配(模式守卫)

执行逻辑的差异

传统 switch 语句存在严重的穿透问题。如果某个case块后面没有break语句,代码会继续执行下一个case块的代码,直到遇到break或到达 switch 语句的末尾。这种行为虽然在某些场景下有用,但更多时候会导致意想不到的错误。

match 表达式则不存在穿透问题。一旦某个 case 分支匹配成功并执行相应的代码块后,会立即退出 match 表达式,不会执行后续的 case 分支。

表达式与语句的差异

传统 switch 语句是一个语句(statement),不能直接返回值。如果需要获取 switch 语句的执行结果,必须使用一个额外的变量来存储。

match 表达式是一个表达式(expression),可以直接返回值。这意味着可以将 match 表达式的结果直接赋值给变量,或者在其他表达式中使用。

2.3 使用场景的差异

由于功能特性的不同,传统 switch 语句和 match 表达式在实际使用场景中也有很大差异。

传统 switch 语句的适用场景

传统 switch 语句适用于以下简单场景:

  • 基于整数的分支选择
  • 基于枚举类型的分支选择
  • 基于字符串的分支选择(部分语言支持)
  • 少量固定值的条件判断

match 表达式的优势场景

match 表达式在以下场景中具有明显优势:

  1. 复杂数据结构匹配

match 表达式特别适合处理复杂的数据结构,如元组、枚举、自定义对象等。例如,可以直接匹配一个包含姓名和年龄的元组:

main() {
    let person = ("Alice", 24)
    let description = match (person) {
        case ("Bob", age) => "Bob is ${age} years old"
        case ("Alice", age) => "Alice is ${age} years old"
        case (name, 100) => "${name} is 100 years old"
        case (_, _) => "someone"
    }
    println(description)
}

在这个例子中,直接使用元组模式匹配了 person 变量,代码简洁明了。

  1. 枚举类型处理

在处理枚举类型时,match 表达式的优势更加明显。例如,可以匹配一个带有参数的枚举:

enum Shape {
    | Circle(Float)
    | Rectangle(Float, Float)
}
func area(shape: Shape) -> Float {
    match (shape) {
        case Circle(r) => 3.14 * r * r
        case Rectangle(w, h) => w * h
    }
}

在这个例子中,直接从枚举构造器中提取参数并计算面积,无需手动判断类型和提取成员。

  1. 范围匹配和条件判断

虽然传统 switch 语句不支持范围匹配,但 match 表达式可以通过模式守卫实现复杂的条件判断:

main() {
    let score = 85
    let level = match (score) {
        case n where n >= 90 => "优秀"
        case n where n >= 80 => "良好"
        case n where n >= 70 => "中等"
        case n where n >= 60 => "及格"
        case _ => "不及格"
    }
    println(level)
}

在这个例子中,使用模式守卫实现了分数等级的判断,代码简洁且易于维护。

  1. 类型匹配和多态处理

match 表达式支持类型模式,可以方便地进行类型判断和向下转型:

open class Animal {
    public func speak() {
        print("Animal speaks")
    }
}
class Dog <: Animal {
    override public func speak() {
        print("Woof!")
    }
}
class Cat <: Animal {
    override public func speak() {
        print("Meow!")
    }
}
func makeSound(animal: Animal) {
    match (animal) {
        case d: Dog => d.speak()
        case c: Cat => c.speak()
        case _: Animal => animal.speak()
    }
}

在这个例子中,使用类型模式直接判断动物类型并调用相应的方法,无需使用instanceof等类型判断语句。

2.4 安全性和表达能力的差异

在安全性和表达能力方面,match 表达式相比传统 switch 语句具有显著优势。

穷尽性检查的优势

match 表达式最重要的特性之一是穷尽性检查(exhaustive check)。编译器会在编译期检查 match 表达式是否覆盖了所有可能的情况,如果发现有遗漏的情况,会报错提示。

例如,在匹配一个 RGBColor 枚举时,如果没有覆盖所有构造器,编译器会报错:

enum RGBColor {
    | Red | Green | Blue
}
main() {
    let c = Green
    let cs = match (c) {  // Error, Not all constructors of RGBColor are covered.
        case Red => "Red"
        case Green => "Green"
    }
    println(cs)
}

在这个例子中,由于没有处理 Blue 情况,编译器会报错提示 “RGBColor 的所有构造器未被覆盖”。

传统 switch 语句则没有这种穷尽性检查机制,即使遗漏了某些 case 分支,也不会在编译期报错,只有在运行时才可能发现问题。

类型安全的保证

match 表达式在类型安全方面也有优势:

  • 模式必须与待匹配值的类型兼容
  • 类型模式会自动进行类型检查和转换
  • 所有模式匹配都在编译期进行类型检查

传统 switch 语句在类型安全方面存在不足:

  • 只能匹配有限的数据类型
  • 没有类型模式匹配能力
  • 需要手动进行类型转换和检查

代码可读性和可维护性

match 表达式在代码可读性和可维护性方面具有以下优势:

  1. 代码简洁性:match 表达式使用更简洁的语法,减少了样板代码。例如,无需编写大量的break语句。
  2. 模式清晰性:通过模式匹配,可以直接从复杂数据结构中提取所需信息,代码意图更加清晰。
  3. 维护便利性:当需要添加新的情况时,只需要在 match 表达式中添加新的 case 分支,不会影响现有的逻辑。

传统 switch 语句的劣势包括:

  • 容易出现穿透错误
  • 代码冗长,需要大量break语句
  • 难以处理复杂的数据结构
  • 维护困难,修改一个 case 可能影响其他 case

2.5 性能考虑

虽然 match 表达式在功能和安全性方面有很多优势,但在某些极端性能敏感的场景中,可能需要考虑其性能影响。

match 表达式的性能特点

  1. 模式匹配的效率:对于简单的常量匹配,match 表达式的性能与 switch 语句相当。但对于复杂的模式匹配(如嵌套模式、类型模式),可能会有一定的性能开销。
  2. 穷尽性检查的开销:穷尽性检查是在编译期进行的,不会产生运行时开销。
  3. 跳转表优化:现代编译器会对 match 表达式进行优化,特别是对于枚举类型,会生成跳转表实现 O (1) 时间复杂度的匹配。

性能优化建议

  1. 在性能要求极高的场景,可以考虑使用更简单的条件判断语句。
  2. 对于大量的模式匹配,可以尝试将常用的模式放在前面,减少匹配时间。
  3. 避免在循环中进行复杂的模式匹配,可以提前将需要匹配的值提取出来。

结论

通过对仓颉 match 表达式的深入分析以及与传统 switch 语句的全面对比,我们可以得出以下结论:

match 表达式的核心优势:

  1. 强大的模式匹配能力:支持常量、通配符、绑定、元组、类型、enum 等多种模式,能够处理各种复杂的数据结构。
  2. 简洁的语法结构:使用case 模式 => 代码块的形式,无需break语句,避免了穿透问题,代码更加简洁优雅。
  3. 穷尽性检查机制:编译器在编译期强制检查是否覆盖所有可能情况,从根本上避免了遗漏分支的错误,极大地提高了代码的安全性。
  4. 表达式特性:match 表达式本身就是一个表达式,可以直接返回值,使用更加灵活。
  5. 类型安全保证:支持类型模式匹配,自动进行类型检查和转换,提供了更好的类型安全。
  6. 模式守卫功能:通过where子句可以实现复杂的条件判断,包括范围匹配等传统 switch 无法实现的功能。

与传统 switch 语句的本质区别:

  1. 语法层面:match 表达式语法更简洁,无需break语句,支持更多模式类型。
  2. 功能层面:match 表达式支持复杂的模式匹配、类型匹配、范围匹配等,而传统 switch 只能进行简单的常量匹配。
  3. 安全层面:match 表达式具有穷尽性检查,传统 switch 没有此功能。
  4. 使用场景:match 表达式适合处理复杂数据结构、枚举类型、需要模式匹配的场景;传统 switch 适合处理简单的离散值匹配。

适用场景建议:

  1. 推荐使用 match 表达式
    • 处理枚举类型时
    • 需要匹配复杂数据结构(如元组、自定义对象)时
    • 需要进行范围匹配或条件匹配时
    • 需要类型匹配和多态处理时
    • 处理 Option 类型进行错误处理时
  1. 传统 switch 的保留场景
    • 简单的整数、字符、字符串匹配
    • 性能要求极高且模式匹配不复杂的场景
    • 与已有代码保持一致性的场景
Logo

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

更多推荐