1. 程式人生 > 其它 >Scala模式匹配

Scala模式匹配

[toc]

## Scala模式匹配

> Scala中的模式匹配`類似於Java中的switch語法`,但是更加強大。
>
> 模式匹配語法中,採用==match關鍵字宣告==,每個分支==採用case關鍵字==進行宣告,當需要匹配時,會從第一個case分支開始,如果匹配成功,那麼執行對應的邏輯程式碼,如果匹配不成功,繼續執行下一個分支進行判斷。如果所有case都不匹配,那麼會執行`case _ 分支`,類似於Java中default語句。

### 1. Scala模式匹配快速入門

~~~~scala
/**
* @Date 2021/4/3 14:55
* @Version 10.21
* @Author DuanChaojie
*/
object MatchDemo01 {
def main(args: Array[String]): Unit = {
val oper = '+'
val n1 = 20
val n2 = 10
var res = 0

/**
* 說明
* 1. match (類似java switch) 和 case 是關鍵字
* 2. 如果匹配成功, 則執行 => 後面的程式碼塊.
* 3. 匹配的順序是從上到下,匹配到一個就執行對應的 程式碼
* 4. => 後面的程式碼塊 不要寫 break,會自動的退出match
* 5. 如果一個都沒有匹配到,則執行 case _ 後面的程式碼塊
*/
oper match {
case '+' => res = n1 + n2
case '-' => res = n1 - n2
case '*' => res = n1 * n2
case '/' => res = n1 / n2
case '%' => res = n1 % n2
case _ => println("oper error")
}

println("res = " + res)
}
}
~~~~

> match的細節和注意事項:
>
> 1. 如果所有case都不匹配,那麼會執行case _ 分支,類似於Java中default語句
> 2. 如果所有case都不匹配,又沒有寫case _ 分支,那麼會丟擲MatchError
> 3. 每個case中,不用break語句,自動中斷case
> 4. 可以在match中使用其它型別,而不 僅僅是字元
> 5. `=>` 等價於 java swtich 的 `:`
> 6. `=>` 後面的程式碼塊到下一個 case, 是作為一個整體執行,可以使用{} 擴起來,也可以不擴。

#### 條件守衛

如果想要表達匹配某個範圍的資料,就需要在模式匹配中增加條件守衛。

```scala
/**
* @Date 2021/4/3 15:21
* @Version 10.21
* @Author DuanChaojie
*/
object MatchIfDemo01 {
def main(args: Array[String]): Unit = {
/**
* 是對"+-3!" 遍歷
*/
for (ch <- "+-3!") {

var sign = 0
var digit = 0
// 模式匹配
ch match {
case '+' => sign = 1
case '-' => sign = -1

/**
* 如果 case 後有 條件守衛即if ,那麼這時的 _ 不是表示預設匹配
* 表示忽略傳入的 ch
*/
case _ if ch.toString.equals("3") => digit = 3
case _ if (ch > 1110 || ch < 120) => println("ch > 10")
case _ => sign = 2
}

/**
* 分析
* + 1 0
* - -1 0
* 3 0 3
* ! 2 0
* ch + "" + sign + " " + digit
*/
println(s"\nch = $ch \nsign = $sign \ndigit = $digit")
}
}
}
```

#### 課堂練習

> 練習一:

~~~~scala
/**
* @Date 2021/4/3 15:40
* @Version 10.21
* @Author DuanChaojie
*/
object MatchExercise01 {
def main(args: Array[String]): Unit = {

for (ch <- "+-3!") {

var sign = 0
var digit = 0

ch match {
case '+' => sign = 1
case '-' => sign = -1
// 可以有多個 預設匹配,但是後面的預設匹配無效,編譯器沒有報錯
case _ => digit = 3
case _ => sign = 2
}

/**
* + 1 0
* - -1 0
* 3 0 3
* ! 0 3
*/
println(s"\nch = $ch \nsign = $sign \ndigit = $digit")
}
}
}
~~~~

> 練習二:

~~~~scala
/**
* @Date 2021/4/3 16:00
* @Version 10.21
* @Author DuanChaojie
*/
object MatchExercise02 {
def main(args: Array[String]): Unit = {
for (ch <- "+-3!") {

var sign = 0
var digit = 0

ch match {
case _ if ch > 10000 => digit = 3
case '+' => sign = 1
case '-' => sign = -1
// 說明..
case _ => println("沒有任何匹配~~~")
}

/**
* 結果分析
* + 1 0
* - -1 0
* 沒有任何匹配~~~ 3 0 0
* 沒有任何匹配~~~ ! 0 0
*/
println(s"\nch = $ch \nsign = $sign \ndigit = $digit")
}
}
}
~~~~

#### 模式中的變數

> 如果在case關鍵字後跟變數名,那麼match前表示式的值會賦給那個變數

~~~~scala
/**
* @Date 2021/4/3 16:05
* @Version 10.21
* @Author DuanChaojie
*/
object MatchVarDemo01 {
def main(args: Array[String]): Unit = {
val ch = 'U'
// 模式匹配
ch match {
case '+' => println("ok~")
// 下面 case mychar 含義是 mychar = ch
case mychar => println("ok~" + mychar)
case _ => println("ok~~")
}

val ch1 = '+'
/**
* match是一個表示式,因此可以有返回值
* 返回值就是匹配到的程式碼塊的最後一句話的值
*/
val res = ch1 match {
case '+' => ch1 + " hello "
// 下面 case mychar 含義是 mychar = ch
case _ => println("ok~~")
}

println("res = " + res)
}
}
~~~~

> 變數宣告中的模式:match中每一個case都可以單獨提取出來,意思是一樣的。

~~~~scala
/**
* @Date 2021/4/3 16:52
* @Version 10.21
* @Author DuanChaojie
*/
object MatchVarDemo02 {
def main(args: Array[String]): Unit = {
val (x, y, z) = (1, 2, "Hello")

/**
* x = 1
* y = 2
* z = Hello
*/
println(s"x = $x\ny = $y\nz = $z\n")
/**
* i = BigInt(10)/3
* j = BigInt(10)%3
*/
val (i, j) = BigInt(10) /% 3

/**
* i = 3
* j = 1
*/
println(s"i = $i\nj = $j")
val arr = Array(1, 3, 5, 7, 9)
// 提出 arr 的前兩個元素
val Array(first, second, _*) = arr

/**
* first = 1
* second = 3
*/
println(s"first = $first \nsecond = $second")
}
}
~~~~

#### For表示式中的模式

for表示式中的模式

~~~~scala

/**
* @Date 2021/4/3 17:18
* @Version 10.21
* @Author DuanChaojie
*/
object MatchForDemo01 {
def main(args: Array[String]): Unit = {

val map = Map("A" -> 1, "B" -> 0, "C" -> 3)

// 出來三個key-value ("A"->1), ("B"->0), ("C"->3)
for ((k, v) <- map) {
println(k + " -> " + v)
}

/**
* 說明 : 只遍歷出 value = 0 的key-value ,其它的過濾掉
*/
println("--------------(k, 0) <- map-------------------")
for ((k, 0) <- map) {
println(k + " --> " + 0)
}

/**
* 說明:這個就是上面程式碼的另外寫法,只是下面的用法靈活和強大
*/
println("--------------(k, v) <- map if v == 0-------------------")
for ((k, v) <- map if v >= 1) {
println(k + " ---> " + v)
}
}
}
~~~~

### 2. Scala模式匹配詳解

#### 型別匹配

> 可以匹配物件的任意型別,這樣做避免了使用isInstanceOf和asInstanceOf方法

~~~~scala
/**
* @Date 2021/4/3 16:09
* @Version 10.21
* @Author DuanChaojie
*/
object MatchTypeDemo01 {
def main(args: Array[String]): Unit = {
val a = 8
// 說明 obj 例項的型別 根據 a 的值來返回
val obj = if (a == 1) 1
else if (a == 2) "2"
else if (a == 3) BigInt(3)
else if (a == 4) Map("aa" -> 1)
else if (a == 5) Map(1 -> "aa")
else if (a == 6) Array(1, 2, 3)
else if (a == 7) Array("aa", 1)
else if (a == 8) Array("aa")

/**
* 根據 obj的型別來匹配
*/
val result = obj match {
case a: Int => a
case b: Map[String, Int] => "物件是一個字串-數字的Map集合"
case c: Map[Int, String] => "物件是一個數字-字串的Map集合"
case d: Array[String] => d //"物件是一個字串陣列"
case e: Array[Int] => "物件是一個數字陣列"
case f: BigInt => Int.MaxValue
case y: Float => println("xx")
case _ => "啥也不是"
}

println(result)
}
}
~~~~

> 型別匹配注意事項:
>
> 1. Map[String, Int] 和Map[Int, String]是兩種不同的型別,其它類推。
>
> 2. 在進行型別匹配時,編譯器會`預先檢測是否有可能的匹配`,如果沒有則報錯。
>
> 3. 一個說明
>
> ~~~~scala
> // case i : Int => i 表示 將 i = obj (其它類推),然後再判斷型別
> val result = obj match {
> case i : Int => i
> }
> ~~~~
>
>
>
> 4. 如果 case _ 出現在match 中間,則表示隱藏變數名,即不使用,而不是表示預設匹配。

#### 匹配陣列

> 1. Array(0) 匹配只有一個元素且為0的陣列。
> 2. Array(x,y) 匹配陣列有兩個元素,並將兩個元素賦值為x和y。當然可以依次類推Array(x,y,z) 匹配陣列有3個元素的等等....
> 3. Array(0,_*) 匹配陣列以0開始

~~~scala
import scala.collection.mutable.ArrayBuffer

/**
* @Date 2021/4/3 16:14
* @Version 10.21
* @Author DuanChaojie
*/
object MatchArrDemo01 {
def main(args: Array[String]): Unit = {

// 這裡arrs僅僅是一個數據
val arrs = Array(
Array(0), Array(1, 0),
Array(0, 1, 0), Array(1, 1, 0),
Array(1, 1, 0, 1)
)

// 遍歷資料,arrs總共有五個元素
for (arr <- arrs) {
// 開始模式匹配
val result = arr match {
case Array(0) => "0"
case Array(x, y) => x + " = " + y
case Array(0, _*) => "以0開頭和陣列"
case _ => "什麼集合都不是"
}

/** 對結果分析:
* result = 0
* result = 1 = 0
* result = 以0開頭和陣列
* result = 什麼集合都不是
* result = 什麼集合都不是
*/
println("result = " + result)
}

println("--------------------匹配陣列練習-------------------------")
/**
* 給你一個數組集合,如果該陣列時 Array(10,20) , 請使用預設匹配,返回Array(20,10)
*/
val arrs2 = Array(
Array(0),
Array(1, 0),
Array(0, 1, 0), Array(1, 1, 0),
Array(1, 1, 0, 1)
)

for (arr <- arrs2) {
val result = arr match {
case Array(x, y) => ArrayBuffer(y, x) //Array(y,x).toBuffer //? ArrayB(y,x)
case _ => "不處理~~"
}

println("result = " + result) //ArrayBuffer(0,1)
}
}
}
~~~

#### 匹配列表

~~~~scala
/**
* @Date 2021/4/3 16:21
* @Version 10.21
* @Author DuanChaojie
*/
object MatchListDemo01 {
def main(args: Array[String]): Unit = {
val array = Array(
List(0),
List(1, 0),
List(88),
List(0, 0, 0), List(1, 0, 0)
)

for (list <- array) {

// 進行模式匹配
val result = list match {
case 0 :: Nil => "0" //
case x :: y :: Nil => x + " " + y

// "以0開頭的陣列"
case 0 :: tail => "0 ..." //
case x :: Nil => x
case _ => "something else"
}

/**
* result = 0
* result = 1 0
* result = 88
* result = 0 ...
* result = something else
*/
println("result = " + result)
}
}
}
~~~~

#### 匹配元組

~~~~scala
/**
* @Date 2021/4/3 16:27
* @Version 10.21
* @Author DuanChaojie
*/
object MatchTupleDemo01 {
def main(args: Array[String]): Unit = {
val array = Array((0, 1), (1, 0), (10, 30), (1, 1), (1, 0, 2))

/**
* 如果要匹配 (10, 30) 這樣任意兩個元素的對偶元組,應該如何寫
*/
for (pair <- array) {
// 開始模式匹配
val result = pair match {
case (0, _) => "0 ..."
case (y, 0) => y
case (x, y) => (y, x) //"匹配到(x,y)" + x + " " + y
case _ => "other"
}

/**
* result = 0 ...
* result = 1
* result = (30,10)
* result = (1,1)
* result = other
*/
println("result = " + result)
}
}
}
~~~~

#### 匹配物件

> 物件匹配,什麼才算是匹配呢?規則如下:
>
> case中物件的unapply方法(物件提取器)返回Some集合則為匹配成功,返回none集合則為匹配失敗。
>
> 快速入門案例:

~~~~scala
/**
* @Date 2021/4/3 16:32
* @Version 10.21
* @Author DuanChaojie
*/
object MatchObjectDemo01 {
def main(args: Array[String]): Unit = {
val number: Double = Square(6.0)
println(number)
number match {
/**
* 說明 case Square(arg) 的執行的機制
* 1. 當匹配到 case Square(arg)
* 2. 呼叫Square 的 unapply(arg: Double),arg的值就是 number
* 3. 如果物件提取器 unapply(arg: Double) 返回的是Some(6.0) ,
* 則表示匹配成功,同時將6.0 賦給 Square(arg) 的 arg
* 4. 果物件提取器 unapply(arg: Double) 返回的是None ,則表示匹配不成功
*/
case Square(arg) => println(s"匹配成功arg = $arg")
case _ => println("nothing matched")
}
}
}

object Square {
/**
* unapply方法是物件提取器
*
* @param arg 接收的Double型別的引數
* @return 返回型別是Option[Double],
* 返回值是Some(math.sqrt(arg)),返回arg的開平方的值並放入到Some()中
*/
def unapply(arg: Double): Option[Double] = {
Some(math.sqrt(arg))
}

def apply(arg: Double): Double = arg * arg
}

~~~~

> 應用案例二:

~~~scala
/**
* @Date 2021/4/3 16:48
* @Version 10.21
* @Author DuanChaojie
*/
object MatchObjectDemo2 {
def main(args: Array[String]): Unit = {

val namesString = "Alice,Bob,Thomas"
// 開始模式匹配
namesString match {
/**
* 當執行case Names(first, second, third)
* 1. 會呼叫 unapplySeq(str),把 "Alice,Bob,Thomas" 傳入給 str
* 2. 如果 返回的是 Some("Alice","Bob","Thomas"),分別給 (first, second, third)
* 注意,這裡的返回的值的個數需要和 (first, second, third)要一樣
* 3. 如果返回的None ,表示匹配失敗
*/
case Names(first, second, third) => {
// the string contains three people's names
// Alice Bob Thomas
println("the string contains three people's names")
// 列印字串
println(s"$first $second $third")
}
case _ => println("nothing matched")
}
}
}

object Names {
//當構造器是多個引數時,就會觸發這個物件提取器
def unapplySeq(str: String): Option[Seq[String]] = {
if (str.contains(",")) {
Some(str.split(","))
} else {
None
}
}
}
~~~

> 1. 當case 後面的物件提取器方法的引數為多個,則會預設呼叫`def unapplySeq()` 方法
> 2. 如果`unapplySeq`返回是Some,獲取其中的值,判斷得到的sequence中的元素的個數是否是三個,如果是三個,則把三個元素分別取出,賦值給first,second和third
> 3. 其它的規則不變。

### 3. 樣例類

> 1. 樣例類仍然是類
> 2. 樣例類用case關鍵字進行宣告。
> 3. `樣例類是為模式匹配而優化的類`。
> 4. 構造器中的每一個引數都成為val——除非它被顯式地宣告為var(不建議這樣做)
> 5. 在樣例類對應的伴生物件中提供apply方法讓你不用new關鍵字就能構造出相應的物件提供unapply方法讓模式匹配可以工作
> 6. 將自動生成toString、equals、hashCode和copy方法(有點類似模板類,直接給生成,供程式設計師使用)
> 7. 除上述外,樣例類和其他類完全一樣。你可以新增方法和欄位,擴充套件它們。

#### 樣例類快速入門

~~~~scala
package com.atguigu.chapter12.caseclass

/**
* @Date 2021/4/3 17:23
* @Version 10.21
* @Author DuanChaojie
*/
object CaseClassDemo01 {
def main(args: Array[String]): Unit = {
println("-------------------------------")

/**
* 型別(物件)--序列化---字串:
* 1.可以儲存到檔案中
* 2.反序列化
* 3.網路傳輸
*/
}
}

abstract class Amount

/**
* 樣例類
* @param value
*/
case class Dollar(value: Double) extends Amount

/**
* 樣例類
* @param value
* @param unit
*/
case class Currency(value: Double, unit: String) extends Amount

/**
* 樣例類
*/
case object NoAmount extends Amount
~~~~

#### 樣例類最佳實踐

> 樣例類最佳實踐一:當我們有一個型別為Amount的物件時,可以用模式匹配來匹配他的型別,並將屬性值繫結到變數(即:把樣例類物件的屬性值提取到某個變數,該功能有用)

~~~~scala
package com.atguigu.chapter12.caseclass

/**
* @Date 2021/4/3 18:15
* @Version 10.21
* @Author DuanChaojie
*/
object CaseClassDemo02 {
def main(args: Array[String]): Unit = {
val array = Array(Dollar2(1000.0), Currency2(1000.0, "RMB"), NoAmount2)
for (amt <- array) {
val result = amt match {
case Dollar2(v) => "$" + v
case Currency2(v, u) => v + " " + u
case NoAmount2 => ""
}
println("amt = " + amt)
println("result =" + result)
}
}
}

abstract class Amount2

/**
* 樣例類
*
* @param value
*/
case class Dollar2(value: Double) extends Amount2

/**
* 樣例類
*
* @param value
* @param unit
*/
case class Currency2(value: Double, unit: String) extends Amount2

/**
* 樣例類
*/
case object NoAmount2 extends Amount2
~~~~

> 樣例類最佳實踐二:
>
> 1. 樣例類的copy方法和帶名引數
> 2. copy建立一個與現有物件值相同的新物件,並可以通過帶名引數來修改某些屬性。

~~~~scala
/**
* @Date 2021/4/3 18:30
* @Version 10.21
* @Author DuanChaojie
*/
object CaseClassDemo03 {
def main(args: Array[String]): Unit = {
val amt1 = new Currency3(3000.0, "RMB")
// 克隆,建立的物件和amt的屬性一樣
val amt2 = amt1.copy()

// amt2.value = Currency3(3000.0,RMB).value amt2.unit = Currency3(3000.0,RMB).unit
println(s"amt2.value = $amt2.value \t amt2.unit = $amt2.unit")

val amt3 = amt1.copy(value = 8000.0)
// amt3 = Currency3(8000.0,RMB)
println("amt3 = " + amt3)

val amt4 = amt1.copy(unit = "美元")
// amt4 = Currency3(3000.0,美元)
println("amt4 = " + amt4)
}
}

abstract class Amount3

/**
* 樣例類
*
* @param value
*/
case class Dollar3(value: Double) extends Amount3

/**
* 樣例類
*
* @param value
* @param unit
*/
case class Currency3(value: Double, unit: String) extends Amount3

/**
* 樣例類
*/
case object NoAmount3 extends Amount3
~~~~

#### case語句的中置(綴)表示式

> 什麼是中置表示式?1 + 2,這就是一箇中置表示式。如果unapply方法產出一個元組,你可以在case語句中使用中置表示法。比如可以匹配一個List序列

~~~~scala
/**
* @Date 2021/4/3 18:38
* @Version 10.21
* @Author DuanChaojie
*/
object MidCase {
def main(args: Array[String]): Unit = {
List(1, 3, 5, 9) match {
/**
* 修改並測試
* 1.兩個元素間::叫中置表示式,至少first,second兩個匹配才行.
* 2.first 匹配第一個 second 匹配第二個, rest 匹配剩餘部分(5,9)
*/
case first :: second :: rest => println(first + " " + second + "\nrest.length = "+ rest.length + "\n" + rest) //
case _ => println("匹配不到...")
}
}
}
~~~~

#### 匹配巢狀結構

> 操作原理類似於正則表示式
>
> 最佳實踐案例——商品捆綁打折出售
>
> 1. 現在有一些商品,請使用Scala設計相關的樣例類,完成商品捆綁打折出售。
> 2. 要求商品捆綁可以是單個商品,也可以是多個商品。
> 3. 打折時按照折扣x元進行設計
> 4. 能夠統計出所有捆綁商品打折後的最終價格。
>
> 樣例類的設計:

~~~~scala
/**
* 設計樣例類
*/
abstract sealed class Item

case class Book(description: String, price: Double) extends Item

case class Food(description: String, price: Double) extends Item

/**
* Bundle 捆 , discount: Double 折扣 , item: Item*
*
* @param description 描述
* @param discount 折扣的價格
* @param item
*/
case class Bundle(description: String, discount: Double, item: Item*) extends Item
~~~~

> 完成這個案例需要有以下知識儲備:
>
> 1. 如果我們進行物件匹配時,不想接受某些值,則使用_ 忽略即可,_* 表示所有
> 2. 通過@表示法將巢狀的值繫結到變數。_*繫結剩餘Item到rest
> 3. 不使用_*繫結剩餘Item到rest

~~~~scala
/**
* @Date 2021/4/3 18:43
* @Version 10.21
* @Author DuanChaojie
*/
object CaseClassExercise {
def main(args: Array[String]): Unit = {
/**
* 這裡給出了一個具體的打折的案例
*/
val sale = Bundle("書籍", 10, Book("漫畫", 40),
Bundle("文學作品", 20, Book("《陽關》", 80), Book("《圍城》", 30), Book("天龍八部", 100)))

/** 完成上面例子之前需要學習三個知識點:
* 知識點1 :使用case語句,得到 "漫畫"
*/
val res1 = sale match {
/**
* 如果我們進行物件匹配時,不想接受某些值,則使用_ 忽略即可,_* 表示所有
*/
case Bundle(_, _, Book(desc, _), _*) => desc
}
// res1 = 漫畫
println("res1 = " + res1)

/**
* 知識點2-通過@表示法將巢狀的值繫結到變數。_*繫結剩餘Item到rest
*/
val res2 = sale match {
//如果我們進行物件匹配時,不想接受某些值,則使用_ 忽略即可,_* 表示所有
case Bundle(_, _, art@Book(_, _), rest@_*) => (art, rest)
}
// res2 = (Book(漫畫,40.0),WrappedArray(Bundle(文學作品,20.0,WrappedArray(Book(《陽關》,80.0), Book(《圍城》,30.0), Book(天龍八部,100.0)))))
println("res2 = " + res2)


/**
* 知識點3-不使用_*繫結剩餘Item到rest
*/
val res3 = sale match {
// 如果我們進行物件匹配時,不想接受某些值,則使用_ 忽略即可,_* 表示所有
case Bundle(_, _, art3@Book(_, _), rest3) => (art3, rest3)
}
// res3 = (Book(漫畫,40.0),Bundle(文學作品,20.0,WrappedArray(Book(《陽關》,80.0), Book(《圍城》,30.0), Book(天龍八部,100.0))))
println("res3 = " + res3)
}
}
~~~~

> 完成上面的案例:

~~~scala
/**
* @Date 2021/4/3 18:43
* @Version 10.21
* @Author DuanChaojie
*/
object CaseClassExercise {
def main(args: Array[String]): Unit = {
/**
* 這裡給出了一個具體的打折的案例
*/
val sale = Bundle("書籍", 10, Book("漫畫", 40),
Bundle("文學作品", 20, Book("《陽關》", 80), Book("《圍城》", 30), Book("天龍八部", 100)))

/**
* 完成案例
* price(sale) = 220.0
*/
println("price(sale) = " + price(sale))

}

def price(it: Item): Double = {
it match {
case Book(_, p) => p
case Bundle(_, disc, its@_*) => its.map(price).sum - disc
}
}
}
~~~

### 4. 密閉類

> 1. 如果想讓case類的所有子類都必須在申明該類的相同的原始檔中定義,可以將樣例類的通用超類宣告為sealed,這個超類稱之為密封類。
> 2. 密封就是不能在其他檔案中定義子類。
> 3. ![image-20210403200246976](assets/image-20210403200246976.png)

## ☆