1. Values, Variables, and Types in Matches
(1) Basic
Let’s cover several kinds of matches
. The following example matches on specific values, all values of specific types, and it shows one way of writing a “default” clause that matches anything:
code example
object Hello {
def main(args: Array[String]): Unit = {
for (x <- Seq(1, 2, 2.7, "one", "two", 'four)) {
val str = x match {
case 1 => "int 1"
case i: Int => "other int: "+i // 假设匹配此句, i=x
case d: Double => "a double: "+x
case "one" => "string one"
case s: String => "other string: "+s
case unexpected => "unexpected value: "+unexpected
}
println(str)
}
}
}
Note:
- Because
x
is of typeAny
, we need enough clauses to cover all possible values. That’s why the “default” clause (withunexpected
) is needed. - Matches are eager(及早的), so more specific clauses must appear before less specific clauses.(如果
case 1 => "int 1"
和case i: Int => "other int: "+i
交换顺序,则case 1
将永远不会匹配). - Matching on floating-point literals is a bad idea, as rounding errors mean two values that appear to be the same often differ in the least significant digits.
(2) Using Placeholder _
We can replace the variables i
, d
, s
, and unexpected
with the placeholder _
.
object Hello {
def main(args: Array[String]): Unit = {
for (x <- Seq(1, 2, 2.7, "one", "two", 'four)) {
val str = x match {
case 1 => "int 1"
case _: Int => "other int: "+x
case _: Double => "a double: "+x
case "one" => "string one"
case _: String => "other string: "+x
case _ => "unexpected value: "+x
}
println(str)
}
}
}
(3) Using defined variable after case
In case
clauses, a term that begins with a lowercase letter is assumed to be the name of a new variable that will hold an extracted value. To refer to a previously defined variable, enclose it in back ticks. Conversely, a term that begins with an uppercase letter is assumed to be a type name.
object Hello {
def main(args: Array[String]): Unit = {
checkY(100)
}
def checkY(y :Int): Unit = {
for (x <- Seq(99, 100, 101)) {
val str = x match {
case `y` => "found y!" // 如果你使用y,而不是`y`,则它并不是函数参数y
case i: Int => "int: "+i
}
println(str)
}
}
}
(4) Using |
Sometimes we want to handle several different matches with the same code body. To avoid duplication, we could refactor the code body into a method, but case
clauses also support an “or” construct, using a |
method:
code example
object Hello {
def main(args: Array[String]): Unit = {
for (x <- Seq(1, 1.2, 3, "haha")) {
val str = x match {
case _: Int | _: Double => "number: "+x
case _ => "string: "+x
}
println(str)
}
}
}
output
number: 1
number: 1.2
number: 3
string: haha
2. Matching Sequences
(1) Matching Sequences
Seq
(for “sequence”) is a parent type for the concrete collection types that support iteration over the elements in a deterministic order, such as List
and Vector
.
Let’s examine the classic idiom for iterating through a Seq
using pattern matching and recursion, and along the way, learn some useful fundamentals about sequences:
object Hello {
def main(args: Array[String]): Unit = {
val nonEmptySeq = Seq(1, 2, 3, 4, 5)
val emptySeq = Seq.empty[Int]
val nonEmptyList = List(1, 2, 3, 4, 5)
val emptyList = Nil
val nonEmptyVector = Vector(1, 2, 3, 4, 5)
val emptyVector = Vector.empty[Int]
val nonEmptyMap = Map("one"->1, "two"->2, "three"->3)
val emptyMap = Map.empty[String, Int]
val newSeq = Seq(nonEmptySeq, emptySeq, nonEmptyList, emptyList, nonEmptyVector, emptyVector, nonEmptyMap.toSeq, emptyMap.toSeq) // (4)
for (seq <- newSeq){
println(seqToString(seq))
}
}
def seqToString[T](seq: Seq[T]): String = { // (1)
seq match {
case head +: tail => s"$head +: "+seqToString(tail) // (2)
case Nil => "Nil" // (3)
}
}
}
Explain:
(1)
Define a recursive method that constructs aString
from aSeq[T]
for some typeT
. The body is one expression that matches on the inputSeq[T]
。(2)
There are twomatch
clauses and they are exhaustive. The first matches on any nonemptySeq
, extracting thehead
, the first element, and thetail
, which is the rest of theSeq
. (Seq
hashead
andtail
methods, but here these terms are interpreted as variable names as usual forcase
clauses.) The body of the clause constructs aString
with thehead
followed by+:
followed by the result of callingseqToString
on thetail
.(3)
The only other possiblecase
is an emptySeq
. We can use the special object for empty Lists,Nil
, to match all the empty cases. Note that anySeq
can always be interpreted as terminating with an empty instance of the same type, although only some types, likeList
, are actually implemented that way.(4)
Put theSeqs
in anotherSeq
(callingtoSeq
on the Maps to convert them to a sequence of key-value pairs), then iterate through it and print the results of callingseqToString
on each one.
Note:
- The operator
+:
is the “cons”(construction) operator for sequences. It is similar to the::
operator forLists
. - The terms
head
andtail
are arbitrary variable names. That’s mean that you can replace them withx
andy
. - If your sequence is
List
,you can replace+:
with::
. But it’s more conventional now to write code that usesSeq
, so it can be applied to all subclasses, includingList
andVector
.
(2) Reconstruct List
, Map
We can use +:
and ::
restruct List
:
scala> val s1 = (1 +: (2 +: (3 +: (4 +: (5 +: Nil)))))
s1: List[Int] = List(1, 2, 3, 4, 5)
scala> val l = (1 :: (2 :: (3 :: (4 :: (5 :: Nil)))))
l: List[Int] = List(1, 2, 3, 4, 5)
scala> val s2 = (("one",1) +: (("two",2) +: (("three",3) +: Nil)))
s2: List[(String, Int)] = List((one,1), (two,2), (three,3), (four,4))
scala> val m = Map(s2 :_*)
m: scala.collection.immutable.Map[String,Int] = Map(one -> 1, two -> 2, three -> 3, four -> 4)
Note that the Map.apply
factory method expects a variable argument list of two-element tuples. So, in order to use the sequence s2
to construct a Map
, we had to use the :_*
idiom for the compiler to convert it to a variable-argument list.
So, there’s an elegant symmetry between construction and pattern matching (“deconstruction”) when using +:
and ::
.
3. Matching on Tuples
Tuples are so easy to match on:
object Hello {
def main(args: Array[String]): Unit = {
val langs = Seq(
("Scala", "Martin", "Odersky"),
("Clojure", "Rich", "Hickey"),
("Lisp", "John", "McCarthy"))
for (tuple <- langs) {
tuple match {
case ("Scala", _, _) => println("Found Scala")
case (lang, firstName, lastName) => println(s"Found other language: $lang, $firstName $lastName")
}
}
}
}
output
Found Scala
Found other language: Clojure, Rich Hickey
Found other language: Lisp, John McCarthy
A tuple
can be taken apart into its constituent elements. We can match on literal values within the tuple
, at any positions we want, and we can ignore elements we don’t care about.
4. Guards in case
Clauses
Code Example:
object Hello {
def main(args: Array[String]): Unit = {
val list = List(1, 2, 3, 4, 5)
for (value <- list) {
value match {
case _ if value%2 == 0 => println(s"even: $value")
case _ => println(s"odd: $value")
}
}
}
}
Output:
odd: 1
even: 2
odd: 3
even: 4
odd: 5
5. Maching on case
Classes
(1) Deep Matching
Let’s see more examples of deep matching, where we examine the contents of instances of case
classes:
Code Example:
case class Address(street: String, city: String, country: String)
case class Person(name: String, age: Int, address: Address)
val alice = Person("Alice", 25, Address("1 Scala Lane", "Chicago", "USA"))
val bob = Person("Bob", 29, Address("2 Java Ave.", "Miami", "USA"))
val charlie = Person("Charlie", 32, Address("3 Python Ct.", "Boston", "USA"))
for (person <- Seq(alice, bob, charlie)) {
person match {
case Person("Alice", 25, Address(_, "Chicago", _) => println("Hi Alice!")
case Person("Bob", 29, Address("2 Java Ave.", "Miami", "USA")) => println("Hi Bob!")
case Person(name, age, _) => println(s"Who are you, $age year-old person named $name?")
}
}
Output:
Hi Alice!
Hi Bob!
Who are you, 32 year-old person named Charlie?
Note that we could match into nested types.