Try的使用
2015-05-17
考虑一道题,我们从控制台输入获得除数和被除数,最后算出整除的结果。显然需要考虑除数和被除数必须是整数,除数还必须不能为零。我们可以用以下的算法:
def divide1 {
val dividend:Option[Int] = try {
Some(Console.readLine("Enter an Int that you'd like to divide:\n").toInt)
}catch {
case e:Exception =>
println("You must've divided by zero or entered something that's not an Int. Try again!")
None
}
val divisor:Option[Int] = try {
Some(Console.readLine("Enter an Int that you'd like to divide by:\n").toInt)
}catch {
case e:Exception =>
println("You must've divided by zero or entered something that's not an Int. Try again!")
None
}
val problem = dividend.flatMap(x => divisor.map(y => x/y))
problem match {
case None => divide1
case Some(result) => println("Result of " + dividend.get + "/"+ divisor.get +" is: " + result)
}
}
结果如果返回的是Some类型,则表示整除正确执行,如果返回的是None则表示有异常抛出。
这样确实可以,相比java,处理起来也更加简洁。
不过scala还针对异常处理提供了一个特殊类型:Try。
参考scala api提供的处理方式:
def divide: Try[Int] = {
val dividend = Try(Console.readLine("Enter an Int that you'd like to divide:\n").toInt)
val divisor = Try(Console.readLine("Enter an Int that you'd like to divide by:\n").toInt)
val problem = dividend.flatMap(x => divisor.map(y => x/y))
problem match {
case Success(v) =>
println("Result of " + dividend.get + "/"+ divisor.get +" is: " + v)
Success(v)
case Failure(e) =>
println("You must've divided by zero or entered something that's not an Int. Try again!")
println("Info from the exception: " + e.getMessage)
divide
}
}
这里使用Try代替传统的Option,当有异常抛出时,会返回Failure(e),如果程序正常执行则返回Success(result),通过模式匹配很容易得出最终结果。
通过使用Try,可以省去计算过程中多次的try-catch,统一在计算结果处进行模式匹配,代码可读性更高!
其实scala也提供了另外一个类:Either。
可以完成Try的工作,甚至不仅限于异常处理:
val in = Console.readLine("Type Either a string or an Int: ")
val result: Either[String,Int] = try {
Right(in.toInt)
} catch {
case e: Exception => Left(in)
}
println( result match {
case Right(x) => "You passed me the Int: " + x + ", which I will increment. " + x + " + 1 = " + (x+1)
case Left(x) => "You passed me the String: " + x
})
使用Either,你可以定义一个有可能返回两种类型返回值的对象,针对异常处理来说就是计算结果和异常信息。
val l: Either[String, Int] = Left("flower")
val r: Either[String, Int] = Right(12)
l.left.map(_.size): Either[Int, Int] // Left(6)
r.left.map(_.size): Either[Int, Int] // Right(12)
l.right.map(_.toDouble): Either[String, Double] // Left("flower")
r.right.map(_.toDouble): Either[String, Double] // Right(12.0)
唯一需要注意的地方就是如果我们对没有right值得Either调用right值,并继而调用map,flatMap,foreach等方法,会直接原封不动的返回它的left值。
要不再详细看看Try的源码,看看都有些什么内容?
sealed abstract class Try[+T] {
def foreach[U](f: T => U): Unit
def flatMap[U](f: T => Try[U]): Try[U]
def map[U](f: T => U): Try[U]
def filter(p: T => Boolean): Try[T]
def flatten[U](implicit ev: T <:< Try[U]): Try[U]
def recoverWith[U >: T](f: PartialFunction[Throwable, Try[U]]): Try[U]
}
其实看看代码也觉得没什么新鲜的,但自己为什么从没想过对异常处理有这样一种优雅的方式呢?感叹一下自己毕竟图样啊。
如代码所示,Try是一个带协变泛型的抽象类。其中包含若干方法,我抽出了比较有代表性的几个方法。foreach~flatten这五个方法肯定不会陌生,这也是支撑scala高级for循环的五件套,有这5个方法就能放在for循环中使用。
Success和Failure在实现这5件套时,Success(v)会操作对应的v值,而Failure会原封不动的返回自身。下面是一个简单的例子:
for (v <- Try(Array(1)(1))) yield v + 1 //Failure(java.lang.ArrayIndexOutOfBoundsException: 1)
for (v <- Try(Array(1)(0))) yield v + 1 //Success(2)
另外值得注意的实现是recoverWith方法,Success代表成功自然没有任何需要recover的地方,返回自身是很合理的。
Failure的recoverWith实现,如下所示:
final case class Failure[+T](val exception: Throwable) extends Try[T] {
def recoverWith[U >: T](f: PartialFunction[Throwable, Try[U]]): Try[U] =
try {
if (f isDefinedAt exception) f(exception) else this
} catch {
case NonFatal(e) => Failure(e)
}
}
recoverWith方法接收一个偏函数,当造成Failure的异常属于偏函数中定义的异常,则执行偏函数,否则返回对象本身。
这是我第一次偏函数是怎么被调用的,语义上很容易理解,有机会深入源码看看偏函数是怎么实现的。
对了,这里遇到了try-catch时的NonFatal对异常的过滤。
这个异常会过滤掉VirtualMachineError,ThreadDeath,InterruptedException,LinkageError, ControlThrowable,NotImplementedError,除了这些异常之外的异常都被认为是NonFatal异常。