您的位置:首页 > 其它

Scala之模式匹配和样例类

2016-03-21 15:08 441 查看
Scala强大的模式匹配机制,可以应用在switch语句、类型检查以及“析构”等场合。样例类对模式匹配进行了优化。

更好的switch

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

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

1
2
3
4
5
6
7
8

var
sign =
...

val ch:
Char =
...
 

ch match
{
  case
'+' =>
sign =
1

  case
'-' =>
sign =
-1
  case
_ =>
sign =
0

}

上面代码中,case _模式对应于switch语句中的default,能够捕获剩余的情况。如果没有模式能匹配,会抛出MatchError。而且不像常见的switch语句,在一种模式匹配之后,需要使用break来声明分支不会进入下一个分支。

match是表达式,不是语句,所以是有返回值的,故可将代码简化:

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

1
2
3
4
5

sign
= ch
match {

  case
'+' =>
1
  case
'-' =>
-1

  case
_ =>
0
}

match表达式中可以使用任何类型。模式总是从上往下进行匹配。

守卫

看代码就好,与if表达式的守卫相同作用:

ch match {
case '+' => sign = 1
case '-' => sign = -1
case _ if Character.isDigit(ch) => digit = Character.digit(ch, 10)
case _ => sign = 0
}

1
2
3
4
5
6

ch
match {

  case
'+' =>
sign =
1
  case
'-' =>
sign =
-1

  case
_ if
Character.isDigit(ch)
=>
digit =
Character.digit(ch,
10)
  case
_ =>
sign =
0

}

模式中的变量

如果在case关键字后跟着一个变量名,那么匹配的表达式会被赋值给那个变量。case _是这个特性的一个特殊情况,变量名是_。

"Hello, world" foreach { c => println (
c match {
case ' ' => "space"
case ch => "Char: " + ch
}
)}

1
2
3
4
5
6

"Hello, world"
foreach {
c =>
println (

    c
match {
      case
' ' =>
"space"

      case
ch =>
"Char: " +
ch
    }

)}

经过我的尝试,在如果变量名是_,那么在=>后使用_是不行的。

在模式中使用变量可能会与常量冲突。

import scala.math._
x match {
case Pi => ...
...
}

1
2
3
4
5

import
scala.math._

x match
{
  case
Pi =>
...

  ...
}

在上面的代码中,要如何判断Pi这个标志符是一个用来匹配的常量还是模式中的变量?规则是:变量比需要以小写字母开始。如果有常量是小写字母开头的,那么需要用反引号将常量名包起来:

import java.io.File._
str match {
case `pathSeparator` => ...
...
}

1
2
3
4
5

import
java.io.File._

str match
{
  case
`pathSeparator`
=>
...

  ...
}

类型模式

相比使用isInstanceOf来判断类型,使用模式匹配更好。

obj match {
case x: Int => x
case s: String => Integer.parseInt(s)
case _: BigInt => Int.MaxValue
case _ => 0
}

1
2
3
4
5
6

obj
match {

  case
x:
Int =>
x
  case
s:
String =>
Integer.parseInt(s)

  case
_:
BigInt =>
Int.MaxValue
  case
_ =>
0

}

在匹配类型时,需要使用一个变量名,否则就是使用对象本身来进行匹配了。

obj match {
case _: BigInt => Int.MaxValue // 匹配任何类型为BigInt的对象
case BigInt => -1 // 匹配类型为Class的BigInt对象
}

1
2
3
4

obj
match {

  case
_:
BigInt =>
Int.MaxValue  // 匹配任何类型为BigInt的对象
  case
BigInt =>
-1  // 匹配类型为Class的BigInt对象

}

因为匹配是发生在运行期的,而且JVM中泛型的类型信息会被擦掉,因此不能使用类型来匹配特定的Map类型(大部分集合类型也都不可以吧):

case m: Map[String, Int] => ... // 不行
case m: Map[_, _] => ... // 匹配通用的Map,OK

1
2

case
m:
Map[String,
Int]
=>
...  // 不行

case m:
Map[_,
_]
=>
...  // 匹配通用的Map,OK

但对于数组来说,类型信息是完好的,所以可以在Array上匹配。

匹配数组、列表和元组

arr match {
case Array(0) => "0" // 匹配包含0的数组
case Array(x, y) => x + " " + y // 匹配任何带有两个元素的数组,并将元素绑定到x和y
case Array(0, _*) => "0 ..." // 匹配任何以0开始的数组
case _ => "something else"
}

1
2
3
4
5
6

arr
match {

  case
Array(0)
=>
"0"  // 匹配包含0的数组
  case
Array(x,
y)
=>
x +
" " + y  // 匹配任何带有两个元素的数组,并将元素绑定到x和y

  case
Array(0,
_*)
=>
"0 ..."  // 匹配任何以0开始的数组
  case
_ =>
"something else"

}

下面的模式匹配,功能与上面的代码是一样的,不过将数组换成了列表。

lst match {
case 0 :: Nil => "0"
case x :: y :: Nil => x + " " + y
case 0 :: tail => "0 ..."
case _ => "something else"
}

1
2
3
4
5
6

lst
match {

  case
0 ::
Nil =>
"0"
  case
x ::
y ::
Nil =>
x +
" " +
y

  case
0 ::
tail =>
"0 ..."
  case
_ =>
"something else"

}

与上面两个例子差不多,模式匹配也可以使用在元组上。

注意到变量将会被绑定到这三种数据结构的不同部分上,这种操作被称为“析构”。

提取器

在上一节中,使用模式匹配来对数组、列表和元组进行了匹配,在这个过程的背后的是提取器(extractor)机制。使用unapply来提取固定数量的对象,使用unapplySeq来提取一个序列。

在前面的代码case
Array(0,
x)
=>
...中,

Array(0,
x)部分实际上是使用了伴生对象中的提取器,实际调用形式是:Array.unapplySeq(arr)。根据Doc,提取器方法接受一个Array参数,返回一个Option。

正则表达式是另一个适用提取器的场景。正则有分组时,可以用提取器来匹配分组:

val pattern = "([0-9]+) ([a-z]+)".r
"99 bottles" match {
case pattern(num, item) => ...
}

1
2
3
4

val
pattern =
"([0-9]+) ([a-z]+)".r

"99 bottles" match
{
  case
pattern(num,
item)
=>
...

}

变量声明中的模式

在变量声明中的模式对于返回对偶(更广一点也可以用在元组上吧?)的函数来说很有用。

val (x, y) = (1, 2)
val (q, r) = BigInt(10) /% 3 // 返回商和余数的对偶
val Array(first, second, _*) = arr // 将第一和第二个分别给first和second

1
2
3

val
(x,
y)
= (1,
2)

val (q,
r)
= BigInt(10)
/%
3  // 返回商和余数的对偶
val
Array(first,
second,
_*)
= arr  // 将第一和第二个分别给first和second

for表达式中的模式

这一部分的内容多在介绍for表达式时提过了,不过当时并没有意识到使用的是模式。

import scala.collection.JavaConversions.propertiesAsScalaMap

for ((k, v) <- System.getProperties()) // 这里使用了模式
println(k + " -> " + v)

for ((k, "") <- System.getProperties()) // 失败的匹配会被忽略,所以只打印出值为空的键
println(k)

1
2
3
4
5
6
7

import
scala.collection.JavaConversions.propertiesAsScalaMap

 
for
((k,
v)
<-
System.getProperties())  //
这里使用了模式

  println(k
+ " -> "
+ v)
 

for ((k,
"")
<-
System.getProperties())  //
失败的匹配会被忽略,所以只打印出值为空的键
  println(k)

样例类

样例类是种特殊的类,经过优化以用于模式匹配。

abstract class Amount
// 继承了普通类的两个样例类
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount

// 样例对象
case object Nothing extends Amount

1
2
3
4
5
6
7

abstract
class Amount

// 继承了普通类的两个样例类
case
class Dollar(value:
Double)
extends Amount

case class
Currency(value:
Double,
unit:
String)
extends Amount
 

// 样例对象
case
object Nothing
extends Amount

使用:

amt match {
case Dollar(v) => "$" + v
case Currency(_, u) => "Oh noes, I got " + u
case Nothing => "" // 样例对象没有()
}

1
2
3
4
5

amt
match {

  case
Dollar(v)
=>
"$" +
v
  case
Currency(_,
u)
=>
"Oh noes, I got "
+ u

  case
Nothing =>
""  // 样例对象没有()
}

在声明样例类时,下面的过程自动发生了:

构造器的每个参数都成为val,除非显式被声明为var,但是并不推荐这么做;
在伴生对象中提供了apply方法,所以可以不使用new关键字就可构建对象;
提供unapply方法使模式匹配可以工作;
生成toString、equals、hashCode和copy方法,除非显示给出这些方法的定义。
除了上述之外,样例类和其他类型完全一样,方法字段等。

copy方法和带名参数

样例类的copy方法创建一个与现有对象相同的新对象。可以使用带名参数来修改某些属性:

val amt = Currency(29.95, "EUR")
val price = amt.copy(values = 19.95)
val price = amt.copy(unit = "CHF")

1
2
3

val
amt =
Currency(29.95,
"EUR")

val price
= amt.copy(values
= 19.95)
val
price =
amt.copy(unit
= "CHF")

case语句中的中置表示法

如果unapply方法产出一个对偶,则可以在case语句中使用中置表示法。对于有两个参数的样例类,可以使用中置表示法。

amt match { case a Currency u => ... } // 等于case Currency(a, u)

1

amt
match {
case a
Currency u
=>
...
}  // 等于case Currency(a, u)

这个特性的本意是要匹配序列。举例,List对象要么是Nil,要么是样例类::。所以可以:

lst match { case h :: t => ... } // 等同于case ::(h, t),调用::.unapply(lst)

1

lst
match {
case h
:: t
=>
...
}  // 等同于case ::(h, t),调用::.unapply(lst)

多个中置表达式放在一起时会比普通的形式更加易读。

匹配嵌套结构

这个解释起来有点绕。

abstarct class Item
case class Article(description: String, price: Double) extends Item
case class Bundle(description: String, price: Double, items: Item*) extends Item

Bundle("Father's day special", 20.0,
Article("Scala for the Impatient", 39.95),
Bundle("Anchor Distillery Sampler", 10.0,
Article("Old Potrero Straight Rye Whisky", 79.95),
Article("Junipero Gin", 32.95)
)
)

1
2
3
4
5
6
7
8
9
10
11

abstarct
class Item

case class
Article(description:
String,
price:
Double)
extends Item
case
class Bundle(description:
String,
price:
Double,
items:
Item*)
extends Item

 
Bundle("Father's day special",
20.0,

  Article("Scala for the Impatient",
39.95),
  Bundle("Anchor Distillery Sampler",
10.0,

    Article("Old Potrero Straight Rye Whisky",
79.95),
    Article("Junipero Gin",
32.95)

  )
)

模式可以匹配到特定的嵌套:

case Bundle(_, _, Article(descr, _), _*) => ...

1

case
Bundle(_,
_,
Article(descr,
_),
_*)
=>
...

上面的代码中descr这个变量被绑定到第一个Article的description。另外还可以使用@来将值绑定到变量:

// art被绑定为第一个Article,rest是剩余的Item序列
case Bundle(_, _, art @ Article(_, _), rest @ _*) => ...

1
2

// art被绑定为第一个Article,rest是剩余的Item序列

case Bundle(_,
_,
art @
Article(_,
_),
rest @
_*)
=>
...

下面是个使用了模式匹配来递归计算Item价格的函数。

实际应用
def price(it: Item): Double = it match {
case Article(_, p) => p
case Bundle(_, disc, its @ _*) => its.map(price _).sum - disc
}

1
2
3
4

def
price(it:
Item):
Double =
it match
{

  case
Article(_,
p)
=>
p
  case
Bundle(_,
disc,
its @
_*)
=>
its.map(price
_).sum
- disc

}

密封类

当使用样例类来做模式匹配时,如果要让编译器确保已经列出所有可能的选择,可以将样例类的通用超类声明为sealed。

密封类的所有子类都必须在与该密封类相同的文件中定义。

如果某个类是密封的,那么在编译期所有的子类是可知的,因而可以检查模式语句的完整性。

让所有同一组的样例类都扩展某个密封的类或特质是个好的做法。

模拟枚举

可以使用样例类来模拟枚举类型:

sealed abstract class TrafficLightColor
case object Red extends TrafficLightColor
case object Yellow extends TrafficLightColor
case object Green extends TrafficLightColor

color match {
case Red => "stop"
case Yellow => "hurry up"
case Green => "go"
}

1
2
3
4
5
6
7
8
9
10

sealed
abstract class
TrafficLightColor

case object
Red extends
TrafficLightColor
case
object Yellow
extends TrafficLightColor

case object
Green extends
TrafficLightColor
 

color match
{
  case
Red =>
"stop"

  case
Yellow =>
"hurry up"
  case
Green =>
"go"

}

Option类型

Option类型用来表示可能存在也可能不存在的值。样例子类Some包装了某个值,而样例对象None表示没有值。Option支持泛型。

scores.get("Alice") match {
case Some(score) => println(score)
case Nome => println("No score")
}

1
2
3
4

scores.get("Alice")
match {

  case
Some(score)
=>
println(score)
  case
Nome =>
println("No score")

}

偏函数(L2)

被包在花括号内的一组case语句是一个偏函数。

偏函数是一个并非对所有输入值都有定义的函数,是PartialFunction[A, B]类的一个实例,其中A是参数类型,B是返回类型。该类有两个方法:apply方法从匹配的模式计算函数值;isDefinedAt方法在输入至少匹配其中一个模式时返回true。

val f: PartialFunction[Char, Int] = { case '+' => 1; case '-' => -1 }
f('-') // 返回-1
f.isDefinedAt('0') // false
f('0') //抛出MatchError
1
2
3
4

val
f:
PartialFunction[Char,
Int]
= {
case '+'
=>
1;
case '-'
=>
-1
}

f('-')  // 返回-1
f.isDefinedAt('0')  //
false

f('0')  //抛出MatchError

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: