在 Scala 中选择正确的异常处理

Picking the right exception handling in scala

我是 Scala 的新手,对处理异常的各种方法感到有点困惑,并正在寻找有关该主题的最佳实践建议。我正在编写一个简单的方法来使用现有的阻塞 SDK 检索客户。可能的结果是:

  1. 找到客户
  2. 未找到客户(从 SDK 返回 NotFoundException)
  3. 与远程服务器通信时出错(SDK 抛出一些其他异常)

所以我希望我的方法有一个 return 类型 Future[Option[Customer]] 和 return 以上每种情况:

  1. 成功的未来:一些(客户)
  2. 成功的未来:None
  3. 失败的未来

这是我用try/catch写的:

private def findCustomer(userId: Long): Future[Option[Customer]] = future {
  try {
    Some(gateway.find(userId))
  } catch {
    case _: NotFoundException => None
  }
}

这很好用,对我来说似乎很干净,但似乎并不是 "Scala way" 真正做到的 - 有人告诉我要避免使用 try/catch。所以我一直在寻找一种使用 Try 来重写它的方法。

这里有 2 个变体(我认为)表现完全相同,使用 Try

变体 A:

private def findCustomer(userId: Long): Future[Option[Customer]] = future {
  Try(
    Some(gateway.find(userId))
  ).recover {
    case _: NotFoundException => None
  }.get
}

变体 B:

private def findCustomer(userId: Long): Future[Option[Customer]] = future {
  Try(
    Some(gateway.find(userId))
  ).recover {
    case _: NotFoundException => None
  }
} flatMap {
  case Success(s) => Future.successful(s)
  case Failure(f) => Future.failed(f)
}

我不是 A 的超级粉丝(尽管它比 B 更简洁),因为 .get 似乎有点诡异。 B 绝对是最明确的,但是将 Try 案例映射到相应的 Future 结果似乎很无聊。

经验丰富的 Scala 程序员会如何编写此代码?

一种选择是避免链接 Try 和 Future。从某种意义上说,未来是一个异步尝试。

您可以直接使用 Future[Customer] 并将 NotFoundException 视为要从中恢复的对象,而不是 None 值。通常,您会在 future 本身上进行链式操作,而无需处理失败案例(通过映射等)。这完全取决于您将如何使用它(未来完成时您的逻辑如何分支)。换句话说,也许它对您来说似乎很复杂,因为它确实如此,并且您通过强制 Future[Option[Customer]] return 类型来强制它。

当且仅当一切顺利时,执行链式多个操作并继续计算的能力是这里很好的 Scala 功能(尤其是在理解等方面)。

您可能正在寻找:

Future.fromTry(myTry)

因此,对于您的示例,您可以这样做:

Future.fromTry {
    Try(Some(gateway.find(userId))).recover {
        case _: NotFoundException => None
    }
}

只捕获 NotFoundException,就像您的解决方案一样。这里唯一的问题是该方法不会异步执行。如果有必要,请按照 Ionut 的建议考虑使用 Future.recover

这里值得一提的另一个惯用选项是使用 Either[F, S],其中 S 是成功 return 的类型,而 F 可以容纳错误(你可能想要传播)。因此,您可以使用 Either[ Exception, Option[String]]Either[String, Option[String]],其中第一个 String 是一条错误消息。

我认为您使用 try/catch 的初始版本非常好,因为它是现有 SDK 的包装器。

或者,您可以在 Future 上使用 recover 方法:

def findCustomer(userId: Long): Future[Option[Customer]] =
  Future(Option(gateway.find(userId))).recover {
    case e: NotFoundException => None
  }

如果您准备好领养一些 scalaz。从 scalaz 分离在处理错误场景时非常有用和自然。这就像 scala Either 但 scalaz 析取 \/ 是右偏的。您将在右侧获得成功值,在左侧获得异常值。在分离的左侧用 \/.fromTryCatch returns 异常包装你的代码块。正确的总会有成功的价值。析取映射比 Scala Either 更容易,因为析取是右偏的,很容易从右给你价值。

import scalaz.\/

private def findCustomer(userId: Long): Future[\/[Throwable,Option[Customer]] ]= future {
  \/.fromTryCatch {
    Some(gateway.find(userId))

  }
}