为什么要设计一种没有异常处理机制的现代语言?


47

许多现代语言提供了丰富的异常处理功能,但是Apple的Swift编程语言没有提供异常处理机制

像我一样,我陷入了异常,无法理解这意味着什么。Swift有断言,当然还有返回值。但是我很难想象我基于异常的思维方式如何映射到一个没有例外的世界(就此而言,为什么这样的世界是可取的)。有像Swift这样的语言我无法做的事情我可以做例外吗?通过丢掉异常我能获得一些收益吗?

例如,我如何最好地表达这样的话

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

缺少异常处理的语言(例如,Swift)?


11
如果我们只是忽略panic不完全相同的内容,则可能需要将Go添加到列表中。除了那里所说的,例外只是一种复杂的(但舒适的)执行a的方法GOTO,尽管出于明显的原因没有人这么称呼它。
JensG 2014年

3
您问题的排序答案是,您需要对异常的语言支持才能编写它们。语言支持通常包括内存管理;由于可以在任何地方抛出异常并在任何地方捕获异常,因此需要一种不依赖控制流的对象处置方法。
罗伯特·哈维

1
我不太关注你,@ Robert。C ++设法支持无垃圾收集的异常。
卡尔·比勒费尔德

3
@KarlBielefeldt:据我所知,付出了巨大的代价。再说一次,至少在努力和所需的领域知识上,有没有用C ++进行的大量花费?
罗伯特·哈维

2
@RobertHarvey:好点。我是对这些事情不够认真思考的人之一。我一直不敢相信ARC是GC,但事实并非如此。因此,从根本上(如果我大致理解的话),在一种处理对象依赖于控制流的语言中,异常将是一件混乱的事情(尽管有C ++)?
orome 2014年

Answers:


33

在嵌入式编程中,传统上不允许使用异常,因为在尝试维护实时性能时,必须解开的堆栈开销被认为是不可接受的可变性。尽管从技术上讲智能手机可以视为实时平台,但它们现在已经足够强大,而嵌入式系统的旧局限性不再适用。我只是为了透彻而提出。

功能编程语言通常支持异常,但是异常很少使用,因此也可能不支持。原因之一是惰性评估,即使在默认情况下也不是惰性的语言中,有时也会这样做。由于函数执行的堆栈不同于其排队执行的位置,因此很难确定将异常处理程序放置在何处。

另一个原因是,一流的函数允许使用诸如期权和期货之类的结构,这些结构为您带来异常的语法优势,并且具有更大的灵活性。换句话说,该语言的其余部分具有足够的表现力,以至于异常不会给您带来任何好处。

我对Swift不熟悉,但是我对它的错误处理的了解不多,这表明它们打算进行错误处理以遵循更多的功能样式模式。我看过带有successfailure块的代码示例,它们看起来非常像期货。

下面是一个使用的例子Future,从这个斯卡拉教程

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

您可以看到它的结构与使用异常的示例大致相同。该future块是象try。该onFailure块就像一个异常处理程序。与大多数功能语言一样,在Scala中Future完全使用该语言本身来实现。它不需要像异常一样的任何特殊语法。这意味着您可以定义自己的类似结构。timeout例如,可能添加一个块或记录功能。

此外,您可以传递未来,从函数返回未来,将其存储在数据结构中,等等。这是一流的价值。您不会像必须直接在堆栈中传播的异常那样受到限制。

选项以略有不同的方式解决错误处理问题,在某些用例中效果更好。您不会只局限于一种方法。

这些就是您“通过丢失例外获得的收益”。


1
因此Future,本质上是一种检查从函数返回的值而不停止等待的方法。与Swift一样,它都是基于返回值的,但是与Swift不同的是,对返回值的响应可能会在以后发生(有点像异常)。对?
orome 2014年

1
您理解Future正确,但是我认为您可能误解了Swift。例如,请参阅此stackoverflow答案的第一部分。
Karl Bielefeldt

嗯,我是Swift的新手,所以我很难解析这个答案。但是,如果我没记错的话,那实际上是传递了一个可以在以后调用的处理程序;对?
orome

是。发生错误时,您基本上是在创建回调。
Karl Bielefeldt 2014年

Either会是一个更好的例子,恕我直言
帕维尔Prażak

19

异常会使代码更难以推理。 尽管它们不如gotos强大,但由于其非本地性,它们可能导致许多相同的问题。例如,假设您有一段这样的命令性代码:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

您不能一目了然地判断这些过程中的任何一个是否都可以引发异常。您必须阅读每个过程的文档才能弄清楚这一点。(某些语言通过使用此信息来增强类型签名,使此操作稍微容易一些。)上面的代码将编译得很好,而不管是否有任何过程抛出,这使得忘记处理异常真的很容易。

此外,即使目的是将异常传播回调用者,通常也需要添加其他代码以防止事物处于不一致状态(例如,如果咖啡机坏了,则仍然需要清理混乱并返回)杯子!)。因此,在许多情况下,由于需要额外的清理,使用异常的代码看起来与没有使用异常的代码一样复杂。

可以使用功能强大的类型系统来模拟异常。许多避免异常的语言都使用返回值来获得相同的行为。它类似于在C语言中的处理方式,但是现代类型的系统通常使它更优雅,并且更难忘处理错误条件。它们还可以提供语法糖,以减轻麻烦,有时几乎和例外一样干净。

特别是,通过将错误处理嵌入类型系统而不是将其实现为单独的功能,可以将“例外”用于甚至与错误无关的其他事物。(众所周知,异常处理实际上是monad的属性。)


Swift所拥有的那种类型系统(包括可选的)是否是实现此目标的那种“强大的类型系统”?
orome 2014年

2
是的,可以选择,并且更一般地,总和类型(在Swift / Rust中称为“枚举”)可以实现此目的。然而,需要花费一些额外的工作才能使它们使用起来令人愉悦:在Swift中,这是通过可选的链接语法实现的;在Haskell中,这是通过单子符号实现的。
Rufflewind 2014年

一个“足够强大的类型系统”能不能给一个堆栈跟踪,如果不是它的好看不中用
帕维尔Prażak

1
+1指出异常会掩盖控制流。就像一个辅助说明:可以合理地解释异常实际上是否不比邪恶gotogoto仅限于单个函数,如果函数确实很小,则范围很小,异常的作用类似于gotocome fromen.wikipedia.org/wiki/INTERCAL ;-))。它几乎可以连接任何两段代码,可能会跳过某些第三功能的代码。它唯一不能做的事情,goto可以回去。
cmaster

2
@PawełPrażak使用许多高阶函数时,堆栈跟踪几乎没有价值。关于输入和输出的有力保证以及避免的副作用是防止这种间接导致令人困惑的错误的原因。
杰克

11

这里有一些很好的答案,但是我认为其中一个重要的原因还没有得到足够的重视:当发生异常时,对象可以处于无效状态。如果您可以“捕获”异常,则您的异常处理程序代码将能够访问和使用那些无效对象。除非这些对象的代码编写得很完美,否则这将是非常错误的,这是很难做到的。

例如,假设实现Vector。如果有人用一组对象实例化Vector,但是在初始化过程中发生异常(例如,将对象复制到新分配的内存中),则很难正确地编码Vector实现,而无需内存泄漏。Stroustroup的这篇简短论文涵盖了Vector示例

那仅仅是冰山一角。例如,如果您复制了某些元素,但不是全部元素,该怎么办?为了正确地实现像Vector这样的容器,您几乎必须使您采取的所有操作都是可逆的,因此整个操作是原子性的(就像数据库事务一样)。这很复杂,大多数应用程序都会弄错。即使正确完成,它也会大大简化实现容器的过程。

因此,一些现代语言认为这是不值得的。例如,Rust确实有例外,但是不能“捕获”它们,因此代码无法与处于无效状态的对象进行交互。


1
捕获的目的是使对象在发生错误后保持一致(或在不可能的情况下终止某些对象)。
JoulinRouge

@JoulinRouge我知道。但是某些语言决定不给您机会,而是使整个过程崩溃。那些语言设计师知道您想要进行的清理,但是得出的结论是太棘手了,而这样做的权衡是不值得的。我意识到您可能不同意他们的选择...但是了解他们出于这些特殊原因有意识地做出选择是很有价值的。
查理·

6

我最初对Rust语言感到惊讶的一件事是它不支持catch异常。您可以抛出异常,但是当任务(认为线程,但不总是一个单独的OS线程)死亡时,只有运行时才能捕获它们。如果您自己启动任务,则可以询问任务是正常退出还是完成fail!()

因此,它并非fail经常出现。确实发生这种情况的情况很少,例如,在测试工具中(它不知道用户代码是什么样的),在编译器的顶层(大多数编译器派生),或者在调用回调时根据用户输入。

取而代之的是,常见的成语是使用Result模板明确放过应该处理的错误。通过可以大大简化此过程,try!可以包装在产生Result的任何表达式周围,如果存在则返回成功的分支,否则可以从函数中早返回。

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}

1
因此,可以公平地说这也(类似于Go的方法)类似于Swift,后者具有assert,但没有catch
orome 2014年

1
在Swift中,尝试!意思是:是的,我知道这可能会失败,但是我确定它不会,所以我不会处理它,如果失败,那么我的代码是错误的,因此在这种情况下请崩溃。
gnasher729

6

我认为,异常是在运行时检测代码错误的基本工具。无论是在测试中还是在生产中。使他们的消息足够详细,以便与堆栈跟踪结合使用,您可以从日志中找出发生了什么。

异常通常是一种开发工具,也是一种在意外情况下从生产中获取合理错误报告的方法。

除了关注点分离(幸福的道路,只有预期的错误,以及在遇到意外错误之前到达通用处理程序之前)是一件好事,使您的代码更具可读性和可维护性之外,实际上不可能为所有可能的事情准备代码意外的情况,甚至通过使用错误处理代码使其膨胀来完全不可读的情况也是如此。

这实际上是“意外”的意思。

顺便说一句,什么是预期的,什么不是只能在呼叫站点做出的决定。这就是为什么Java中无法检查异常的原因-在开发API时就做出了决定,当时还不清楚是什么预期或意外情况。

一个简单的例子:哈希映射的API可以有两种方法:

Value get(Key)

Option<Value> getOption(key)

第一个抛出异常(如果找不到),后者为您提供可选值。在某些情况下,后者更有意义,但在其他情况下,您的代码必须清楚地知道给定键的值,因此,如果没有一个,这是该代码无法修复的错误,因为基本假设失败了。在这种情况下,如果调用失败,从代码路径掉到某种通用处理程序实际上是期望的行为。

代码绝不应尝试处理失败的基本假设。

当然,除了检查它们并抛出可读性强的异常。

抛出异常不是邪恶的,但是捕获异常可能是邪恶的。不要尝试修复意外错误。在一些您希望继续执行某些循环或操作的地方捕获异常,将其记录下来,也许报告一个未知错误,仅此而已。

到处都是鱼块是一个非常糟糕的主意。

以易于表达意图的方式设计API,即声明是否期望某种情况,例如找不到密钥。然后,API的用户只能在真正意外的情况下选择throwing调用。

我猜想人们会讨厌异常,而忽略了错误处理自动化以及将关注点与新语言更好地分离的关键工具而走得太远,这是不好的经验。

那,以及对它们实际上有什么好处的一些误解。

通过Monadic绑定进行所有操作来模拟它们,会使代码的可读性和可维护性降低,并且最终没有堆栈跟踪,这会使此方法变得更糟。

函数样式错误处理非常适合预期的错误情况。

让异常处理自动处理其余所有工作,这就是它的作用:)


3

因此,Swift在这里使用与Objective-C相同的原理。在Objective-C中,异常表示编程错误。除崩溃报告工具外,不会处理它们。通过修复代码来完成“异常处理”。(有一些例外。例如,在进程间通信中。但这很少见,并且很多人从未遇到过。Objective-C实际上具有try / catch / finally / throw,但很少使用它们)。Swift只是消除了捕获异常的可能性。

Swift具有看起来像异常处理的功能,只是强制执行的错误处理。从历史上看,Objective-C的错误处理模式非常普遍:方法将返回BOOL(成功则为YES)或对象引用(失败则为nil,成功则为nil),并具有参数“指向NSError *的指针”。这将用于存储NSError参考。Swift自动将对这种方法的调用转换为类似于异常处理的内容。

通常,Swift函数可以轻松地返回替代方法,例如,如果一个函数运行良好,则返回结果;如果失败,则返回错误;这使得错误处理变得容易得多。但是,对于原始问题的答案是:Swift设计师显然认为,如果语言没有例外,创建一种安全的语言并用这种语言编写成功的代码会更容易。


对于Swift,这是正确的答案,IMO。Swift必须与现有的Objective-C系统框架保持兼容,因此在幕后他们没有传统的例外。我写了一个博客张贴在不久前对ObjC如何工作的错误处理:orangejuiceliberationfront.com/...
uliwitness

2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

在C语言中,您将得到与上述类似的结果。

在Swift中我有不能做的事情可以做例外吗?

不,没有任何东西。您最终只处理结果代码,而不是异常。
异常允许您重新组织代码,以使错误处理与快乐路径代码分开,但仅此而已。


而且,同样,那些调用...throw_ioerror()返回错误而不是抛出异常?
orome 2014年

1
@raxacoricofallapatorius如果我们声称不存在异常,那么我假设程序遵循通常的模式:失败时返回错误代码,成功时返回错误代码0。
stonemetal 2014年

1
@stonemetal某些语言,例如Rust和Haskell,使用类型系统返回比错误代码更有意义的内容,而没有像异常一样添加隐藏的退出点。例如,Rust函数可以返回一个Result<T, E>枚举,该枚举可以是Ok<T>Err<E>,并且T是您想要的类型(如果有),并且E是表示错误的类型。模式匹配和某些特定方法简化了成功和失败的处理。简而言之,不要认为缺少异常会自动意味着错误代码。
8bittree '16

1

除了查理的答案:

您在许多手册和书中看到的这些声明的异常处理示例仅在很小的示例上看起来非常聪明。

即使您忽略关于无效对象状态的争论,它们在处理大型应用程序时也会带来巨大的痛苦。

例如,当您必须使用某些加密技术处理IO时,可能有20种异常可能被排除在50种方法之外。想象一下您将需要多少异常处理代码。异常处理将花费比代码本身多几倍的代码。

实际上,您知道什么时候不会出现异常,并且您根本不需要编写太多的异常处理,因此您仅使用一些变通办法来忽略声明的异常。在我的实践中,只有大约5%的声明异常需要通过代码进行处理才能拥有可靠的应用程序。


好吧,实际上,这些异常通常只能在一个地方处理。例如,在“下载数据更新”功能中,如果SSL失败或DNS无法解析,或者Web服务器返回404,则没关系,将其捕获在顶部并向用户报告错误。
Zan Lynx
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.