Baeldung Pro – Scala – NPI EA (类别 = Baeldung on Scala)
announcement - icon

通过超简洁的 Baeldung Pro 体验学习

>> 会员和 Baeldung Pro.

没有广告,深色模式,并免费获得 6 个月的 IntelliJ Idea Ultimate,供您入门。

1. 概述

模式匹配是 Scala 语言的一个强大特性。它允许编写更简洁、更易读的代码,同时提供根据复杂模式匹配元素的能力。

在本教程中,我们将发现如何使用模式匹配,以及如何从中受益。

2. 模式匹配

与 Java 的 switch 语句中的“精确匹配”不同,模式匹配允许匹配模式而不是精确值。

例如,在 Java 中,如果 input.equals(caseClause) 返回 false,我们会直接评估下一个 case clause。 然而,在 Scala 中并非总是如此。

让我们更详细地了解模式匹配的工作原理。

2.1. 语法

match 表达式由多个部分组成

  1. 我们将用于匹配模式的值称为 候选值 
  2. 关键字 match
  3. 多个 case 子句 ,包括 case 关键字、 模式、一个箭头符号以及模式匹配时要执行的代码
  4. 当没有其他模式匹配时,一个 默认子句 。 默认子句 以划线字符 (_) 开头,并且是 case 子句的最后一个。

让我们看一个简单的示例来说明这些部分

def patternMatching(candidate: String): Int = {
  candidate match { 
    case "One" => 1 
    case "Two" => 2 
    case _ => -1 
  }
}

3. Match 表达式中的模式

3.1. 案例类

案例类帮助我们使用继承的力量来执行模式匹配。案例类扩展了一个通用的抽象类。然后,match 表达式将对抽象类的引用与每个案例类表示的每个模式进行评估。

让我们从编写我们的类开始

abstract class Animal

case class Mammal(name: String, fromSea: Boolean) extends Animal

case class Bird(name: String) extends Animal

case class Fish(name: String) extends Animal

现在让我们看看如何将模式匹配应用于案例类

def caseClassesPatternMatching(animal: Animal): String = {
  animal match {
    case Mammal(name, fromSea) => s"I'm a $name, a kind of mammal. Am I from the sea? $fromSea"
    case Bird(name) => s"I'm a $name, a kind of bird"
    case _ => "I'm an unknown animal"
  }
}

这种模式匹配的另一种说法是构造函数匹配,这意味着在这种情况下使用构造函数来使匹配成为可能。

例如,我们可以注意到Mammal 模式与上面定义的案例类的构造函数完全匹配。

3.2. 常量

与大多数语言一样,Scala 使用常量来定义数字或布尔值。模式可以由常量组成。

让我们看看如何在 match 表达式中使用常量

def constantsPatternMatching(constant: Any): String = {
  constant match {
    case 0 => "I'm equal to zero"
    case 4.5d => "I'm a double"
    case false => "I'm the contrary of true"
    case _ => s"I'm unknown and equal to $constant"
  }
}

3.3. 序列

数组、列表和向量由元素组成。这些序列及其元素也用于形成模式。

此外,我们通常希望使用通配符来表达模式的动态部分

  • 要匹配单个元素,我们将使用下划线通配符 _ 这不要与默认子句混淆,默认子句也使用下划线字符。也可以使用别名来表示该元素
  • 另一方面,要匹配未知数量的元素(零个、一个或多个),我们将使用星号通配符 *

让我们看看将序列用作模式的样子

def sequencesPatternMatching(sequence: Any): String = {
  sequence match {
    case List(singleElement) => s"I'm a list with one element: $singleElement"
    case List(_, _*) => s"I'm a list with one or multiple elements: sequence"
    case Vector(1, 2, _*) => s"I'm a vector: $sequence"
    case _ => s"I'm an unrecognized sequence. My value: $sequence"
  }
}

在第一个 case 子句中,我们使用别名 –  singleElement – 来定义List的单个元素

在其他 case 子句中,我们只是使用下划线字符来忽略这些值。 除了用于默认 case 子句外,下划线字符也可以在 match 表达式中忽略特定值时使用。

3.4. 元组

元组是包含有限数量子对象的对象。我们可以将它们想象成具有有限大小的混合元素集合。

接下来,我们来看一个在模式匹配中使用元组的例子

def tuplesPatternMatching(tuple: Any): String = {
  tuple match {
    case (first, second) => s"I'm a tuple with two elements: $first & $second"
    case (first, second, third) => s"I'm a tuple with three elements: $first & $second & $third"
    case _ => s"Unrecognized pattern. My value: $tuple"
  }
}

在这个例子中,我们使用在元组模式中定义的名称提取了元素。如果我们想使用元组的第一个元素,我们将在元组模式中定义一个局部变量。在上面的例子中,我们可以将这些变量识别为firstsecondthird

3.5. 类型模式

Scala 是一种类型语言,这意味着每个对象都有一个静态类型,该类型不能更改。例如,一个 Boolean 对象只能包含布尔表达式。

Scala 使得根据类型模式匹配对象变得容易,如下所示

def typedPatternMatching(any: Any): String = {
  any match {
    case string: String => s"I'm a string. My value: $string"
    case integer: Int => s"I'm an integer. My value: $integer"
    case _ => s"I'm from an unknown type. My value: $any"
  }
}

3.6. 正则表达式模式

我们已经知道正则表达式在处理字符字符串时有多么有用。在 Scala 中,好消息是——我们也可以在匹配表达式中匹配对象时使用正则表达式

def regexPatterns(toMatch: String): String = {
  val numeric = """([0-9]+)""".r
  val alphabetic = """([a-zA-Z]+)""".r
  val alphanumeric = """([a-zA-Z0-9]+)""".r

  toMatch match {
    case numeric(value) => s"I'm a numeric with value $value"
    case alphabetic(value) => s"I'm an alphabetic with value $value"
    case alphanumeric(value) => s"I'm an alphanumeric with value $value"
    case _ => s"I contain other characters than alphanumerics. My value $toMatch"
  }
}

3.7. 字符串插值模式

在上一节中,我们探讨了使用正则表达式进行模式匹配。然而,在许多情况下,正则表达式的复杂性是不必要的。通常,匹配字符串中更简单的模式就足够了。我们可以通过将 字符串插值 与模式匹配相结合来提取相关部分

def stringInterpolationMatching(toMatch: String): String = {
  toMatch match {
    case s"$firstName.$lastName@$domain.$extension" =>
      s"Hey ${firstName.capitalize} ${lastName.capitalize}, $domain.$extension is your email domain"
    case s"$day-$month-${year}T$time" => s"$month $day, $year"
    case s"$something($parenthesis)${_}" => s"String between parenthesis: $parenthesis"
    case _ => "unknown pattern"
  }
}

在上面的例子中,我们在 case 语句中使用了简单的字符串插值。提取括号之间的文本是数据分析中常见的任务,通常使用正则表达式处理。然而,我们展示了如何在没有正则表达式的情况下做到这一点。这种方法允许我们毫不费力地根据 case 模式将字符串的不同部分提取到单独的变量中。

3.8. 选项:Some<T>None

在 Scala 等函数式语言中,选项是结构,可以包含或不包含一个值。Scala 中的 Option 很容易与 Java 的 Optional 类进行比较。

可以使用 Option 对象进行模式匹配。在这种情况下,我们将有两个可能的 case 子句

  • Some<T> — 包含类型为 T 的值
  • None — 不包含任何内容

让我们看看如何在我们的匹配表达式中使用它们

def optionsPatternMatching(option: Option[String]): String = {
  option match {
    case Some(value) => s"I'm not an empty option. Value $value"
    case None => "I'm an empty option"
  }
}

3.9. 变量绑定

也可以使用 @ 符号将变量绑定到完全或部分匹配结果

def binderPatternMatching(animal: Any): String = {
    animal match {
      case m@Mammal(_, true) => s"${m.name} is a mammal from sea"
      case Mammal(name, fromSea) => s"${name} is a mammal, fromSea:${fromSea}"
      case _ => "unknown animal"
    }
  }

在这种情况下,变量 m 现在被分配给任何具有 fromSeatrueMammal 对象。

同样,我们可以将匹配对象的某个部分绑定到变量。假设我们只想将值 Lion 分配给变量 name

def binderPatternWithPartMatch(animal: Any): String = {
  animal match {
    case Mammal(name @ "Lion", _) => s"$name is a mammal"
    case _ => "unknown"
  }
}

4. 模式守卫

我们已经看到了模式匹配的强大之处。我们可以以许多不同的方式构建我们的模式。但有时,我们希望确保除了模式匹配之外,还满足特定条件,才能执行 case 子句内的代码。

我们可以使用模式守卫来实现这种行为。模式守卫是与 case 子句相同级别的布尔表达式。

让我们看看使用模式守卫有多么方便

def patternGuards(toMatch: Any, maxLength: Int): String = {
  toMatch match {
    case list: List[Any] if (list.size <= maxLength) => "List is of acceptable size"
    case list: List[Any] => "List has not an acceptable size"
    case string: String if (string.length <= maxLength) => "String is of acceptable size"
    case string: String => "String has not an acceptable size"
    case _ => "Input is neither a List nor a String"
  }
}

在代码片段的例子中,我们注意到 StringList 对象都有两个模式。每个模式之间的区别在于,其中一个模式在进入 case 子句之前还会检查对象长度。

例如,如果一个 List 包含 5 个对象但最大长度为 6,那么它将进入第一个 case 子句并返回。另一方面,如果最大长度为 4,那么它将执行第二个子句中的代码。

5. 密封类

我们有时希望将密封类与模式匹配一起使用。 密封类是一个超类,它知道所有扩展它的类。 这种行为可以通过在同一个文件中表达密封类及其所有子类来实现。

当我们想避免在匹配表达式中使用默认行为时,此功能特别有用。

让我们从编写密封类及其子类开始

sealed abstract class CardSuit

case class Spike() extends CardSuit

case class Diamond() extends CardSuit

case class Heart() extends CardSuit

case class Club() extends CardSuit

之后,让我们使用模式匹配来使用这些类

def sealedClass(cardSuit: CardSuit): String = {
  cardSuit match {
    case Spike() => "Card is spike"
    case Club() => "Card is club"
    case Heart() => "Card is heart"
    case Diamond() => "Card is diamond"
  }
}

这里,默认 case 子句的使用不是强制性的,因为我们为每个子类都有一个 case。

6. 提取器

提取器对象是包含名为 unapply 的方法的对象。当模式匹配成功时,将执行此方法。

让我们在一个例子中使用它。假设我们有一个包含全名的 Person,当 Person 与模式匹配时,我们不想使用全名,而只想获取他们的首字母。

让我们看看提取器如何帮助我们实现此需求。

首先,我们将创建 Person 对象

object Person {
  def apply(fullName: String) = fullName

  def unapply(fullName: String): Option[String] = {
    if (!fullName.isEmpty)
      Some(fullName.replaceAll("(?<=\\w)(\\w+)", "."))
    else
      None
  }
}

现在 Person 对象存在,我们将能够在我们的 match 表达式中使用它,并利用 unapply 方法的结果

def extractors(person: Any): String = {
  person match {
    case Person(initials) => s"My initials are $initials"
    case _ => "Could not extract initials"
  }
}

如果这个人名为 John Smith,在这种情况下,返回的 String 将是‘我的首字母是 J. S.’。

7. 其他用法

7.1. 闭包

闭包也可以使用模式匹配。

让我们看看闭包模式匹配在实践中是什么样的

def closuresPatternMatching(list: List[Any]): List[Any] = {
  list.collect { case i: Int if (i < 10) => i }
}

当调用时,这段代码将

  1. 过滤掉不是 Int 的元素
  2. 过滤掉小于 10 的 Int
  3. 将剩余的元素从 List 中收集到一个新的 List

7.2. Catch 块

我们还可以使用模式匹配来处理 try-catch 块中抛出的异常。

让我们实际看一下

def catchBlocksPatternMatching(exception: Exception): String = {
  try {
    throw exception
  } catch {
    case ex: IllegalArgumentException => "It's an IllegalArgumentException"
    case ex: RuntimeException => "It's a RuntimeException"
    case _ => "It's an unknown kind of exception"
  }
}

8. 结论

在本教程中,我们发现如何以多种不同的方式使用 Scala 的强大模式匹配。

支持本文的代码可在 GitHub 上获取。 一旦你Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。
© .