天天看點

ScalaNote21-模式比對

match

模式比對文法中,采用match關鍵字聲明,每個分支采用case關鍵字進行聲明,當需要比對時,會從第一個case分支開始,如果比對成功,那麼執行對應的邏輯代碼,如果比對不成功,繼續執行下一個分支進行判斷。如果所有case都不比對,那麼會執行case _ 分支。看個Demo:

val oper = '#'
val n1 = 20
val n2 = 10
var res = 0
oper match {
case '+' => res = n1 + n2
case '-' => res = n1 - n2
case '*' => res = n1 * n2
case '/' => res = n1 / n2
case _ => println("oper error")
}
println("res=" + res)      
oper error
res=0





oper: Char = #
n1: Int = 20
n2: Int = 10
res: Int = 0      
  • 如果所有case都不比對,那麼會執行​

    ​case _​

    ​分支
  • 如果所有case都不比對,又沒有寫case _ 分支,那麼會抛出MatchError
  • 每個case中,不用break語句,自動中斷case
  • => 後面的代碼塊到下一個 case, 是作為一個整體執行,可以使用{} 擴起來,也可以不擴

守衛

如果想要表達比對某個範圍的資料,就需要在模式比對中增加條件守衛! 看個Demo:

for (ch <- "+-3!") {
var sign = 0
var digit = 0
ch match {
case '+' => sign = 1
case '-' => sign = -1
// 說明..
case _ if ch.toString.equals("3") => digit = 3
case _ => sign = 2
}
println(ch + " " + sign + " " + digit)
}      
+ 1 0
- -1 0
3 0 3
! 2 0      
  • 代碼結構和match基本一緻
  • _ if這個操作暫且隻能記住了,scala中"_"意義真的多,import時還能指代引入所有函數。。

模式中的變量

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

val ch = "V"
ch match {
case "+" => println("ok")
case mychar => println("mychar = "+mychar)
// case _ => println("ok")
}      
mychar = V





ch: String = V      
  • mychar這裡的邏輯是把ch指派給mychar
  • match表達式本身可以賦給一個變量
val ch = "V"
var ch1= ch match {
case "+" => println("ok")
case mychar => mychar
// case _ => println("ok")
}      
ch: String = V
ch1: Any = V      

類型比對

可以比對對象的任意類型,這樣做避免了使用isInstanceOf和asInstanceOf方法。看個Demo:

val a = 2
val obj = a match {
    case 1=>1
    case 2 => "2"
    case 3 =>  Map("aa" -> 1)
    case _ => "Nothing"
}
val result = obj match {
    case a : Int => a
    case a : String => a+" is String"
    case a : Array[String] => a+"is Array"
    case _:Float => "is Float"
    case _ => "Nothing too"
}
println(result)      
2 is String





a: Int = 2
obj: Any = 2
result: Any = 2 is String      
  • ​case a:Int​

    ​,這裡相當于把obj指派給a
  • ​case _:Float​

    ​,這裡相當于直接驗證obj是否為Float類型

比對數組

直接把韓老師的代碼搬過來了:

for (arr <- Array(Array(0), Array(1, 0), Array(0, 1, 0),
Array(1, 1, 0), Array(1, 1, 0, 1))) {
val result = arr match {
case Array(0) => "0"
case Array(x, y) => x + "=" + y
case Array(0, _*) => "0^*"
case _ => "Nothing"
}
println("result = " + result)
}      
result = 0
result = 1=0
result = 0^*
result = Nothing
result = Nothing      
  • Array(0) 比對隻有一個元素且為0的數組
  • Array(x,y) 比對數組有兩個元素,并将兩個元素指派為x和y。當然可以依次類推Array(x,y,z) 比對數組有3個元素的等等…
  • Array(0,_*) 比對數組以0開始

比對清單

感覺有點正則的意思了,不過scala這些用法真多,不能搞點通用的方法嗎?

for (list <- Array(List(0), List(1, 0), List(0, 0, 0), List(1, 0, 0))) {
val result = list match {
case 0 :: Nil => "0" //隻有一個元素0
case x :: y :: Nil => x + " " + y //有兩個元素
case 0 :: tail1111 => "0 ..." //0開頭的元素,tail可以為任務字元串
case _ => "something else"
}
println(result)
}      
0
1 0
0 ...
something else      

比對元組

for (pair <- Array((0, 1), (1, 0), (1, 1),(1,0,2),(10,30))) {
val result = pair match { 
case (0, _) => "0 ..." //第一進制素為0,一共兩個元素
case (y, 0) => y // 第二個元素為0
case (10,30)=>(10,30)    
case _ => "other" //
}
println(result)
}      
0 ...
1
other
other
(10,30)      

對象比對

這裡看的比較蒙,涉及到前面講的單例對象。在伴生對象中,我們定義apply方法,可以不通過new直接建立一個執行個體。但是單例對象似乎也可以。下面的單例對象提供了apply方法和unapply方法。這倆似乎不專屬的函數??先看個用法吧~ }

object Square {
  //說明
  //1. unapply方法是對象提取器
  //2. 接收z:Double 類型
  //3. 傳回類型是Option[Double]
  //4. 傳回的值是 Some(math.sqrt(z)) 傳回z的開平方的值,并放入到Some(x)
def unapply(z: Double): Option[Double] = Some(math.sqrt(z))
    // apply是對象生成器??
def apply(z: Double): Double = z * z
}
println("Square(5) = " + Square(5))
println("Square.apply(5) = " + Square.apply(5))
println("Square.unapply(25) = " + Square.unapply(25))      
Square(5) = 25.0
Square.apply(5) = 25.0
Square.unapply(25) = Some(5.0)





defined object Square      

這樣看單例對象和伴生類型的apply方法一樣? 再說回對象比對:

  • case中對象的unapply方法(對象提取器)傳回Some集合則為比對成功
  • 傳回none集合則為比對失敗
val number: Double = 36.0
number match {
case Square(n) => println(n)
case _ => println("nothing matched")
}      
6.0





number: Double = 36.0      
  • 建構對象時apply會被調用 ,比如 val n1 = Square(5)
  • 當将 Square(n) 寫在 case 後時[case Square(n) => xxx],會預設調用unapply 方法(對象提取器)
  • number 會被 傳遞給def unapply(z: Double) 的 z 形參
  • 如果傳回的是Some集合,則unapply提取器傳回的結果會傳回給 n 這個形參
  • case中對象的unapply方法(提取器)傳回some集合則為比對成功
  • 傳回None集合則為比對失敗

再看另一個case

object Names {
    //unapplySeq是一個專有名詞,指定的函數
def unapplySeq(str: String): Option[Seq[String]] = {
if (str.contains(",")) Some(str.split(","))
else None
}}
val namesString = "Alice,Bob,Thomas"
//說明
namesString match {
    //first, second, third這三個參數名随便取,必須要要是三個
case Names(first, second, third) => {
println("the string contains three people's names")
// 列印字元串
println(s"$first $second $third")
}
case _ => println("nothing matched")
}      
the string contains three people's names
Alice Bob Thomas





defined object Names
namesString: String = Alice,Bob,Thomas      

當執行​

​case Names(first, second, third)​

  1. 會調用 unapplySeq(str),把 “Alice,Bob,Thomas” 傳入給 str
  2. 如果 傳回的是 Some(“Alice”,“Bob”,“Thomas”),分别給(first, second, third)
  3. 注意,這裡的傳回的值的個數需要和(first, second, third)要一樣
  4. 如果傳回的None ,表示比對失敗

變量聲明中的模式

簡略的做變量命名的方法,scala中騷操作是真的多。。。

val (x, y) = (1, 2)
val (q, r) = BigInt(10) /% 3  //說明  q = BigInt(10) / 3 r = BigInt(10) % 3
val arr = Array(1, 7, 2, 9)
val Array(first, second, _*) = arr // 提出arr的前兩個元素
println(first, second)      
(1,7)





x: Int = 1
y: Int = 2
q: scala.math.BigInt = 3
r: scala.math.BigInt = 1
arr: Array[Int] = Array(1, 7, 2, 9)
first: Int = 1
second: Int = 7      

for表達式中的模式

filter不是就能完成過濾操作了嗎?這是為了一題多解?

val map = Map("A"->1, "B"->0, "C"->3)
for ( (k, v) <- map ) {
println(k + " -> " + v)
}
// 過濾出value=0,key-value
for ((k, 0) <- map) {
println(k + " --> " + 0)
}
// 和上面的功能一樣
for ((k, v) <- map if v == 0) {
println(k + " ---> " + v)
}      
A -> 1
B -> 0
C -> 3
B --> 0
B ---> 0





map: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 0, C -> 3)      

樣例類

樣例類是幹嘛滴?他的優點和用處我暫時還沒有體會到~樣例類的基本特點如下:

  • 樣例類仍然是類
  • 樣例類用case關鍵字進行聲明
  • 樣例類是為模式比對而優化的類
  • 構造器中的每一個參數都成為val——除非它被顯式地聲明為var(不建議這樣做)
  • 在樣例類對應的伴生對象中提供apply方法讓你不用new關鍵字就能構造出相應的對象,提供unapply方法讓模式比對可以工作(預設的方法)
  • 還将自動生成toString、equals、hashCode和copy方法(有點類似模闆類,直接給生成,供程式員使用)
  • 除上述外,樣例類和其他類完全一樣。你可以添加方法和字段,擴充它們

具體用法先看個demo,慢慢體會

abstract class Amount
case class Dollar(value: Double) extends Amount 
case class Currency(value: Double, unit: String) extends Amount
case object NoAmount extends Amount 
// Dollar、Currency、NoAmount為三個樣例類  
val Arr = Array(Dollar(1000.0), Currency(1000.0, "RMB"), NoAmount)
for (amt <- Arr) {
val result = amt match {
//和前面對象比對邏輯類似
    // 如果是Dollar類,會調用unapply方法把amt指派給了v
case Dollar(v) => "$" + v
//說明
case Currency(v, u) => v + " " + u
case NoAmount => ""
}
println(amt + ": " + result)
}      
Dollar(1000.0): $1000.0
Currency(1000.0,RMB): 1000.0 RMB
NoAmount: 





defined class Amount
defined class Dollar
defined class Currency
defined object NoAmount
Arr: Array[Product with Serializable with Amount] = Array(Dollar(1000.0), Currency(1000.0,RMB), NoAmount)      

剛才有提到樣例類提供了unapply和apply等方法,也提供了copy方法,copy建立一個與現有對象值相同的新對象,并可以通過帶名參數來修改某些屬性!看個例子:

val amt = Currency(29.95, "RMB")
val amt1 = amt.copy() //建立了一個新的對象,但是屬性值一樣
val amt2 = amt.copy(value = 19.95) //建立了一個新對象,但是修改了貨币機關
val amt3 = amt.copy(unit = "Pound")//..
println(amt)
println(amt2)
println(amt3)      
Currency(29.95,RMB)
Currency(19.95,RMB)
Currency(29.95,Pound)





amt: Currency = Currency(29.95,RMB)
amt1: Currency = Currency(29.95,RMB)
amt2: Currency = Currency(19.95,RMB)
amt3: Currency = Currency(29.95,Pound)      

中置(綴)表達式

還是沒有體會到這個用法的用處。。。直接看例子,後面用到再細究吧

List(1, 3, 5, 9) match { //修改并測試
//1.兩個元素間::叫中置表達式,至少first,second兩個比對才行.
//2.first 比對第一個 second 比對第二個, rest 比對剩餘部分(5,9)
case first :: second :: rest => println(first + second + rest.length) //
case _ => println("Nothing Match...")
}      
6      

比對嵌套結構

這個題目蠻有意思的,綜合使用了直接介紹的樣例類、對象比對、遞歸等方法。先看題幹:

現在有一些商品,請使用Scala設計相關的樣例類,完成商品捆綁打折出售。要求

  • 商品捆綁可以是單個商品,也可以是多個商品
  • 打折時按照折扣x元進行設計
  • 能夠統計出所有捆綁商品打折後的最終價格
// 定義抽象類,可以直覺的了解成他是商品粒度的,也是最細粒度的
abstract class Item 
// 定義樣例類Book,其繼承Item類。可以直覺了解為書本類别商品,參數為描述和價格
case class Book(description: String, price: Double) extends Item
//Bundle 捆 , discount: Double 折扣 , item: Item* , 注意這裡的Item是可變參數,即可以傳入多個參數,并且可以是Item的子類
// 從邏輯上講,discount指這些Item的總價在減去折扣
case class Bundle(description: String, discount: Double, item: Item*) extends Item      
defined class Item
defined class Book
defined class Bundle      

下面定義一個Bundle,作為測試資料

  • sale變量為一個Bundle的執行個體
  • 參數description:book_info,discount:10,item有兩個參數,一個是Book(“book01”, 40),一個是Bundle
  • 那麼最終最終價格為(40-10)+(80+30-20)
val sale = Bundle("book_info", 10,  Book("book01", 40), Bundle("literature", 20, Book("book02", 80), Book("book03", 30)))      
sale: Bundle = Bundle(book_info,10.0,WrappedArray(Book(book01,40.0), Bundle(literature,20.0,WrappedArray(Book(book02,80.0), Book(book03,30.0)))))      

那麼我們的處理邏輯就是去除Item裡的價格和折扣的價格

  • 先取出折扣價格10,再取出book01的價格40,計算折扣之後的而價格
  • 對于第二個Item,是Bundle,這時候可以采取遞歸的邏輯,取出折扣價格和對應商品的原價

先看怎麼取出對應的元素

  • 下面的discount,description,price隻是個代稱,名字随便取
  • 不想接受某些值,則使用_ 忽略即可
  • _* 表示所有
sale match{
    case Bundle(_,discount,Book(description,price),_*)=>(discount,price)
}      
res21: (Double, Double) = (10.0,40.0)      

如果取出整個變量咋取?

  • 和上面取出元素邏輯一緻,可以直接指代,比如​

    ​case Bundle(_,_,book01,_*)=>book01​

    ​​,這樣就可以把​

    ​Book("book01", 40)​

    ​取出來
  • 那如果想把​

    ​_*​

    ​​取出來給一個變量,咋整?此時可用@符号,即​

    ​rest @ _*​

看個demo

val result2 = sale match {
    // art@_也可以
case Bundle(_, _, art@Book(_, _), rest@_*) => (art, rest)
}
println(result2)
println("art =" + result2._1)
println("rest=" + result2._2)      
(Book(book01,40.0),WrappedArray(Bundle(literature,20.0,WrappedArray(Book(book02,80.0), Book(book03,30.0)))))
art =Book(book01,40.0)
rest=WrappedArray(Bundle(literature,20.0,WrappedArray(Book(book02,80.0), Book(book03,30.0))))





result2: (Book, Seq[Item]) = (Book(book01,40.0),WrappedArray(Bundle(literature,20.0,WrappedArray(Book(book02,80.0), Book(book03,30.0)))))      

至此,我們對已知結構的資料可以完成資料取出,計算的操作了,那麼怎麼使之通用化呢?考慮遞歸的邏輯

  • 用對象比對的方法,取出對應商品的原價
  • 如果Item是Bundle,遞歸調用,取出裡面的價格和折扣資訊

直接看代碼:

def price(it: Item): Unit = {
  it match {
    //如果是Book對象,則取出價格
    case Book(index, p) => println(index+": "+p)
    //1、如果是Bundle,會取出所有的item即可變參數Item*
    //2、對所有的item進行周遊,周遊采用map方法  
    //3、map中遞歸調用price函數,目前來看就是提取book對象的價格元素
    case Bundle(_, disc, its @ _*) => its.map(price)
  }
}
price(sale)      
book01: 40.0
book02: 80.0
book03: 30.0





price: (it: Item)Unit      
  • 把原始價格都加起來
  • 把折扣價格都加起來
  • 做加法即可
val sale = Bundle("book_info", 10,  Book("book01", 40), Bundle("literature", 20, Book("book02", 80), Book("book03", 30)))      
def price(it: Item): Double = {
  it match {
    //如果是Book對象,則取出價格
    case Book(_, p) => {println(p);p}
    //1、如果是Bundle,會取出所有的item即可變參數Item*
    //2、對所有的item進行周遊,周遊采用map方法  
    //3、map中遞歸調用price函數  
    case Bundle(_, disc, its @ _*) => its.map(price).sum - disc
  }
}
// --- 遞歸之後的計算邏輯
// 1)傳入sale之後,走第二個case分支 
// 2)取出disc和Item*
// 3)對Item*進行周遊  
  //3.1 先取出book01的價格,此時價格應該傳回到map之後的集合A中
  //3.2 再調用price函數對第二個Item處理,此時也是先取出折扣價,再一次取出book02、book03的原價,放在map之後的集合B中
  //3.3 對第二個Item傳回的集合B求和,減去折扣價,這時候相當于計算好折後價,傳回給集合A  
  //3.4 最終集合A求和,這個時候第一個元素是原價,再減去折扣價就行
// --- 加入println邏輯之後
  // 1、最裡面的Bundle,取出book02、book03的價格,求和-折扣,即80+30-20=90
  // 2、之後90傳給集合A,計算90+40-10
price(sale)      
40.0
80.0
30.0





price: (it: Item)Double
res41: Double = 120.0      

密封類

  • 如果想讓case類的所有子類都必須在申明該類的相同的源檔案中定義,可以将樣例類的通用超類聲明為sealed,這個超類稱之為密封類
  • 密封就是不能在其他檔案中定義子類
  • abstract sealed class cat此時cat類的子類必須在同一個源檔案中,不可以跨檔案繼承