scala学习笔记 - 模式匹配和样例类(二)

25 篇文章 0 订阅
15 篇文章 2 订阅

样例类

样例类是一种特殊的类,它们经过优化以被用于模式匹配;在本例中,有两个扩展自常规(非样例)类的样例类:

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

你也可以有针对单例的样例对象:

case object Nothing extends Amount

当我们有一个类型为Amount的对象时,就可以用模式匹配来匹配到它的类型 ,并将属性值绑定到变量:

amt match {
  case Dollar(v) => s"$$$v"
  case Currency(_, u) => s"Oh noes, I got $u"
  case Nothing => ""
}

说明:样例类的实例使用(),样例对象不使用圆括号。
当声明样例类时,有如下几件事会自动发生:

  • 构造器中的每一个参数都成为val,除非它被显式地声明为var(不建议这样做)。
  • 在伴生对象中提供apply方法让你不用new关键字就能构造出相应的对象,比如,Dollar(29.95)或Currency(29.95, “EUR”)。
  • 提供unapply方法让模式匹配可以工作(如果只是使用样例类来做模式匹配,则你并不真正需要掌握这些细节)。
  • 将生成toString、equals、hashCode和copy方法,除非显式地给出这些方法的定义。
    除上述几点外,样例类和其他类完全一样,你可以添加方法和字段,扩展它们,等等。

copy方法和带名参数

样例类的copy方法创建一个与现有对象值相同的新对象,例如:

val amt = Currency(29.95, "EUR")
val price = amt.copy()

这个方法本身并不是很有用,毕竟,Currency对象是不可变的,我们完全可以共享这个对象引用;不过,你可以用带名参数来修改某些属性:

val price = amt.copy(value = 19.95) // Currency(19.95, "EUR")
// 或者
val price = amt.copy(unit = "CHF") // Currency(29.95, "CHF")

case语句中的中置表示法

如果unapply方法交出的是一个对偶,则你可以在case语句中使用中置表示法。其实,对于有两个参数的样例类,你可以使用中置表示法来表示它,例如:

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

这个特性的本意是要匹配序列。举例来说,每个List 象要么是Nil,要么是样例类 ::,定义如下:

case class ::[E](head: E, tail: List[E]) extends List[E]
// 因此,你可以写成这样:
lst match {case h::t => ...} // 等同于 ::(h, t),将调用::.unapply(lst)

多个中置表达式放在一起的时候,它们会更易读。例如:

result match {case p ~ q ~ r => ...} // 这样的写法要好过 ~(~(p, q), r)

如果操作符以冒号结尾,则它是从右向左结合的。例如:

case first::second::rest
// 上面代码的意思是:
case ::(first, ::(second, rest))

说明:中置表示法可用于任何返回对偶的unapply方法。以下是一个示例:

case object +: {
  def unapply[T](input: List[T]){
    if(input.isEmpty) None else Some((input.head, input.tail))
  }
}

这样一来你就可以用+:,来析构列表了:

1 +: 7 +: 2 +: 9 +: Nil match {
  case first +: second +: rest => first + second + rest.length
}

匹配嵌套结构

样例类经常被用于嵌套结构。例如,某个商店售卖的物品,有时,我们会将物品捆绑在一起打折出售:

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

因为不用使用new,所以我们可以很容易地给出嵌套对象定义:

Bundle("Father's day special", 20.0,
  Article("Scala for the Impatient", 39. 95),
  Bundle("An chor Disti llery Sampler", 10.0, 
    Article("Old Potrero Straight Rye Whiskey", 79.95), 
    Article("Junipero Gin", 32.95)))

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

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

上述代码将descr绑定到Bundle第一个Article的描述。你也可以用@表示法将嵌套的值绑定到变量:

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

这样一来,art就是Bundle中的第一个Article,而rest则是剩余Item的序列。
注意,在本例中,_*是必需的。以下模式:

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

将只能匹配到那种只有Article再加上不多不少正好一个Item的Bundle ,而这个Item将被被绑定rest变量。
作为该特性一个实际应用,以下是一个计算某Item价格的函数:

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

注意:对于那些扩展其他样例类的样例类而言,toString、equals、hashCode和copy方法不会被生成。如果你有一个样例类继承自其他样例类,你将得到一个编译器警告。Scala的未来版本可能会完全禁止这样的继承关系。如果你需要多层次的继承来将样例类的通用行为抽象到样例类外部的话 ,请只把继承树的叶子部分做成样例类。

密封类

当你用样例类来做模式匹配时,可能想让编译器帮你确保自己已经列出了所有可能的选择,要达到这个目的,你需要将样例类的通用超类声明为sealed:

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

密封类的所有子类都必须在与该密封类相同的文件中定义,举例来说,如果有人想要为欧元添加另一个样例类:

case class Euro(value : Double) extends Amount

他们必须在Amount被声明的那个文件中完成。
如果某个类是密封的,那么在编译期所有子类就是可知的,因而编译器可以检查模式语句的完整性,让所有(同一组)样例类都扩展某个密封的类或特质是一个好的做法。

模拟枚举

样例类让你可以在Scala中模拟出枚举类型:

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"
}

注意超类被声明为sealed ,让编译器可以帮我们检查match语句是否完整。如果你觉得这样的实现方式有些过重,也可以使用之前介绍过的Enumeration助手类。

Option类型

标准类库中的Option类型用样例类来表示那种可能存在也可能不存在的值。样例子类Some包装了某个值,例如:Some(“Fred”),而样例对象None表示没有值。这比使用空字符串的意图更加清晰,比使用 null来表示缺少某值的做法更加安全。
Option支持泛型,举例来说,Some(“Fred”)的类型为Option[String]。
Map类的get方法返回一个Option。如果对于给定的键没有对应的值,则get返回None,如果有值,就会将该值包在Some中返回。你可以用模式匹配来分析这样一个值。

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

不过老实说,这很烦琐。或者你也可以使用isEmpty和get:

if(alicesScore.isEmpty) println("No scores")
else println(alicesScore.get)

这也很烦琐。用getOrElse方法会更好:

println(alicesScore.getOrElse("No score"))

如果alicesScore为None,getOrElse将返回"No score"。
处理可选值(option)更强力的方式是将它们当作拥有0或1个元素的集合,你可以用for循环来访问这个元素:

for(score <- alicesScore) println(score)

如果alicesSorce是None,则什么都不会发生。如果它是一个Some,那么循环将被执行一次,而score会被绑上可选值的内容。
你也可以用诸如map、filter或foreach方法。例如:

val biggerScore = alicesScore.map(_ + 1) // Some(score + 1) 或 None
val acceptableScore = alicesScore.filter(_ > 5) 
// 如采score > 5,如Some(score);否则,得到None
alicesScore.foreach(println _) // 如采存在,则打印出 score 的值

提示:在从一个可能为null的值创建Option时,你可以简单地使用Option(value)。如果value为null结果就是None;其余情况将得到Some(value)。

偏函数

被包在花括号内的case语句是一个偏函数(partial function),一个并非对所有输入值都有定义的函数。它是PartialFunction[A, B]类的一个实例(A是参数类型,B是返回类型)。该类有两个方法:apply方法从匹配到的模式计算函数值,而isDefinedAt方法在输入至少匹配其中一个模式时返回true。例如:

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

有一些方法接受PartialFunction作为参数。举例来说,GenTraversable特质的collect方法将一个偏函数应用到所有在该偏函数有定义的元素,并返回包含这些结果的序列。

"-3+4".collect{ case '+' => 1; case '-' => -1 } // Vector(-1, 1)

偏函数表达式必须是在一个编译器能够推断出返回类型的上下文里。当你将它赋值给一个有类型的变量或作为参数传递时,就属于这样的情形。
说明:完全覆盖了所有场景的样例子句组成的集定义的是Function1,而不仅仅是一个PartialFunction,只要预期这样一个函数,你都可以将它传入。

"-3+4".map{ case '+' => 1; case '-' => -1; case _ => 0} // Vector(-1, 0, 1, 0)

Seq[A ]是一个PartialFunction[Int, A],而Map[K, V]是一个PartialFunction[K, V]。例如,你可以将映射传入collect:

val names = Array("Alice", "Bob", "Carmen") 
val scores = Map("Alice" -> 10, "Carmen" -> 7) 
names.collect(scores) // 将交出Array(10, 7)
// lift方法将PartialFunction[T, R]变成一个返回类型为Option[R]的常规函数
val f: PartialFunction[Char, Int] = { case '+' => 1; case '-' => -1 }
val g = f.lift // 一个类型为Char => Option[Int]的函数

这样一来,g('-')得到Some(-1),而g('*')得到None。
Regex.replaceSomeIn方法要求一个String => Option[String]的函数用来做替换,如果你有一个映射(或某个其他的PartialFunction),则可以用lift来产出这样的函数:

val varPattern = """\{([0-9]+)\}""".r 
val message = "At {1}, there was {2} on {0}" 
val vars =Map("{0}" -> "planet 7", "{1}" -> "12:30 pm",  "{2}" -> "a disturbance of the force.") 
val result= varPattern.replaceSomeIn(message, m => vars.lift(m.matched))

反过来,你也可以调用Function.unlift将返回Option[R]的函数变成一个偏函数。
说明:try语句的catch子句是一个偏函数,你甚至可以使用一个持有函数的变量:

def tryCatch[T](b: => T, catcher: PartialFunction[Throwable, T]) = try { b } catch catcher 
// 然后,你就可以像如下这样提供一个定制的catch子句
val result= tryCatch(str.toint, { case _: NumberFormatException => -1 })

参考:快学scala(第二版)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值