样例类会在编译时自动添加好些东西
另外一个为函数式编程提供支持的就是样例类。一个样例类具有一个普通类所有的功能,并且还有很多特别的,当编译器看到case关键字,会进行很多处理:
- 默认情况下,样例类的构造器参数是 public val 字段,会对每个参数生成访问方法。
- 在这个类的伴生对象中会创建 apply 方法。
- 在这个类的伴生对象中会创建 unapply 方法。
- 在类中生成 copy 方法。
- 生成 equals 和 hashCode 方法。
- 生成 toString 方法
apply方法和不可变参数
scala> case class Person(name: String, relation: String)
defined class Person
// "new" not needed before Person
scala> val christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)
scala> christina.name
res0: String = Christina
// can't mutate the `name` field
scala> christina.name = "Fred"
<console>:10: error: reassignment to val
christina.name = "Fred"
^
unapply方法:
trait Person {
def name: String
}
case class Student(name: String, year: Int) extends Person
case class Teacher(name: String, specialty: String) extends Person
def getPrintableString(p: Person): String = p match {
case Student(name, year) =>
s"$name is a student in Year $year."
case Teacher(name, whatTheyTeach) =>
s"$name teaches $whatTheyTeach."
}
Scala标准的 unapply 方法返回 Option包装起来的 样例类构造字段的 tuple形式。
copy 方法
克隆一个对象,或者 在克隆的过程中更改字段 ,copy是很有用的。
scala> case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
defined class BaseballTeam
scala> val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
cubs1908: BaseballTeam = BaseballTeam(Chicago Cubs,1908)
scala> val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
因为在函数式编程中你不会改变数据,这就是从已经存在的示例中创建一个新的很好的方法。
equals 和 hashCode
scala> case class Person(name: String, relation: String)
defined class Person
scala> val christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)
scala> val hannah = Person("Hannah", "niece")
hannah: Person = Person(Hannah,niece)
scala> christina == hannah
res1: Boolean = false
有助于你进行对象间的比较,还有存放对象在 Set 和 Map。
样例类的模式匹配对进行函数式编程帮助很大。
样例对象
object
在进入 case object 之前,我们先看一下Scala中的普通 object。
当你想创建一个单例对象时可以使用 object 。一个类的单个实例不相关的方法和值,属于单例对象;用object取代class来实现单例对象。
一个常见的例子是当你创建工具对象时:
object PizzaUtils {
def addTopping(p: Pizza, t: Topping): Pizza = ...
def removeTopping(p: Pizza, t: Topping): Pizza = ...
def removeAllToppings(p: Pizza): Pizza = ...
}
object FileUtils {
def readTextFileAsString(filename: String): Try[String] = ...
def copyFile(srcFile: File, destFile: File): Try[Boolean] = ...
def readFileToByteArray(file: File): Try[Array[Byte]] = ...
def readFileToString(file: File): Try[String] = ...
def readFileToString(file: File, encoding: String): Try[String] = ...
def readLines(file: File, encoding: String): Try[List[String]] = ...
}
case object
一个 case object 很像 object ,但还有更多的特性:
- 它是可序列化的
- 它有一个默认的 hashCode 实现
- 它有一个改进的 toString 方法
由于这些特点,case object 常用于下面两个方面: - 当创建枚举时
- 当创建消息的“容器”,用于在与别的对象之前传递时
用case object 创建枚举
sealed trait Topping
case object Cheese extends Topping
case object Pepperoni extends Topping
case object Sausage extends Topping
case object Mushrooms extends Topping
case object Onions extends Topping
sealed trait CrustSize
case object SmallCrustSize extends CrustSize
case object MediumCrustSize extends CrustSize
case object LargeCrustSize extends CrustSize
sealed trait CrustType
case object RegularCrustType extends CrustType
case object ThinCrustType extends CrustType
case object ThickCrustType extends CrustType
case class Pizza (
crustSize: CrustSize,
crustType: CrustType,
toppings: Seq[Topping]
)
用 case object 作为消息
加入你要创建一个亚马逊的 Alexa 应用,你想要传递说话消息,像 “speak the enclosed text,” “stop speaking,”, “pause,” 和“resume.” ,你可以这样写:
case class StartSpeakingMessage(textToSpeak: String)
case object StopSpeakingMessage
case object PauseSpeakingMessage
case object ResumeSpeakingMessage
StartSpeakingMessage 是 case class 。因为 case object 不能有构造器参数。
如果有用到Akka库,可以这样写:
class Speak extends Actor {
def receive = {
case StartSpeakingMessage(textToSpeak) =>
// code to speak the text
case StopSpeakingMessage =>
// code to stop speaking
case PauseSpeakingMessage =>
// code to pause speaking
case ResumeSpeakingMessage =>
// code to resume speaking
}
}
这是一个很好的安全的在Scala程序之间传递消息的方式。
scala中的函数错误处理
因为函数式编程像代数,没有空值和异常。但是你确实还会遇到异常,当你访问关闭的服务器或丢失的文件时。那你能做什么?
Option/Some/None
前面章节有说过:
def toInt(s: String): Option[Int] = {
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
}
toInt(x) match {
case Some(i) => println(i)
case None => println("That didn't work.")
}
val y = for {
a <- toInt(stringA)
b <- toInt(stringB)
c <- toInt(stringC)
} yield a + b + c
Try/Success/Failure
Try/Success/Failure 使用起来与 Option/Some/None 很像,但还有两个很好的特性:
- Try使得捕获异常很简单
- Failure 包含异常信息
使用前先导入:
import scala.util.{Try,Success,Failure}
更短了些:
def toInt(s: String): Try[Int] = Try {
Integer.parseInt(s.trim)
}
def toInt(s: String): Try[Int] = Try(Integer.parseInt(s.trim))
scala> val a = toInt("1")
a: scala.util.Try[Int] = Success(1)
scala> val b = toInt("boo")
b: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "boo")
上面的两个都可以用来处理错误异常,一般,用Try/Success/Failure处理会抛出异常的代码,用Option/Some/None处理避免空值的问题。
Future
当你想用scala写并行和并发程序时,你可以使用原生的Java Thread ,但是用Scala 的Future来进行并发和并行程序的编写会更简单。
一个Future代表一个值,或许会或许不会 马上获取,但是会在某个时间点能够获取值,如果不能获取值会获取异常。
在单线程编程中,你把函数调用的结果绑定给一个变量:
def aShortRunningTask(): Int = 42
val x = aShortRunningTask
当使用Future时呢:
def aLongRunningTask(): Future[Int] = ???
val x = aLongRunningTask
aLongRunningTask 会消耗不确定的时间来返回值。
Future 时一次性的。“在其他线程上进行一个相对较慢的计算,等结果出来了再告诉我。”
一个Future的示例
当你有一个较慢的算法要计算时,这时像把他从主线程中拿掉,放到别处慢慢运行,让主线程能继续运行。
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
scala> val a = Future { Thread.sleep(10*1000); 42 }
a: scala.concurrent.Future[Int] = Future(<not completed>)
scala> val b = a.map(_ * 2)
b: scala.concurrent.Future[Int] = Future(<not completed>)
scala> b
res1: scala.concurrent.Future[Int] = Future(Success(84))
Future的值类型为Success或Failure:
a.onComplete {
case Success(value) => println(s"Got the callback, value = $value")
case Failure(e) => e.printStackTrace
}
多个Future配合
想象你要去服务器访问股票价格,这要消耗些时间,还可能访问不到:
def getStockPrice(stockSymbol: String): Future[Double] = ???
以随机值代表访问消耗时间:
def getStockPrice(stockSymbol: String): Future[Double] = Future {
val r = scala.util.Random
val randomSleepTime = r.nextInt(3000)
val randomPrice = r.nextDouble * 1000
sleep(randomSleepTime)
randomPrice
}
现在你想同时获取三支股票的价格
package futures
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Success}
object MultipleFutures extends App {
// use this to determine the “delta time” below
val startTime = currentTime
// (a) create three futures
val aaplFuture = getStockPrice("AAPL")
val amznFuture = getStockPrice("AMZN")
val googFuture = getStockPrice("GOOG")
// (b) get a combined result in a for-expression
val result: Future[(Double, Double, Double)] = for {
aapl <- aaplFuture
amzn <- amznFuture
goog <- googFuture
} yield (aapl, amzn, goog)
// (c) do whatever you need to do with the results
result.onComplete {
case Success(x) => {
val totalTime = deltaTime(startTime)
println(s"In Success case, time delta: ${totalTime}")
println(s"The stock prices are: $x")
}
case Failure(e) => e.printStackTrace
}
// important for a short parallel demo: you need to keep
// the jvm’s main thread alive
sleep(5000)
def sleep(time: Long): Unit = Thread.sleep(time)
// a simulated web service
def getStockPrice(stockSymbol: String): Future[Double] = Future {
val r = scala.util.Random
val randomSleepTime = r.nextInt(3000)
println(s"For $stockSymbol, sleep time is $randomSleepTime")
val randomPrice = r.nextDouble * 1000
sleep(randomSleepTime)
randomPrice
}
def currentTime = System.currentTimeMillis()
def deltaTime(t0: Long) = currentTime - t0
}
Future还有很多其他方法,可以专门了解:
- onComplete
- onSuccess
- onFailure
- filter
- foreach
- map
- andThen
- fallbackTo
- recoverWith