天天看点

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类的子类必须在同一个源文件中,不可以跨文件继承