Scala编程——第11章:模式匹配和样例类(上)


本章学习Scala中十分强大的模式匹配机制,同时Scala提供了样例类,对模式匹配进行了优化。
本文主要学习模式匹配,样例类知识点击阅读: 样例类

一、模式匹配

Scala中的模式匹配类似于Java中的switch语法,但是更加强大。

模式匹配语法中,采用match关键字声明,每个分支采用case关键字进行声明。

当需要匹配时,按照从上到下顺序匹配。会从第一个case分支开始,如果匹配成功,那么执行对应的逻辑代码,如果匹配不成功,继续执行下一个分支进行判断。

如果所有case都不匹配,那么会执行case _ 分支,类似于Java中default语句。语法上允许多个case _分支,但是只会匹配第一个case _分支。后面的无效。

1.基础案例

案例:匹配操作符,根据不同的操作符,给sign赋值。

var sign = ...
val ch: Char = ...

ch match{
	case '+' => sign = 1
	case '-' => sign = -1
	case _ => sign = 0
}

与if类似,match也是一个表达式,而不是语句。上述代码可以简化为:

var sign = ...
val ch: Char = ...

sign = ch match{
	case '+' => 1
	case '-' => -1
	case _  => 0 
}

细节说明:
1. match 和 case 是关键字,匹配的顺序是从上到下。如果匹配成功, 则 执行 => 后面的代码块。
2.每个case中,不用break语句,自动中断case,自动的退出match。
3.所有case都不匹配,会执行case _ 分支。如果所有case都不匹配,又没写case _ ,会抛出MatchError
4.=> 后面的代码块到下一个 case, 是一个整体,可以使用{} 扩起来
5.可以在match中使用其它类型,而不仅仅是字符

2.模式守卫

有时模式匹配不够精准,如果想要表达匹配某个范围的数据,就需要在模式匹配中增加条件守卫
比如我们想要扩展自己的示例,以匹配所有的数字:

var sign = ...
val ch: Char = ...
var digit: Int =... 
ch mathch {
	case '+' => sign = 1
	case '-' => sign = -1
	//匹配所有的数字。  Character.digit()在指定的基数返回字符ch的数值.这里返回ch的10进制数。
	case _ if(Character.isDigit(ch))) => digit = Character.digit(ch,10)  
	case _ => sign = 0   
}

细节说明
1.守卫可以任何Boolean条件表达式
2. case _ 后面有条件守卫if时, 这时的下划线 _ 不是表示默认匹配,而是match传入变量ch的简写。即等价
case ch if(Character.isDigit(ch))) => digit = Character.digit(ch,10)

二、模式种类

模式匹配有很多种类,模式的语法很容易理解,且模式与相应的表达式几乎相同,所以很容易掌握。本小节主要介绍:通配模式、常量模式、变量模式、构造方法模式、序列模式、元组模式、带类型的模式、变量绑定

1.通配模式

通配模式(_)会匹配任何对象。前面已经看到过通配模式用于缺省、捕获所有的可选路径,就像这样:

var sign = ...
val ch: Char = ...

sign = ch match{
	case '+' => 1
	case _  => 0 //通配模式
}

通配模式还可以用来忽略某个对象中你并不关心的局部。例如BinOp是个二元操作函数,可能你并不需要关心二元操作的操作元是什么,它只是检查这个表达式是否是二元操作

expr match {
	case BinOp (_,_,_) => println(expr +" is a binary operation")
	case _ => println("It’s something else") 
}

2.常量模式

常量模式仅匹配自己 。任何字面量都可以作为常量(模式)使用。同时,任何 val单例对象也可以被当作常量(模式)使用。例如, Nil 这个单例对象能且仅能匹配空列表。常量模式示例:

def describle(x: Any) = x match{
	case 5 => "five"
	case true => "truth"
	case "hello" => "Hi"
	case Nil => "the empty list"
	case _ => "something else"
}

使用describle()方法:
在这里插入图片描述

3.变量模式

变量模式匹配任何对象,这一点跟通配模式相同。不过不同于通配模式的是,Scala 将对应的变量绑定成匹配上的对象。在绑定之后,你就可以用这个变量来对 对象做进一步的处理。即 如果case关键字后面跟着一个变量名,那么match前面的表达式结果 会赋值给这个变量。


str(i) mathch {
	case '+' => sign = 1
	case '-' => sign = -1
	//变量模式   case ch  的含义是 ch = str(i)
	case ch  => digit = Character.digit(ch,10) 
}

如何区分常量和变量
Scala 采用了一个简单的词法规则来区分:一个以小写字母打头的简单名称会被当作模式变量处理。如果需要用小写的名称来作为模式常量,可以使用反引号 `` 将名称包起来。

示例:判断常量E( 2.71828 …)和 Pi(3 .14159 … ) 是否相等:

import math.{E, Pi}
E match {
    case Pi =>"strange math? Pi =" + Pi
    case _  => "OK"
} 

执行结果:
在这里插入图片描述
如果给 Pi 创建一个pi的别名,然后尝试如下代码:

import math.{E, Pi}
val pi = Pi
E match {
	//小写pi 会被当做变量模式,此时pi = E 不会等以Pi
    case pi =>"strange math? Pi =" + Pi
    case _  => "OK"
} 

执行结果:
在这里插入图片描述
从执行结果我们可以看到:小写的pi被当做了变量,所以match前面的表达式 E常量,赋值给了变量pi,从而执行了第一个case 分支,得到了错误的结果。

如果仍需要使用小写pi 当做常量模式,可以使用反引号`` :

import math.{E, Pi}
val pi = Pi
E match {
	//小写加上返单引号`` 会被当做常量
    case `pi` =>"strange math? Pi =" + Pi
    case _  => "OK"
} 

执行结果:
在这里插入图片描述

4.构造方法模式

构造方法模式是真正体现出模式匹配威力的地方。一个构造方法模式看上去像这样:
BinOp("+", e Number(0))
它由一个名称( BinOp )和一组圆括号中的模式 :"+"、e 和 Number(0) 组成。
假定这里的名称BinOp指定的是一个样例类,先检查匹配的对象是否是 以BinOp命名的样例类的实例
再检查这个对象的构造方法参数是否匹配给出的模式。示例:

//样例类  (后面单独讲解)
abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator : String, arg: Expr) extends Expr
case class BinOp(operator: String,left: Expr , right: Expr) extends Expr 

def simplifyTop(expr: Expr): Expr = expr match {
case UnOp("-", UnOp ("-", e)) => e 	//双重取负
case BinOp("+", e, Number(O)) => e 	//加0
case BinOp ("*", e, Number(1)) => e 	//乘1
case _ => expr
}

5.序列模式

序列类型也可以做匹配,比如 List或者Array使用的语法是相同的,可以在模式中给出任意数量的元素。 示例:匹配一个以0开始的三元素列表的模式

expr match{
	case List(0,_,_) => pritln("match success!")
	case _ =>    //缺省
}

如果你想匹配一个序列,但又不想给出多长,你可以用 _* 作为模式的最后一个元素
示例:匹配一个任意长度的以0开始的列表。

expr match{
	case List(0, _*) => println("match success")
	case _ =>
}

6.元组模式

元组类型可以做匹配。如(a, b, c)这样的模式能匹配任意的三元组:

def tupleMatch(expr: Any) = 
	expr match{
		case(a,b,c) => println("match" + a + b + c)
		case _ =>
	}

运行结果:
在这里插入图片描述

7.带类型的模式

可以用带类型的模式( typed pattern )来替代类型测试类型转换。示例:

def generalSize(x: Any) = x match{
	case s: String => s.length
	case m: Map[_,_] =>m.size
	case _ => -1
}

执行结果:
在这里插入图片描述
在Java中做类型测试和类型转换需要用到 isInstanceOfasInstanceOf。在Scala中,我们更倾向于使用模式匹配

需要注意两点:

  • 1.注意模式中的变量名,尽管s和x指向同一个值,x类型是Any,而s的类型是String。因此可以在对应分支中,使用s.length,但不能写x.length因为类型Any 并没有 length 的成员方法。
  • 2.第一个模式中,匹配到的值被当做String类型绑定到 s; 而第二个模式中,值被当做Map类型绑定到m。这里不需要使用asInstanceOf做类型转换。

类型擦除机制

Scala采用擦除式的泛型,这意味着在运行时并不会保留类型参数的信息。所以我们不能用类型来匹配特定的元素的类型。

示例:匹配Map[Int, Int]类型,匹配成功返回true,否则返回false

def isMapIntInt(x: Any) = x match{
	case m: Map[Int, Int] => true
	case _ => false	
}

执行结果:

在这里插入图片描述
从执行结果看出,编译器给出warning了,由于类型擦除,在编译期间并不能检查case m: Map[Int, Int] => true。所以不管是Map[Int,Int]类型 还是Map[String,String]或者Map其他类型都能匹配成功。

注意:数组Array不受类型擦除的影响。因为 Java和Scala 都对它们做了特殊处理,数组的元素类型是跟数组一起保存的,因此我们可以对它进行模式匹配

示例:匹配Array[String]类型,成功返回true,否则返回false

def isArrayString(x: Any) = x match{
	case a: Array[String] => true
	case _ => false
}

在这里插入图片描述

8.变量绑定

除了独自存在的变量模式外,我们还可以对任何其他模式添加变量。只需要变量名一个@符模式本身,就得到一个变量绑定模式。意味着这个模式将跟平常一样执行模式匹配, 如果匹配成功,就将匹配的对象赋值给这变量,就像简单的变量模式一样。

示例:查找绝对值操作被连续应用两次

expr match{
	case Unop("abs", e @ Unop("abs", _)) => e
	case _ =>
}

示例包括了一个以e为变量,UnOp( “abs”, _)为模式的变量绑定模式。如果整个匹配成功了,那么匹配了 UnOp( “abs”, _)的部分就被赋值给变量e。这个 case 的结果就是 e,这是因为 expr 的值相同,但是少了一次求绝对值的操作

三、模式应用

Scala中很多地方都允许使用模式,并不仅仅是 match 表达式。

1.变量定义中的模式

每当我们定义一个 val或var ,都可以用模式而不是简单的标识符。
示例:
在这里插入图片描述

2.作为偏函数的case 序列(了解)

用花括包起来的一系列 case(即可选分支)可以用在任何允许出现函数字面量的地方。本质上讲, case 序列就是一个函数字面量。不像普通函数那样只有一个入口和参数列表, case 序列可以有多个入口,每个入口都有自己的参数列表。每个 case 对应该函数的一个入口,而该入口的参数列表用模式来指定。每个入口的逻辑主体是 case 右边的部分。

示例:

val withDefault: Option[Int] => Int  = {
	case Some(x) => x
	case None => 0
}

该函数的函数体有两个 case 第一个 case 匹配 Some ,返回 Some 中的值。第二个case 匹配 None ,返回默认值0。

执行结果:

在这里插入图片描述
在这里插入图片描述

3.for表达中的模式

我们还可以在 for 表达式中使用模式。

示例:匹配映射Map的键值对
在这里插入图片描述

四、提取器

通过模式匹配我们可以匹配数组、列表和元组。这些匹配功能的背后是提取器机制提取器是拥有unapply或者unapplySeq方法的对象。 可以把unapply方法当做是伴生对象apply方法的反向操作。apply方法接受参数,然后将他们变成对象。而unapply方法接受一个对象,然后从中提取值——提取的值通常就是当初用来构造该对象的值。

通常而言,unapply方法常用模式匹配,但模式匹配可能会失败。因此unapply方法返回的是一个Option。它包含一个元组,每个匹配到的变量各自都有一个值与之对应。

1.unapply方法

每一个样例类都自动具备apply和unapply方法。示例:创建一个Currency 样例类
case class Currency(vaule: Double, unit: String) //货币类

你可以构建一个Currency实例(样例类自动带有applay方法,同构造参数带有val)
Currency(30.2, "RMB") //调用的是Currency.apply

你也可以从Currency对象中提取值:
case Currency(amount,"RMB") => println(amount)// 调用Currency.unapply()方法获取amount

2.unapplySeq方法

使用unapplySeq 可以提取任意长度的值的序列。返回一个Option[Seq[A]],A是被提取值的类型。

实例:Name 提取器可以产出名字所有组成部分的序列:

object Name{ //提取器是拥有unapply或者unapplySeq方法的对象。
	def unapplySeq(input String): Option[Seq[String]] = 
		if(input.trim == " ") None
		else Some(input.trim.split("\\s+")) //   \\s+ 匹配任意数量的空格
}

这样一来,你就能匹配到并取到任意数量的变量了。

name match{
	case Name(first,last) => ...
	case Name(first, middle, last) => ...
	case Name(first, "van", "der", last) => ...
}

五、Optio类型

Option的标准类型来表示可选值。这样的值可以有两种形式:Some(x) ,其中 x是实际的值;None对象,代表没有值。

Scala 集合类的某些标准操作会返回可选值。比如,Scala的Map有get方法,当传人的键有对应的值时,返回 Some(value)而当传人的键在Map中没有定义时,返回 None。我们来看下面这个例子
在这里插入图片描述
将可选值解开最常见的方式是通过模式匹配。例如:

def show(x: Option[String]) = x match{
	case Some(s) => s
	case None => "???"
}

执行结果:
在这里插入图片描述
关于Option 与Null值选择:
在这里插入图片描述

六、样例类

点击阅读:样例类

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页