我目前正试图围绕在Haskell中使用异常的正确方法。例外工作如何直截了当;我试图清楚地了解解释它们的正确方法。
基本的立场是,在设计良好的应用程序中,异常不应该逃到顶层。任何例外显然都是设计者没有预料到的例外 - 即程序错误(例如,除以零),而不是异常的运行时发生(例如,未找到文件)。
为此,我编写了一个简单的顶级异常处理程序,它捕获所有异常并将消息打印到stderr
,说“这是一个错误”(在重新抛出异常以终止程序之前)。
但是,假设用户按下Ctrl + C.这会导致抛出异常。显然这是不任何类型的程序错误。但是,未能预期并对此类用户中止做出反应可能会被视为错误。所以也许程序应该抓住它并适当地处理它,在退出之前做任何必要的清理。
问题是,但是......处理这个问题的代码将捕获异常,释放任何资源或其他任何内容,然后重新抛出异常!所以如果异常使它成为顶级,这并不一定意味着它未被处理。这只是意味着我们想要快速退出。
所以,我的问题:是否应该以这种方式将异常用于流量控制?是否每个明确捕获UserInterrupt
的函数都使用显式流控制结构来手动退出而不是重新抛出异常? (但那么调用者如何知道也退出?)UserInterrupt
是否可以达到顶级?但是在那种情况下,ThreadKilled
也可以通过相同的论证吗?
简而言之,中断处理程序是否应为UserInterrupt
(可能ThreadKilled
)提供特殊情况?那么HeapOverflow
或StackOverflow
怎么样? 那是一个错误吗?或者是“超出计划控制范围的情况”?
答案 0 :(得分:7)
但是,如果没有预料到这样的用户中止并做出反应可能会被视为一个错误。所以也许程序应该抓住它并适当地处理它,在退出之前做任何必要的清理。
从某种意义上说,你是对的 - 程序员应该预见到异常。但不是抓住他们。相反,您应该使用异常安全的函数,例如bracket
。
例如:
import Control.Exception
data Resource
acquireResource :: IO Resource
releaseResource :: Resource -> IO ()
workWithResource = bracket acquireResource releaseResource $ \resource -> ...
这样就可以清除资源,无论程序是否会被Ctrl + C中止。
现在,我想谈谈你的另一个陈述:
基本的立场是,在设计良好的应用程序中,异常不应该逃到顶层。
我认为,在设计良好的应用程序中,异常是一种非常好的中止方式。如果这有任何问题,那么你做错了(例如想在main
结束时执行清理操作 - 但这应该在bracket
完成!)。
以下是我在程序中经常做的事情:
定义表示任何可能错误的数据类型 - 可能出错的任何内容。其中一些经常包含其他例外。
data ProgramError
= InputFileNotFound FilePath IOException
| ParseError FilePath String
| ...
定义如何以用户友好的方式打印错误:
instance Show ProgramError where
show (InputFileNotFound path e) = printf "File '%s' could not be read: %s" path (show e)
...
将类型声明为异常:
instance Exception ProgramError
只要我愿意,就可以在程序中抛出这些异常。
您预期的异常必须被捕获并包装(例如在InputFileNotFound
中)以给予他们更多上下文。您没有预料到的例外情况如何?
我可以看到用户打印“这是一个错误”的一些价值,以便他们向您报告问题。如果你这样做,你应该预料到UserInterrupt
- 这不是一个错误,正如你所说。你应该如何对待ThreadKilled
取决于你的申请 - 从字面上看,你是否预料到它!
然而,这与“优秀设计”正交,更多地取决于您的目标用户类型,您对他们的期望以及他们对您的计划的期望。 响应的范围可以从仅打印异常到对话框“我们非常抱歉,您是否要向开发人员提交报告?”。
答案 1 :(得分:4)
是否应以此方式将异常用于流量控制?
是。我强烈建议您阅读Breaking from a loop,其中显示Either
和EitherT
的核心内容仅仅是提前退出代码块的抽象。例外情况只是这种行为的一种特殊情况,你会因为错误而退出,但没有理由认为这应该是你过早退出的唯一情况。