我在这里的特定情况是用户可以将字符串传递到应用程序中,应用程序对其进行解析并将其分配给结构化对象。有时用户可能会输入无效的内容。例如,他们的输入可能描述一个人,但他们可能说他们的年龄是“苹果”。在这种情况下,正确的行为是回滚事务,并告诉用户发生了错误,他们将不得不再次尝试。可能需要报告我们在输入中发现的每个错误,而不仅仅是第一个。

在这种情况下,我认为我们应该抛出一个异常。他不同意,说:“例外应该是例外:应该预期用户可能会输入无效数据,所以这不是例外情况”,我真的不知道该如何辩驳,因为按照单词的定义,他似乎是正确的。

但是,据我了解,这就是为什么首先发明例外的原因。过去,您必须检查结果以查看是否发生错误。如果检查失败,可能会在您不注意的情况下发生不良情况。

无一例外,堆栈的每个级别都需要检查他们调用的方法的结果,如果程序员忘记检查这些级别之一,则代码可能会意外继续并保存无效数据(例如)。这种方式似乎更容易出错。

无论如何,请随时纠正我在这里所说的任何事情。我的主要问题是,如果有人说例外应该是例外,我怎么知道我的情况是否例外?

评论

可能重复吗?何时引发异常。尽管它在那里关闭,但我认为它适合这里。这仍然是一种哲学,一些人和社区倾向于将异常视为一种流控制。

用户哑巴时,他们提供的输入无效。当用户很聪明时,他们通过提供无效的输入进行游戏。因此,无效的用户输入也不例外。

另外,不要将异常(这是Java和.NET中的一种非常特殊的机制)与错误(它是更通用的术语)相混淆。错误处理比抛出异常要多得多。该讨论触及了异常与错误之间的细微差别。

“ Exceptional”!=“很少发生”

我发现埃里克·利珀特(Eric Lippert)的Vexing例外是不错的建议。

#1 楼

发明了异常可以使错误处理更容易,代码混乱更少。如果它们使错误处理更容易且代码混乱更少,则应使用它们。这种“仅在特殊情况下例外”的业务源于将异常处理视为不可接受的性能损失的时间。在绝大多数代码中已经不再是这种情况了,但是人们仍然不顾规则背后的原因而大肆宣传规则。在简化代码时使用异常应该不会感到不好。实际上,Java自己的Integer类无法检查字符串是否为有效整数,而又不会抛出NumberFormatException

此外,尽管您不能仅依靠UI验证,请记住,如果您的UI设计合理,例如使用微调器输入短数值,那么将非数值值真正地放入后端将是一个例外情况。

评论


轻扫一下,在那里。实际上,在我设计的真实应用程序中,性能上的影响确实有所不同,因此我不得不对其进行更改,以免某些解析操作抛出异常。

–罗伯特·哈维(Robert Harvey)
2013年1月24日17:22



我并不是说仍然没有出现性能下降是正当理由的情况,但是这些情况是例外(双关语意)而不是规则。

–卡尔·比勒费尔特(Karl Bielefeldt)
13年1月24日在17:38

@RobertHarvey Java中的窍门是抛出预制的异常对象,而不是抛出新的...。或者,抛出fillInStackTrace()被覆盖的自定义异常。这样一来,您就不会注意到任何性能下降,更不用说点击了。

– Ingo
13年1月24日在21:42

+1:完全是我要回答的问题。在简化代码时使用它。异常可以提供更清晰的代码,您不必费心检查调用堆栈中每个级别的返回值。 (不过,就像其他所有内容一样,如果使用错误的方式,它可能会使您的代码变得一团糟。)

–狮子座
13年1月25日在7:58

@Brendan假设发生某些异常,并且错误处理代码在调用堆栈中位于4个级别以下。如果使用错误代码,则处理程序上方的所有4个函数都需要将错误代码的类型作为其返回值,并且必须执行if(foo()== ERROR){return ERROR; }其他{//继续}。如果抛出未经检查的异常,则不会有嘈杂的冗余“ if error return error”。另外,如果将函数作为参数传递,则使用错误代码可能会将函数签名更改为不兼容的类型,即使可能不会发生错误也是如此。

–Doval
2014年3月25日19:52

#2 楼

什么时候应该引发异常?关于代码,我认为以下解释非常有帮助:

一个例外是,当成员未能完成其名称所指示的应执行的任务时。 (Jeffry Richter,通过C#进行CLR)

为什么有帮助?它建议,这取决于上下文何时应将某些内容作为异常来处理。在方法调用的级别上,上下文由(a)名称,(b)方法的签名和(b)使用或预期使用该方法的客户端代码给出。

要回答您的问题,您应该看一下处理用户输入的代码。可能看起来像这样:

public void Save(PersonData personData) { … }


方法名称是否暗示已完成某些验证?否。在这种情况下,无效的PersonData应该引发异常。

假设类具有另一个如下所示的方法:

public ValidationResult Validate(PersonData personData) { … }


方法名称是否暗示已完成某些验证?是。在这种情况下,无效的PersonData不应引发异常。

为了将它们放在一起,这两种方法都建议客户端代码应如下所示:

ValidationResult validationResult = personRegister.Validate(personData);
if (validationResult.IsValid())
{
    personRegister.Save(personData)
}
else
{
    // Throw an exception? To answer this look at the context!
    // That is: (a) Method name, (b) signature and
    // (c) where this method is (expected) to be used.
}


如果不清楚方法是否应该引发异常,则可能是由于方法名称或签名选择不当所致。也许班级的设计不清楚。有时,您需要修改代码设计,以明确回答是否应引发异常。

评论


就在昨天,我制作了一个名为“ ValidationResult”的结构,并按照您描述的方式构造了代码。

–保罗
2013年1月24日13:14



它无助于回答您的问题,但我只是想指出,您隐式或有意遵循了Command-query分离原则(en.wikipedia.org/wiki/Command-query_separation)。 ;-)

– Theo Lenndorff
13年1月24日在13:21

好主意!一个缺点:在您的示例中,验证实际上执行了两次:一次在Validate(验证无效时返回False),一次在Save(保存无效)时抛出一个经过详细记录的异常。当然,可以将验证结果缓存在对象内部,但这会增加额外的复杂性,因为验证结果需要在更改时失效。

–亨氏
13-10-21在9:27



@Heinzi,我同意。可以对其进行重构,以便在Save()方法内部调用Validate(),并且可以使用ValidationResult中的特定详细信息为异常构造适当的消息。

–菲尔
13年19月19日在13:53

这比我认为的公认答案更好。当呼叫无法执行应执行的操作时抛出。

–安迪
2015年10月29日,下午1:32

#3 楼


异常应该是例外的:预期用户可能
输入无效的数据,因此这不是例外情况。 >

预期文件可能不存在,所以这不是例外情况。
预期到服务器的连接可能会丢失,因此不会不是特殊情况
期望配置文件出现乱码,这不是特殊情况
预期您的请求有时会掉线,所以这不是特殊情况
/>
任何捕获到的异常,您都必须期待,因为您决定捕获它。因此,按照这种逻辑,您永远都不应抛出计划要捕获的任何异常。

因此,我认为“异常应该是例外”是一个可怕的经验法则。

您应该做什么取决于语言。对于何时应该引发异常,不同的语言有不同的约定。例如,Python会引发所有事件的异常,而在Python中,我也会效仿。另一方面,C ++抛出的异常相对较少,我也照常这样做。您可以像对待Python一样对待C ++或Java,并为所有内容抛出异常,但是您在使用该语言时期望使用的语言不一致。将其他语言塞入鞋拔子。

评论


@gnat,我知道。我的观点是,即使不是您最喜欢的语言,您也应该遵循该语言的约定(在本例中为Java)。

–温斯顿·埃韦特(Winston Ewert)
13年1月24日在15:18

+1“例外应该是特殊的”是一个可怕的经验法则。说得好!这就是人们不考虑而重复的事情之一。

– Andres F.
13年1月24日在22:51



“预期”不是由主观论据或约定定义的,而是由API /功能的约定定义的(可能是明确的,但通常只是隐含的)。不同的功能/ API /子系统可能会有不同的期望,例如对于某些更高级别的功能,不希望存在的文件可能会被处理(可能会通过GUI将其报告给用户),对于其他更低级别的功能则可能不是(因此应该抛出异常)。这个答案似乎错过了这一重点。

– mikera
13-10-21在6:05

@mikera,是的,函数应该(仅)抛出其合同中定义的异常。但这不是问题。问题是您如何决定该合同应该是什么。我认为,“例外应该是例外”的经验法则对做出该决定没有帮助。

–温斯顿·埃韦特(Winston Ewert)
13年11月21日在5:58

@supercat,我认为这并不重要,最终变得更加普遍。我认为关键问题是有一个合理的默认值。如果我没有明确处理错误情况,我的代码会假装什么也没有发生,还是得到有用的错误消息?

–温斯顿·埃韦特(Winston Ewert)
2015年4月21日在4:34

#4 楼

考虑异常时,我总是会想到访问数据库服务器或Web API之类的事情。您希望服务器/ Web API能够正常工作,但在特殊情况下可能无法(服务器已关闭)。 Web请求通常可能很快,但是在特殊情况下(高负载)可能会超时。这是您无法控制的。

您可以控制用户的输入数据,因为您可以检查他们发送的内容并按自己喜欢的方式进行处理。在您的情况下,我会在尝试保存用户输入之前对其进行验证。而且我倾向于同意应该期望用户提供无效数据,并且您的应用应该通过验证输入并提供用户友好的错误消息来解决此问题。

我确实在我的大多数域模型设置员中,绝对没有机会输入无效数据。但是,这是最后一道防线,我倾向于使用丰富的验证规则来构建输入表单,因此几乎没有机会触发该域模型异常。因此,当二传手期待一件事情,而又得到另一件事情时,这是一种例外情况,在通常情况下不应该发生。

编辑(需要考虑的其他事情):

将用户提供的数据发送到数据库时,您事先知道应该和不应该输入表中的内容。这意味着可以根据某些预期格式验证数据。这是您可以控制的。
您无法控制的是服务器在查询过程中出现故障。因此,您知道查询是可以的,并且数据已经过过滤/验证,您尝试查询后仍然失败,这是一种例外情况。

与Web请求类似,您不知道请求将超时,或者无法连接,然后再尝试发送。因此,这也保证了try / catch方法,因为当您发送请求时,您不能在几毫秒后询问服务器它是否可以工作。

评论


但为什么?为什么异常在处理更多预期的问题时没那么有用?

–温斯顿·埃韦特(Winston Ewert)
13年1月24日在14:59

@Pinetree,在打开文件之前检查文件是否存在是个坏主意。该文件在检查和打开之间可能不再存在,该文件没有权限让您打开它,并且检查是否存在然后打开文件将需要两个昂贵的系统调用。您最好先尝试打开文件,然后再处理失败。

–温斯顿·埃韦特(Winston Ewert)
2013年1月24日15:20

据我所知,几乎所有可能的故障都可以通过从故障中恢复而进行处理,而不是尝试事先检查是否成功。是否使用异常或其他指示失败的问题是一个单独的问题。我更喜欢例外,因为我不会偶然忽略它们。

–温斯顿·埃韦特(Winston Ewert)
13年1月24日在15:23

我不同意您的假设,因为预期会有无效的用户数据,所以不能认为该数据是例外的。如果我编写了一个解析器,并且有人提供了无法解析的数据,那是一个例外。我无法继续解析。异常的处理方式完全是另一个问题。

–ConditionRacer
2013年1月24日17:40



如果输入错误(例如,不存在的文件名),则File.ReadAllBytes将抛出FileNotFoundException。那是威胁该错误的唯一有效方法,在不导致返回错误代码的情况下还能做什么?

–oɔɯǝɹ
13年1月24日在21:49

#5 楼

参考

从程序员修炼:


我们认为,异常应该很少被用作程序的正常流程的一部分;应该为意外事件保留异常。假定未捕获的异常将终止您的程序,并问自己:“如果删除所有异常处理程序,该代码是否还会运行?”如果答案为“否”,则可能是在非异常情况下使用了异常。


他们继续研究打开文件进行读取的示例,但该文件没有存在-应该引发异常吗?


如果文件应该存在,那么就可以保证有异常。 [...]另一方面,如果您不知道该文件是否应该存在,那么找不到它似乎也不例外,并且错误返回是适当的。
<后来,他们讨论了为什么选择这种方法:


[A] n异常表示控制权的即时,非本地转移-这是goto的一种级联。使用异常作为其正常处理的一部分的程序会遭受经典意大利面条代码的所有可读性和可维护性问题。这些程序破坏了封装:例程和它们的调用者通过异常处理更紧密地耦合。


关于您的情况

您的问题归结为“应该引发验证错误吗?例外?”答案是,这取决于验证发生的位置。

如果有问题的方法在代码的一部分内,假设已验证输入数据,则无效的输入数据应引发异常;如果将代码设计为使得此方法将接收用户输入的确切输入,则应预期会出现无效数据,并且不应引发异常。

#6 楼

这里有很多哲学上的要求,但是通常来说,特殊条件就是那些没有用户干预就无法或不希望处理的条件(除了清理,错误报告等)。换句话说,它们是不可恢复的条件。

如果您给程序提供一个文件路径,目的是以某种方式处理该文件,并且该路径指定的文件不存在,那就是特殊情况。除了向用户报告并允许他们指定其他文件路径外,您无法在代码中对此做任何事情。

评论


+1,非常接近我要说的内容。我想说的是范围,与用户无关。一个很好的例子是两个.Net函数int.Parse和int.TryParse之间的区别,前者别无选择,只能在输入错误时抛出异常,而后者则永远不要抛出异常

– jmoreno
13年1月24日在16:25

@jmoreno:不好意思,当代码可以对不可解析的条件执行某些操作时,可以使用TryParse,而在无法解析的条件下可以使用Parse。

–罗伯特·哈维(Robert Harvey)
13年1月24日在16:26

#7 楼

您应该考虑两个问题:


,您讨论一个问题-称其为Assigner,因为该问题是将输入分配给结构化对象-并表达了其输入为有效的
实现良好的用户界面还有一个附加问题:验证用户输入和对错误的建设性反馈(将其称为Validator

,因此抛出异常是完全合理的,因为您已经表达了已违反的约束。

从用户体验的角度来看,用户不应直接与Assigner第一名。他们应该通过Assigner与之对话。

现在,在Validator中,无效的用户输入并不是一个例外情况,这确实是您更感兴趣的情况。因此,这里的异常不会适当,这也是您要识别所有错误而不是首先解决的地方。

您会注意到我没有提到如何实现这些担忧。看来您在说Validator,而您的同事在说综合Assigner。一旦意识到存在两个单独(或可分离)的问题,至少您可以明智地进行讨论。


为了回答Renan的评论,我只是假设,一旦您确定了两个独立的问题,很明显在每种情况下都应考虑将哪种情况视为例外。

实际上,如果尚不清楚是否应该考虑某些例外情况,我认为您可能还没有完成解决方案中独立问题的识别。

我想这使得直接答案


...我怎么知道我的案子是否特殊?


继续简化直到明了。
当您拥有一堆简单的概念时,您就会很好地理解,可以将它们重新组合成代码,类,库或任何其他内容。

评论


-1是的,有两个问题,但这不能回答“我怎么知道我的案子是否特殊?”的问题。

–RMalke
13年1月24日,11:45

关键是同一情况在一种情况下可能是例外,而在另一种情况下可能不会例外。确定您实际上在谈论的上下文(而不是将二者混为一谈)在这里回答了这个问题。

–没用
13年1月24日,11:47



...实际上,也许不是-我已经在回答中解决了您的观点。

–没用
13年1月24日在11:56

#8 楼

其他人的回答很好,但这仍然是我的简短回答。例外情况是环境中的某些问题出了错,您无法控制并且代码根本无法前进。在这种情况下,您还必须告知用户出了什么问题,为什么不能继续进行以及解决了什么问题。

#9 楼

我从来都不是忠告的忠实拥护者,只有在特殊情况下才应该抛出异常,部分原因是它什么也没说(就像说您应该只吃可食用的食物一样),还因为是非常主观的,并且常常不清楚什么是例外情况,什么不是例外情况。

但是,此建议有充分的理由:抛出和捕获异常很慢,并且如果正在运行您在Visual Studio中的调试器中将代码设置为在引发异常时通知您,因此在解决问题之前很长时间,您可能最终被数十甚至数百条消息所淹没。

通常,如果:


您的代码没有错误,并且
所依赖的服务全部可用,并且
您的用户正在使用您的程序以预期的方式使用(即使它们提供的某些输入无效)

,那么您的代码应永远不会引发异常,ev恩,后来被抓到。要捕获无效数据,可以在UI级别使用验证器,也可以在表示层中使用诸如Int32.TryParse()之类的代码。

对于其他任何事情,您都应坚持以下原则:异常意味着您的方法不能照其名称所说做。通常,使用返回代码指示失败不是一个好主意(除非您的方法名称明确表明已这样做,例如TryParse()),其原因有两个。首先,对错误代码的默认响应是忽略错误条件并继续执行;其次,您可能很容易以使用返回码的某些方法和使用异常的其他方法结束,而忘记了哪个方法。我什至见过代码库,其中同一接口的两个不同的可互换实现在这里采用不同的方法。

#10 楼

异常应该表示条件,即使调用方法可以,也可能不准备立即调用代码来处理。例如,考虑正在从文件中读取某些数据的代码,可以合法地假定任何有效文件都将以有效记录结尾,并且不需要从部分记录中提取任何信息。

如果读取数据例程不使用异常,而只是报告读取是否成功,则调用代码必须看起来像:

temp = dataSource.readInteger();
if (temp == null) return null;
field1 = (int)temp;
temp = dataSource.readInteger();
if (temp == null) return null;
field2 = (int)temp;
temp = dataSource.readString();
if (temp == null) return null;
field3 = temp;


等。每个有用的工作都要花三行代码。相比之下,如果readInteger在遇到文件结尾时将引发异常,并且如果调用方可以简单地传递异常,则代码将变为:

field1 = dataSource.readInteger();
field2 = dataSource.readInteger();
field3 = dataSource.readString();


看起来更加简单和整洁,并且更加注重正常工作的情况。注意,在直接调用者希望处理一个条件的情况下,返回错误代码的方法通常比抛出异常的方法更有帮助。例如,将一个文件中的所有整数求和:

do
{
  temp = dataSource.tryReadInteger();
  if (temp == null) break;
  total += (int)temp;
} while(true);





try
{
  do
  {
    total += (int)dataSource.readInteger();
  }
  while(true);
}
catch endOfDataSourceException ex
{ // Don't do anything, since this is an expected condition (eventually)
}


要求整数,期望这些调用之一将失败。让代码使用一个无限循环,直到发生这种情况,这要比使用一种通过返回值指示失败的方法要优雅得多。

因为类通常不知道客户会期望或不期望的条件,所以提供两种版本的方法可能会有所帮助,这两种方法可能会以某些调用者期望的方式失败,而另一些调用者不会。这样做将使这两种类型的调用方都能干净地使用这些方法。还要注意,如果情况出现,甚至调用者可能没想到的话,即使是“ try”方法也应该抛出异常。例如,如果tryReadInteger遇到干净的文件结束条件,则它不应引发异常(如果调用方不希望这样做,则调用方将使用readInteger)。另一方面,如果由于例如以下原因而无法读取数据,则可能应该抛出异常。拔出包含它的记忆棒。尽管应该始终将此类事件视为可能,但立即调用代码不太可能准备做任何有用的响应。当然,不应以与文件结束条件相同的方式来报告它。

#11 楼

编写软件中最重要的事情是使其具有可读性。所有其他考虑因素都是次要的,包括使其高效并使其正确。如果可读,其余的可以在维护中处理,如果不可读,那么最好扔掉它。因此,当它提高可读性时,您应该抛出异常。

在编写算法时,请考虑一下将来将要阅读它的人。当您来到可能存在潜在问题的地方时,问问自己读者是否想看看您现在如何解决该问题,还是读者更喜欢只继续学习算法?

我喜欢想到巧克力蛋糕的食谱。当它告诉您添加鸡蛋时,它有一个选择:可以假设您已经鸡蛋,然后继续食谱,或者可以开始解释如果没有鸡蛋,如何获得鸡蛋。它可以用整本书来介绍狩猎野鸡的技巧,所有这些都可以帮助您烤蛋糕。很好,但是大多数人都不想阅读该食谱。大多数人宁愿只是假设有鸡蛋,然后继续做菜。这是作者在编写食谱时需要做出的判断。

关于什么是好的例外以及应立即处理的问题,没有任何保证的规则,因为这需要您阅读读者的思想。您将要做的最好的事情就是经验法则,“例外仅在特殊情况下”是一个相当不错的规则。通常,当读者阅读您的方法时,他们正在寻找该方法将在99%的时间内完成的工作,而他们宁愿不要将这些混乱的情况弄得一团糟,例如处理用户输入非法输入和几乎从未发生过的其他事情。他们想直接看到您的软件的正常流程,一个接一个的指令仿佛从未发生过问题。理解您的程序将变得非常困难,而不必处理不断切线的问题以处理可能出现的每个小问题。

#12 楼


可能需要报告我们在输入中发现的每个错误,而不仅仅是第一个错误。


这就是为什么您不能在此处引发异常。异常立即中断验证过程。因此,要解决此问题将有很多变通办法。

一个不好的例子:

Dog类的验证方法使用异常:

void validate(Set<DogValidationException> previousExceptions) {
    if (!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        DogValidationException disallowedName = new DogValidationException(Problem.DISALLOWED_DOG_NAME);
        if (!previousExceptions.contains(disallowedName)){
            throw disallowedName;
        }
    }
    if (this.legs < 4) {
        DogValidationException invalidDog = new DogValidationException(Problem.LITERALLY_INVALID_DOG);
        if (!previousExceptions.contains(invalidDog)){
            throw invalidDog;
        }
    }
    // etc.
}


如何调用:

Set<DogValidationException> exceptions = new HashSet<DogValidationException>();
boolean retry;
do {
    retry = false;
    try {
        dog.validate(exceptions);
    } catch (DogValidationException e) {
        exceptions.add(e);
        retry = true;
    }
} while (retry);

if(exceptions.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}


这里的问题是验证过程要获取所有错误,将需要跳过已找到的例外。上面的方法可能有效,但这显然是对异常的滥用。要求您进行的那种验证应在接触数据库之前进行。因此,无需回滚任何东西。并且,验证的结果很可能是验证错误(虽然希望为零)。

更好的方法是:

方法调用:

Set<Problem> validationResults = dog.validate();
if(validationResults.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}


验证方法:

Set<Problem> validate() {
    Set<Problem> result = new HashSet<Problem>();
    if(!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        result.add(Problem.DISALLOWED_DOG_NAME);
    }
    if(this.legs < 4) {
        result.add(Problem.LITERALLY_INVALID_DOG);
    }
    // etc.
    return result;
}


为什么?原因很多,其他答复中也指出了大多数原因。简而言之:他人阅读和理解要简单得多。其次,您是否要向用户显示堆栈跟踪信息以说明他错误地设置了他的dog

如果在第二个示例的提交过程中仍然出现错误,即使您的验证程序验证了dog的问题为零,然后抛出异常是正确的事情。像:没有数据库连接,数据库条目同时被其他人修改,或者类似。

评论


很好的答案。我想补充一点,有时候事情并非如此简单。考虑一种情况,其中修改狗仅是必须作为一个原子更改应用于数据库的较大逻辑更改的一部分。在这种情况下,这里可能会通过validate()成功完成,并且由于某些其他对象随后无法通过验证,因此仍然需要回滚。一个好的框架将支持对参与事务的所有对象进行验证,然后对所有对象进行另一轮提交更改。

– Mikko Rantalainen
20年5月3日,8:50