使用Scala的Future类型操作

469 阅读8分钟

阅读时间: 6 分钟

简介

Scala Future代表了一个异步计算的结果,这个结果可能是也可能不是可用的。

当我们创建一个新的Future时,Scala会生成一个新的线程并执行其代码。一旦执行完毕,计算的结果(值或异常)将被分配给Future

类型Future操作

地图

当我们拥有一个Future 实例时,我们可以使用map方法来转换它的成功结果,而不阻塞主线程

def increment(number: Int): Int = number + 1
val nextMagicNumberF: Future[Int] = magicNumberF.map(increment)

它通过对magicNumberF 的成功结果应用增量方法创建一个新的Future[Int] 否则,新的Future将包含与magicNumberF相同的异常*。*

增量方法的评估发生在另一个线程上,该线程来自隐含的 ExecutionContext。

flatMap方法

如果我们想用一个返回Future的函数来转换一个Future ,那么我们应该使用flatMap方法。

val updatedMagicNumberF: Future[Boolean] =
  nextMagicNumberF.flatMap(repository.updateMagicNumber)

它的行为方式与map方法相同,但使生成的Future保持平坦,返回Future*[Boolean]而不是Future[Future[Boolean]]*。

有了flatMapmap 方法,我们就有能力写出更容易理解的代码。

Publisher

让我们想象一下,我们有一个神奇数字的列表,我们想用Publisher来发布每一个数字。

val magicNumbers: List[Int] = List(1, 2, 3, 4)
trait Publisher {
  def publishMagicNumber(number: Int): Future[Boolean]
}

在这种情况下,我们可以使用Future.traverse方法,它对多个元素进行并行映射

val published: Future[List[Boolean]] =
  Future.traverse(magicNumbers)(publisher.publishMagicNumber)

它为每个给定的魔法数字调用publishMagicNumber 方法,并将它们合并为一个Future。每个评估都发生在从ExecutionContext中抽取的不同线程上*。*

如果其中任何一个失败,产生的Future也会失败。

Zip

在本节中,我们将演示如何使用Future.zip将两个Future操作的结果合并成一个元组。根据Scala API文档Future.zip将创建一个新的Future,其返回类型将是一个持有两个Future返回类型的元组。

1.定义一个返回Future Option的方法

让我们从我们熟悉的donutStock()方法开始,它返回一个Int类型的Future Option。


def donutStock(donut: String): Future[Option[Int]] = Future {
  println("checking donut stock")
  if(donut == "vanilla donut") Some(10) else None
}

2.2.定义一个方法,为donut的价格返回一个Future Double。

为了这个例子的目的,让我们创建另一个方法donutPrice()。它将返回一个双倍类型的Future来代表甜甜圈的价格。


println(s"\nStep 2: Define a method which returns a Future Double for donut price")
def donutPrice(): Future[Double] = Future.successful(3.25)

3.将第一个future的值与第二个future的值进行压缩

为了将donutStock()和donutPrice()这两个未来操作的结果合并成一个元组,你可以使用Zip方法,如下所示。结果是,注意donutStockAndPriceOperation的返回类型是Future[(Option[Int], Double)]。Option[Int]是donutStock()的返回类型,Double是donutPrice()的返回类型,这两种类型都包含在一个元组中。


println(s"\nStep 3: Zip the values of the first future with the second future")
val donutStockAndPriceOperation = donutStock("vanilla donut") zip donutPrice()
donutStockAndPriceOperation.onComplete {
  case Success(results) => println(s"Results $results")
  case Failure(e)       => println(s"Error processing future operations, error = ${e.getMessage}")
}

当你在IntelliJ中运行你的Scala应用程序时,你应该看到以下输出。


Step 3: Zip the values of the first future with the second future
checking donut stock
Results (Some(10),3.25)

ZipWith

与future zip()方法类似,Scala也提供了一个方便的future**zipWith()**方法。除了合并两个期货的结果外,**zipWith()**方法还允许你通过一个可以应用于结果的函数。

1.定义一个方法,返回一个期货期权

让我们从我们的donutStock()方法开始,它返回一个Int类型的Future Option。


println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
  println("checking donut stock")
  if(donut == "vanilla donut") Some(10) else None
}

2.2.定义一个方法,为donut的价格返回一个Future Double。

接下来,让我们创建另一个名为donutPrice()的方法,它返回一个代表特定甜甜圈价格的Double类型的期货。


println(s"\nStep 2: Define a method which returns a Future Double for donut price")
def donutPrice(): Future[Double] = Future.successful(3.25)

3.定义一个值函数,将Tuple(Option[Int], Double)转换成Tuple(Int, Double)。

当我们用donutPrice()Future压缩donutStock()Future时,结果的返回类型是一个Option[Int]和Double的元组。Option[Int]类型代表来自donutStock()方法的数量,Double类型代表来自donutPrice()的价格。下面的qtyAndPriceF函数是一个假的例子,用于映射来自Option的Int值,我们将把这个函数传递给未来的zipWith()


println(s"\nStep 3: Define a value function to convert Tuple (Option[Int], Double) to Tuple (Int, Double)")
val qtyAndPriceF: (Option[Int], Double) => (Int, Double) = (someQty, price) => (someQty.getOrElse(0), price)

4.调用Future.zipWith并传递函数qtyAndPriceF

通过将步骤3中的函数qtyAndPriceF传递给**zipWith()**方法,类型(Option[Int], Double)将被转换为(Int, Double)。


println(s"\nStep 4: Call Future.zipWith and pass-through function qtyAndPriceF")
val donutAndPriceOperation = donutStock("vanilla donut").zipWith(donutPrice())(qtyAndPriceF)
donutAndPriceOperation.onComplete {
  case Success(result) => println(s"Result $result")
  case Failure(e)      => println(s"Error processing future operations, error = ${e.getMessage}")
}

当你在IntelliJ中运行你的Scala应用程序时,你应该看到以下输出。


Step 4: Call Future.zipWith and pass-through function qtyAndPriceF
checking donut stock
Result (10,3.25)

错误处理

futurerecover()

在本节中,我们将展示如何使用futurerecover()函数来帮助你解决未来操作中可能出现的异常。不过,通常情况下,你会希望从future可能抛出的已知异常中恢复,而不是捕获任何随机异常。

1.定义一个返回Future的方法

像往常一样,让我们从创建donutStock()方法开始,该方法返回一个Int类型的Future。在donutStock()方法的主体中,我们将对任何与字符串vanilla donut不匹配的donut抛出一个IllegalStateException


println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
  if(donut == "vanilla donut") 10
  else throw new IllegalStateException("Out of stock")
}

2.执行donutStock()未来操作

在这一步,我们将调用donutStock()方法,并通过String vanilla donut作为输入。我们希望这个未来操作能够顺利完成,因为我们只在输入的字符串不是香草甜甜圈时抛出一个异常。


println("\nStep 2: Execute donutStock() future operation")
donutStock("vanilla donut")
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

当你在IntelliJ中运行你的Scala应用程序时,你应该看到以下输出。


Step 2: Execute donutStock() future operation
Results 10

3.调用Future.recover以从已知异常中恢复

然而,在这一步中,我们将把未知甜甜圈的输入字符串作为参数传递给donutStock()方法。因此,根据donutStock()方法的实现,我们知道这个输入字符串将抛出一个异常。为此,我们可以使用**recover()**函数来帮助我们继续我们的程序流程。在我们下面的例子中,我们只是简单地返回一个Int值为0的甜甜圈股票。

还要注意的是,我们明确地只对IllegalStateException进行恢复。对于更普遍的情况,你可以用以下方法捕获NonFatal异常:case NonFatal(e) => 0


println("\nStep 3: Call Future.recover to recover from a known exception")
donutStock("unknown donut")
  .recover { case e: IllegalStateException if e.getMessage == "Out of stock" => 0 }
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

当你在IntelliJ中运行你的Scala应用程序时,你应该看到以下输出。


Step 3: Call Future.recover to recover from a known exception
Results 0

Future recoverWith

在上一节中,我们介绍了futurerecover()方法。同样地,Scala也提供了一个future**recoverWith()**方法,但它需要的返回类型是Future。根据Scala官方API文档,通过比较它们的方法签名,你可以直观地发现recover和recoverWith之间的区别。

recover:

def recover[U >: T](pf: PartialFunction[Throwable, U])

recoverWith。

def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])

1.定义一个返回Scala Future的方法

让我们开始创建我们熟悉的donutStock()方法,返回一个Int类型的Future。注意,对于任何不是vanilla donut的输入,我们将抛出一个IllegalStateException()


println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
  if(donut == "vanilla donut") 10
  else throw new IllegalStateException("Out of stock")
}

2.执行donutStock() Scala****Future操作

在这一步,我们将通过香草甜甜圈的输入参数,我们应该期待未来的**onComplete()**回调成功运行。


println("\nStep 2: Execute donutStock() future operation")
donutStock("vanilla donut")
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

当你在IntelliJ中运行你的Scala应用程序时,你应该看到以下输出。


Step 2: Execute donutStock() future operation
Results 10

3.调用Future.recoverWith来恢复已知的异常

在下面的代码片段中,我们将通过一个未知的甜甜圈输入到donutStock()方法中,因此应该期望抛出一个异常。通过使用**recoverWith()**方法,我们可以继续我们程序的执行流程。像往常一样,首先要尽量避免抛出异常但有时在处理I/O时,从一些已知的异常中恢复是有意义的。


println("\nStep 3: Call Future.recoverWith to recover from a known exception")
donutStock("unknown donut")
  .recoverWith { case e: IllegalStateException if e.getMessage == "Out of stock" => Future.successful(0) }
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

当你在IntelliJ中运行你的Scala程序时,你应该看到以下输出。


Step 3: Call Future.recoverWith to recover from a known exception
Results 0

结语

在这篇文章中,我们探索了Scala的FutureAPI*。*

我们看到了如何使用Future启动异步计算,以及如何用其类型操作转换future的结果。

我们展示了如何处理成功的结果和错误,以及如何组合结果。