问题:
什么被认为是“最佳实践” - 以及为什么 - 处理构造函数中的错误?
“最佳实践”可以是Schwartz的引用,或50%的CPAN模块使用它等等;但我对任何人的合理意见感到满意,即使它解释了为什么通用的最佳实践并不是最好的方法。
就我自己对该主题的看法(通过Perl中的软件开发多年来了解),我已经看到了perl模块中错误处理的三种主要方法(在我看来从最好到最差列出):
构造一个对象,设置一个无效的标志(通常是“is_valid
”方法)。通常通过类的错误处理与设置错误消息相结合。
专业人士:
允许标准(与其他方法调用相比)错误处理,因为它允许在错误的构造函数之后使用$obj->errors()
类型调用,就像在任何其他方法调用之后一样。
允许传递其他信息(例如> 1错误,警告等...)
允许轻量级的“redo”/“fixme”功能,换句话说,如果构造的对象非常繁重,许多复杂的属性100%总是正常的,唯一的原因就是它没有有效是因为有人输入了错误的日期,您只需执行“$obj->setDate()
”而不是再次重新执行整个构造函数的开销。这种模式并不总是需要,但在正确的设计中非常有用。
缺点:我不知道。
返回“undef
”。
缺点:无法实现第一个解决方案的任何优点(全局变量之外的每个对象错误消息和重型对象的轻量级“fixme”功能)。
在构造函数中死掉。在一些非常狭隘的边缘情况之外,我个人认为这是一个可怕的选择,因为有太多理由在这个问题的边缘列出。
更新:为了清楚起见,我认为(非常有价值和一个伟大的设计)解决方案有一个非常简单的构造函数,它根本不会失败,而且是一个繁重的初始化方法,其中所有错误检查都发生在为了这个问题的目的,它只是案例#1(如果初始化器设置错误标志)或案例#3(如果初始化器死亡)的子集。显然,选择这样的设计,你会自动拒绝选项#2。
答案 0 :(得分:7)
这取决于您希望构造函数的行为方式。
其余的回应都反映在我的个人观察中,但与Perl的大多数事情一样,最佳实践真的归结为“这是一种方法,你可以根据自己的需要采取或离开。”您描述的偏好完全有效且一致,没有人应该告诉您。
如果构造失败,我实际上更喜欢死亡,因为我们设置它以便在对象构造期间可能发生的唯一类型的错误确实是大的,明显的错误应该停止执行。
另一方面,如果您不希望这种情况发生,我认为我更喜欢2超过1,因为检查一个未定义的对象就像检查一些标志变量一样容易。这不是C,所以我们没有强类型约束告诉我们我们的构造函数必须返回这种类型的对象。因此,返回undef
并检查确定成功或失败是一个很好的选择。
构造失败的“开销”是某些边缘情况下的考虑因素(在发生开销之前你不能快速失败),所以对于那些你可能更喜欢方法1.所以再次,它取决于你的语义为对象构造定义。例如,我更喜欢在构造之外进行重量级初始化。至于标准化,我认为检查构造函数是否返回已定义的对象与检查标志变量一样好。
编辑:为了响应您对初始化程序拒绝情况#2的编辑,我不明白为什么初始化程序不能简单地返回指示成功或失败的值而不是设置标志变量。实际上,您可能希望同时使用两者,具体取决于您对发生的错误的详细程度。但是初始化程序在成功时返回true并且在失败时返回undef
将是完全有效的。
答案 1 :(得分:5)
我更喜欢:
croak
在出现问题时提供信息性消息。此外,如果班级的用户可能不关心失败发生的原因,只有他们获得了有效的对象,那么返回undef
(而不是呱呱叫)是好的。
我鄙视容易忘记is_valid
方法或添加额外的检查以确保在未明确定义对象的内部状态时不调用方法。
我从一个非常主观的角度说这些,而没有对最佳实践做出任何陈述。
答案 2 :(得分:5)
我建议反对#1,因为它会导致更多错误处理代码,而这些代码将无法写入。例如,如果你只是返回false,那么这很好。
my $obj = Class->new or die "Construction failed...";
但是如果你返回一个无效的对象......
my $obj = Class->new;
die "Construction failed @{[ $obj->error_message ]}" if $obj->is_valid;
随着错误处理代码的数量增加,写入概率降低。而且它不是线性的。通过增加错误处理系统的复杂性,您实际上可以减少实际使用中会遇到的错误数量。
在调用任何方法时(除is_valid
和error_message
之外),您还必须小心,您的无效对象会死亡,从而导致更多代码和错误机会。
但我同意能够获得有关失败的信息是有价值的,这会导致返回错误(仅return
而非return undef
)。传统上,这是通过调用DBI中的类方法或全局变量来完成的。
我的$ dbh = DBI-> connect($ data_source,$ username,$ password) 或者死掉$ DBI :: errstr;
但是它会受到A)的影响,你仍然需要编写错误处理代码,B)它只对最后一次操作有效。
通常,最好的办法是使用croak
抛出异常。现在在正常情况下,用户没有编写特殊代码,错误发生在问题点,并且默认情况下它们会收到一条好的错误消息。
my $obj = Class->new;
Perl的传统建议反对将图书馆代码中的异常抛弃为不礼貌,这已经过时了。 Perl程序员(最终)接受异常。而不是一次又一次地编写错误处理代码,严重且经常忘记,例外DWIM。如果您不相信只是开始使用autodie(watch pjf's video about it),那么您将永远不会回去。
异常将霍夫曼编码与实际使用对齐。期望构造函数正常工作并且如果不存在则需要错误的常见情况现在是最少的代码。想要处理该错误的不常见情况需要编写特殊代码。特殊代码非常小。
my $obj = eval { Class->new } or do { something else };
如果你发现自己在eval
中打包每一个电话,那你就错了。之所以称为例外,因为它们是例外的。如果你在上面的评论中想要为用户的利益进行优雅的错误处理,那么请利用错误在堆栈中冒出来的事实。例如,如果您想提供一个不错的用户错误页面并记录错误,您可以这样做:
eval {
run_the_main_web_code();
} or do {
log_the_error($@);
print_the_pretty_error_page;
};
你只需要在一个地方,在你的电话堆栈顶部,而不是分散在任何地方。您可以以较小的增量利用此功能,例如......
my $users = eval { Users->search({ name => $name }) } or do {
...handle an error while finding a user...
};
有两件事正在发生。 1)Users->search
总是返回一个真值,在本例中是一个数组引用。这使得简单的my $obj = eval { Class->method } or do
起作用。这是可选的。但更重要的是2)你只需要在Users->search
周围进行特殊的错误处理。调用Users->search
内部的所有方法以及它们调用的所有方法......它们只是抛出异常。而且他们都被抓到了一点并处理了同样的事情。在关注它的点处理异常会使得更简洁,更紧凑和灵活的错误处理代码。
您可以通过{{1}使用字符串重载对象而不仅仅是字符串将更多信息打包到异常中。
croak
例外:
Try::Tiny等模块修复了使用my $obj = eval { Class->new }
or die "Construction failed: $@ and there were @{[ $@->num_frobnitz ]} frobnitzes";
作为异常处理程序的大多数悬而未决的问题。
至于你的用例,你可能有一个非常昂贵的对象,并希望尝试继续部分构建......闻起来像YAGNI给我。你真的需要它吗?或者你有一个膨胀的对象设计太早做了太多的工作。如果确实需要,可以在异常对象中放置继续构造所需的信息。
答案 3 :(得分:2)
首先是浮夸的一般观察:
return undef;
总是不好[1] 现在谈到实际的问题,我将解释为“你做什么,做什么,考虑最好的做法和为什么”。首先,我会注意到,在失败时返回错误的值有很长的Perl历史(例如,大部分核心都是这样工作的),并且很多模块都遵循这个约定。然而,事实证明,这种约定产生了较差的客户端代码,而较新的模块正在逐渐远离它。[2]
[支持参数和代码示例对于导致创建autodie的异常更为一般,因此我将抵制在此处提出这种情况的诱惑。代替:]
检查成功创建实际上更多比在适当的异常处理级别检查异常更麻烦。其他解决方案要求直接客户端完成比仅获取对象所需的更多工作,当构造函数因抛出异常而失败时,不需要这项工作。[3]异常比undef
更具表现力,同样具有表达性,如为了记录错误并在调用堆栈中的各个级别注释它们而传回一个破坏的对象。
如果在异常中将其传回,您甚至可以获取部分构造的对象。我认为,根据我对构造者与其客户的合同应该是什么的看法,这是一种不好的做法,但行为得到了支持。笨拙。
所以:无法创建有效对象的构造函数应该尽早抛出异常。构造函数可以抛出的异常应该是其接口的文档部分。只有能够对异常有意义的调用级别才能查找它;很多时候,“如果这种结构失败,不做任何事情”的行为是完全正确的。
[1]:我的意思是,我不知道return;
并非严格优越的任何用例。如果有人打电话给我,我可能不得不实际打开一个问题。所以请不要。 ;)
[2]:根据我在过去两年中阅读的模块接口的非常不科学的回忆,受选择和确认偏差的影响。
[3]:请注意,抛出异常仍然需要错误处理,其他提议的解决方案也是如此。这并不意味着将每个实例化包装在eval
中,除非您实际上想要围绕每个构造进行复杂的错误处理(如果您认为这样做,那么您可能错了)。这意味着包装能够在eval
中有意义地处理异常的调用。