16.3 错误处理

有些任务本身就是有风险的。文件或数据库的读取和写入是出了名的容易出错,因为你对文件系统、网络或数据库没有完全的控制权。其实,每当R与其他软件进行交互(如通过rJava访问Java代码,通过R2WinBUGS访问WinBUGS,或任何其他数百个R可以连接的软件),都有引发错误的可能。

对于这些危险的任务1,你需要决定出现问题时如何处理。有时,当错误报出时,即使停止执行程序也是没有用的。例如,如果你正在遍历文件并导入它们,如果这时导入失败,你不会希望仅仅停止执行而失去所有已成功导入的数据。

1好吧,连接到一个文件并不那么严重,但它在编程中却属于高风险。

事实上,这点可以概括为:任何时候如果你的循环中正在运行一些有风险的事,你不会想在一次迭代失败后就放弃所有之前的进展。在下例中,我们尝试将列表中的每个元素转换成数据帧:

  1. to_convert <- list(
  2. first = sapply(letters[1:5], charToRaw),
  3. second = polyroot(c(1, 0, 0, 0, 1)),
  4. third = list(x = 1:2, y = 3:5)
  5. )

如果我们只是纯粹运行以上代码而不加任何保护,它会失败:

  1. lapply(to_convert, as.data.frame)
  2. ## Error: arguments imply differing number of rows: 2, 3

糟糕!第三个元素因为长度不同而不能转换,而我们也失去了所有进展。

防止彻底失败的最简单的方法就是把容易出错的代码包括在try函数里:

  1. result <- try(lapply(to_convert, as.data.frame))

现在,虽然错误会被打印到控制台上,但是代码不会停止执行(你可以通过传递silent = TRUE来抑制这种消息) 。

如果传递给try函数的代码执行成功(没有抛出任何错误),那么result就是原来计算的结果,和平时一样。如果代码运行失败,那么result将是一个try-error类的对象。这就是说,当你写了一行包括try的代码,下一行看起来应该是这样:

  1. if(inherits(result, "try-error"))
  2. {
  3. # 用于错误处理的代码
  4. } else
  5. {
  6. #正常执行的代码
  7. }
  8. ## NULL

因为你每次都需要包括这些额外的代码,所以使用try函数的代码看上去有点丑陋。要使之更美观2则可使用tryCatchtryCatch接收一个表达式来安全运行,正如try一样,且它还有内置的错误处理机制。

2不要低估代码美观的重要性。比起写代码,你会花更多的时间阅读它。

为了处理一个错误,把一个函数传递给一个叫error的参数。这个error参数接受一个错误(从技术上讲,它是类simpleError的对象),你可以任意操作、打印或忽略它。如果这听起来很复杂,请不要担心:它在实际中非常容易。在下例中,当发生错误时,我们会打印错误消息并返回一个空的数据框:

  1. tryCatch(
  2. lapply(to_convert, as.data.frame),
  3. error = function(e)
  4. {
  5. message("An error was thrown: ", e$message)
  6. data.frame()
  7. }
  8. )
  9. ## An error was thrown: arguments imply differing number of rows: 2, 3
  10. ## data frame with 0 columns and 0 rows

使用tryCatch的另一个技巧:你可以把一个表达式传递给一个命名为finally的参数,无论错误是否抛出它都会运行(就像在我们连接数据库时所看到的on.exit函数一样)。

尽管我们已经尝试了trytryCatch,但是还没有解决问题:在遍历时,即使抛出了一个错误,我们也能保存迭代成功的那部分结果。

为了实现这一目标,需把trytryCatch置于循环内:

  1. lapply(
  2. to_convert,
  3. function(x)
  4. {
  5. tryCatch(
  6. as.data.frame(x),
  7. error = function(e) NULL
  8. )
  9. }
  10. )
  11. ## $first
  12. ## x
  13. ## a 61
  14. ## b 62
  15. ## c 63
  16. ## d 64
  17. ## e 65
  18. ##
  19. ## $second
  20. ## x
  21. ## 1 0.7071+0.7071i
  22. ## 2 -0.7071+0.7071i
  23. ## 3 -0.7071-0.7071i
  24. ## 4 0.7071-0.7071i
  25. ##
  26. ## $third
  27. ## NULL

因为这是一个公共代码片断,所以plyr包中有一个函数tryapply,它正好能处理这种情况,且看上去更精简:

  1. tryapply(to_convert, as.data.frame)
  2. ## $first
  3. ## x
  4. ## a 61
  5. ## b 62
  6. ## c 63
  7. ## d 64
  8. ## e 65
  9. ##
  10. ## $second
  11. ## x
  12. ## 1 0.7071+0.7071i
  13. ## 2 -0.7071+0.7071i
  14. ## 3 -0.7071-0.7071i
  15. ## 4 0.7071-0.7071i

眼尖的观察者可能会发现,错误处理在这种情况下被直接移除了。