Skip to content

Scala 提取器

提取器是 Scala 中定义了 unapply 方法的对象。提取器的主要目的是解构(deconstruct)一个值,通常在模式匹配(case 子句)中使用,有效地反转了构造过程。

虽然提取器通常与 apply 方法(用于构造)配对出现,但对象要成为提取器只需要 unapply 方法。

unapply 方法将待解构的值作为输入,通常返回一个包含提取组件的 Option。

  • 用于单个值提取: def unapply(input: InputType): Option[ExtractedType]
  • 用于多个值提取: def unapply(input: InputType): Option[(Type1, Type2, ...)](返回一个包含 Tuple 的 Option)

Option 返回类型处理输入不匹配预期结构的情况:Some(...) 表示成功匹配和提取,而 None 表示失败。

让我们创建一个提取器对象来解析电子邮件地址为用户和域名部分。

object Email {
// 可选:用于构造的 'apply' 方法(注入)
def apply(user: String, domain: String): String = {
s"$user@$domain"
}
// 必须:用于解构的 'unapply' 方法(提取)
def unapply(emailString: String): Option[(String, String)] = {
val parts = emailString.split('@')
if (parts.length == 2 && parts(0).nonEmpty && parts(1).nonEmpty) {
Some((parts(0), parts(1))) // 成功:返回 Some(tuple)
} else {
None // 失败:返回 None
}
}
}
@main def ExtractorDemo(): Unit = {
// 使用 apply (可选)
val emailAddr = Email("alice", "example.com")
println(s"Constructed: $emailAddr")
// 通过模式匹配隐式使用 unapply
def checkEmail(addr: String): Unit = addr match {
case Email(user, domain) => // unapply 在这里被调用!
println(s"Matched! User: $user, Domain: $domain")
case _ =>
println(s"'$addr' is not a valid email address.")
}
checkEmail("bob@test.org")
checkEmail("invalid-email")
checkEmail(emailAddr) // 检查我们构造的那个
}

输出:

Constructed: alice@example.com
Matched! User: bob, Domain: test.org
'invalid-email' is not a valid email address.
Matched! User: alice, Domain: example.com

在 match 表达式中,case Email(user, domain) => ... 触发 Email.unapply(addr) 调用。如果它返回 Some((u, d)),则匹配成功,并且 u 被绑定到 user,d 被绑定到 domain。如果它返回 None,则该 case 失败。

case class 的一个关键特性是 Scala 编译器会根据类参数自动生成一个包含 apply 和 unapply 方法的伴生对象。这使得它们可以开箱即用地与模式匹配无缝协作。

case class Person(name: String, age: Int)
val alice = Person("Alice", 30)
alice match {
// 调用编译器自动生成的 Person.unapply(alice)
case Person(n, a) => println(s"Name: $n, Age: $a")
}

unapply 方法也可以返回 Boolean 用于简单的检查(尽管 Option[Boolean] 或自定义类型通常更清晰)。如果 unapply(value) 返回 true,则模式匹配。

object IsEven {
def unapply(num: Int): Boolean = num % 2 == 0
}
@main def BooleanExtractorDemo(): Unit = {
val number = 42
number match {
case IsEven() => println(s"$number is even.") // 如果 IsEven.unapply(number) 为 true 则匹配
case _ => println(s"$number is odd.")
}
val oddNum = 7
oddNum match {
case IsEven() => println(s"$oddNum is even.")
case _ => println(s"$oddNum is odd.") // 在这里匹配
}
}

对于匹配元素序列,提取器可以定义 unapplySeq 方法。它返回一个序列类型(如 Seq[T])的 Option。这使得 case MyExtractor(a, b, _*) => ... 这样的模式成为可能。

object DomainParts {
// 提取由点分隔的域名部分
def unapplySeq(domain: String): Option[Seq[String]] = {
val parts = domain.split('.').toSeq
if (parts.nonEmpty && parts.forall(_.nonEmpty)) Some(parts) else None
}
}
@main def UnapplySeqDemo(): Unit = {
"www.example.com" match {
// 如果存在 3 个部分则匹配,忽略其余部分
case DomainParts(first, second, third, _*) =>
println(s"Major parts: $first, $second, $third") // www, example, com
case _ => println("Could not parse domain")
}
"example.co.uk" match {
case DomainParts(name, tld1, tld2) => // 精确匹配 3 个部分
println(s"Name: $name, TLDs: $tld1.$tld2") // example, co.uk
case _ => println("Not a 3-part domain")
}
}

提取器提供了一种强大的机制,用于自定义模式匹配,实现对复杂数据类型的干净解构。