bigbully +

Try的使用

考虑一道题,我们从控制台输入获得除数和被除数,最后算出整除的结果。显然需要考虑除数和被除数必须是整数,除数还必须不能为零。我们可以用以下的算法:

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异常。