CH04-异常
抛出异常会产生副作用,在函数式解决方案中,以值的方式返回错误更安全,符合引用透明,并且可以通过高阶函数保存异常的优点 - 统一错误处理逻辑。核心思想就是使用普票值来表现异常。
异常的优点与劣势
一个抛出异常的例子:
def failingFn(i: Int): Int = {
val y:Int = throw new Exception("fail!") // 抛出异常
try{
val x = 42 + 5
x + y
} catch {
case e:Exception => 43
}
}
调用该函数会抛出预期的异常,为了证明异常会破坏引用透明,我们可以将y
进行替换:
def failingFn2(i: Int) = {
try{
val x = 42 + 5
x + (throw new Exception("fail!"): Int) // 抛出异常的表达式可以声明为任何类型
} catch {
case e: Exception => 43
}
}
运行结果会得到 43 而不是像替换前一样排除预期的异常,因此,异常并不是引用透明的。
引用透明的表达式不依赖上下文,可以本地推导,而那些非引用透明的表达式是依赖上下文的,并且需要全局推导。比如把42 + 5
这个表达式嵌入到一个更大的表达式中,他不会受这个更大的表达式的影响,即对更大的表达式产生依赖,它永远等于 47。但是把throw new...
嵌入到一个更大的表达式,比如嵌入到一个try/catch
结构中,它的值取决于catch
部分的处理,因此对更大的表达式产生了依赖。
异常的问题:
- 异常破坏了引用透明并且引入了上下文依赖。
- 异常不是类型安全的。函数
failing
,Int => Int
并不会告诉我们会发生什么样的异常。因为没有受检异常,直到运行时才会发现异常。
我们希望其他选择能够排除异常的这些缺点,但是又不想失去异常最主要的好处:整合集中的错误处理逻辑,而不是在整个代码库发布这个逻辑。
异常的其他选择
比如一个计算列表平均值的函数:
def mean(xs:Seq[Double]): Double =
if (xs.isEmpty) throw new ArithmeticException("mean of empty list!")
else xs.sum / xs.length
这是一个典型的偏函数,他对一些输入没有做定义。如果一个函数对那些非隐式的输入类型做了一些假设,那它就是一个典型的偏函数。
除了选择抛出异常,还有其他的选择。
第一种是返回某个伪造的 Double 类型的值。比如为空时返回Double.NaN
,或者其他报警值,或者null
。但是以下理由使我们放弃这种方案:
- 它允许错误无声的传播,如果忘了对这样的值进行检查也不会得到警告,会使后面的代码出错。
- 使用显式的
if
来检查是否得到了正确的结果会导致大量的模板代码,特别是调用多个函数时。 - 不适用与多态代码。比如一个查找最大值的泛型函数
def max[A](xs:List[A])
,当传入为空时无法发明一个 A 类型的值。也不能是null
,因为null
只对非基础类型有效,但是这里的 A 可能是Int
或Double
。 - 需要一个特定的策略或调用约定 - 告诉调用者如何合理的使用
mean
函数。这导致它不能传递给高阶函数,因为高阶函数对待所有参数都是一致的。
第二种是强迫调用者提供一个参数告诉我们该如何处理。比如:
def mean(xs:Seq[Double], onEmpty: Double): Double =
if (xs.isEmpty) onEmpty
else xs.sum / xs.length
这使mean
函数称为一个完全函数。调用者必须知道如何处理未定义的情况。
Option 数据类型
解决方案是在返回值类型明确表示该函数并不总是有结果。
sealed trait Option[+A]
case class Some[+A](get:A) extends Option[A]
case object None extends Option[A]
Option
是一个最多只包含一个元素的List
。
基础 Option 函数
trait Option[+A] {
def map[B](f: A => B): Option[B] // 如果 Option 不为 None,对其应用 f
def flatMap[B](f: A => Option[B]): Option[B] // 如果 Option 不为 None,对其应用 f,可能会失败
def getOrElse[B >: A](default: B): B // 默认值类型 B 必须是 A 的父类
def orElse[B >: A](ob: => Option[B]): Option[B] // 不对 ob 求值,除非需要
def filter(f: A => Boolean): Option[A] // 如果值不满足,转换 Some 为 None
}
基础函数使用场景
???
Option 组合、提升、面向异常的 API 封装
可能会认为,一旦开始使用Option
,整个库都会受影响,因为一些方法必须改变为接收或返回Option
。但是,我们可以把一个普通函数**提升(lift)**为一个对Option
操作的函数。
比如,map
函数支持我们用一个类型为A => B
的函数来操作一个Option
。从另一个角度看,map
可以把一个A => B
的函数转换为Option[A] => Option[B]
类型的函数:
def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f
现在,可以把普通的abs
函数转换为处理Option
的函数:
val absOpt:Option[Double] => Option[Double] = lift(math.abs)
而Try
函数是一个通用目的的函数,用于将一个基于异常的 API 转换成一个面向Option
的 API。
Either 数据类型
Option
只能用来表示可能不存在的值,并不能表示异常条件下发生了什么错误。如果除了需要获取异常时可能的值,还需要知道异常时的错误信息,可以使用Either
:
sealed trait Either[+E, +A]
case class Left[+E](value: E) extends Either[E, Nothing]
case class Right[+A](value: A) extends Either[Nothing, A]
Either
在两种情况都有值,它的值只能是两种情况中的一种,称他是两个类型的互斥并集。一般使用Left
表示失败,而Right
表示成功。
使用Either
改写前面的mean
函数:
def mean(xs:List[Double]): Either[String, Double] =
if (xs.isEmpty) Left("mean of empty list!")
else Right(xs.sum / xs.length)
或者在Left
中包含处理的异常以获取详细的调用栈信息:
def safeDiv(x:Int, y:Int):Either[Exception, Int] =
try Right(x / y)
catch {case e:Exception => Left(e)}
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.