空保护每个取消引用的指针是否合理?


65

在一项新工作中,我在代码审查中被标记为这样的代码:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender) { }

void PowerManager::SignalShutdown()
{
    msgSender_->sendMsg("shutdown()");
}

有人告诉我最后一个方法应为:

void PowerManager::SignalShutdown()
{
    if (msgSender_) {
        msgSender_->sendMsg("shutdown()");
    }
}

即,即使它是私有数据成员,我也必须对它进行NULL保护msgSender_。我很难克制自己,不能用粗话来形容我对这块“智慧”的感觉。当我要求解释时,我听到一系列恐怖故事,这些故事是关于某位初级程序员如何在某年内对类应该如何工作感到困惑的,并意外删除了一个他本不应该拥有的成员(然后将其设置为NULL之后) (显然)),并且在产品发布后,事情就在现场爆发。我们已经“学到了硬道理,相信我们”,最好NULL检查一下所有内容

在我看来,这就像简单而简单的货物崇拜编程。一些好心的同事正在认真地尝试帮助我“了解它”,并了解这将如何帮助我编写更强大的代码,但是...我忍不住觉得自己是不了解它的人。

编码标准要求首先检查函数中解引用的每个指针NULL(甚至是私有数据成员)是否合理?(注意:为提供背景信息,我们生产的是消费类电子设备,而不是空中交通管制系统或其他“故障等同人员死亡”的产品。)

编辑:在上面的示例中,msgSender_协作者不是可选的。如果是NULL,则表明存在错误。传递给构造函数的唯一原因是PowerManager可以使用模拟IMsgSender子类进行测试。

简介:对此大家有一些非常好的答案,谢谢大家。我接受@aaronps的邮件是因为它简短。似乎有相当广泛的普遍同意:

  1. 强制规定NULL看守每一个复引用指针是矫枉过正,但
  2. 您可以通过使用参考(如果可能)或const指针来避开整个辩论,并且
  3. assert语句是NULL警卫人员的更开明的选择,用于验证是否满足函数的前提条件。

14
暂时不要认为错误是初级程序员的领域。各种经验水平的开发人员中有95%的人偶尔会觉得有些奇怪。其余的5%则躺在牙齿上。
Blrfl 2013年

56
如果我看到有人编写代码以检查是否为空并且无提示地失败了,那么我想当场解雇他们。
Winston Ewert

16
无声的失败几乎是最糟糕的。至少在爆炸时,您知道爆炸发生的位置。检查null和不执行任何操作只是将错误转移到执行过程中的一种方法,这使得追溯到源代码变得更加困难。
MirroredFate

1
+1,非常有趣的问题。除了我的回答外,我还指出,当您不得不编写旨在容纳单元测试的代码时,您真正要做的是将增加的风险换成错误的安全感(更多行代码=更多错误的可能性)。重新设计为使用引用不仅可以提高代码的可读性,而且可以减少为了证明代码有效而必须编写的代码(和测试)数量。
赛斯

6
@Rig,我敢肯定有适当的情况发生了失败的失败。但是,如果有人把沉默一般的做法失败的(除非它是有意义一种境界的工作),我真的不希望被工作他们把代码中的一个项目。
温斯顿·尤尔特

Answers:


66

这取决于“合同”:

如果PowerManager 必须具有有效值IMsgSender,请不要检查是否为null,请让它尽快消失。

另一方面,如果它可能有一个IMsgSender,那么您每次使用时都需要检查,就这么简单。

关于初级程序员故事的最终评论,问题实际上是缺乏测试程序。


4
+1是一个非常简洁的答案,几乎可以总结为我的感觉:“取决于...”。您还敢说“从不检查null”(如果 null无效)。assert()在每个取消引用指针的成员函数中放置一个折衷方案,这可能会使许多人满意,但即使那样对我来说也感觉像是“投机性偏执狂”。我无法控制的事情清单是无限的,一旦我让自己开始担心它们,它就会变得非常滑溜。学会信任我的测试,我的同事以及调试器帮助我在晚上睡得更好。
evadeflow

2
当您阅读代码时,这些断言对于理解代码应该如何工作非常有帮助。我从来没有喜欢过让我走的代码库,“我希望它没有所有这些断言。”但是我在相反的地方看到了很多。
克里斯托弗·普罗沃斯特

1
简单明了,这是对问题的正确答案。
马修·阿兹基莫夫

2
这是最接近的正确答案,但我不同意“从不检查null”,始终在将无效参数分配给成员变量时检查无效参数。
詹姆斯

感谢您的评论。如果有人不读该规范并在必须包含有效值的情况下使用空值对其进行初始化,则最好让它早点崩溃。
aaronps

77

我觉得代码应显示为:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}

实际上,这比保护NULL更好,因为它非常清楚,如果msgSender_为NULL ,则永远不要调用该函数。它还可以确保您会注意到这种情况。

您同事的共同“智慧”将默默地忽略此错误,并产生不可预测的结果。

通常,如果更容易发现错误,则更容易修复错误。在此示例中,建议的NULL保护将导致未设置关闭消息,这可能会或可能不会导致明显的错误。SignalShutdown与整个应用程序刚刚死掉相比,向后工作该函数将花费更多的时间,从而产生方便的回溯跟踪或直接指向的核心转储SignalShutdown()

这有点违反直觉,但是一旦出现任何错误就会崩溃,这会使您的代码更加健壮。那是因为您实际上发现了问题,并且往往也有非常明显的原因。


10
调用assert和OP的代码示例之间的最大区别是,assert仅在调试版本中启用(#define NDEBUG)当产品进入客户手中,并且禁用了debug,并且执行了代码源的永不过时的组合后,离开即使所讨论的函数已执行,指针也为null,而assert无法为您提供保护。
2013年

17
大概您在实际将构建发送给客户之前就已经对其进行了测试,但是是的,这可能会带来风险。不过,解决方案很容易:要么在启用断言的情况下发货(无论如何,这就是您测试的版本),要么将其替换为自己的宏,该宏在调试模式下进行断言,而在发布模式下仅记录,日志+回溯,退出...。
克里斯托弗·普罗沃斯特

7
@James-在90%的情况下,您也不会从失败的NULL检查中恢复。但是在这种情况下,代码将无提示地失败,并且错误可能会在稍后出现-例如,您以为您已保存到文件,但实际上空检查有时会失败,从而使您变得不明智。您无法从所有不可能发生的情况中恢复过来。您可以验证它们不会发生,但是除非您为NASA卫星或核电站编写代码,否则我认为您没有预算。您确实需要有一种说法:“我不知道发生了什么-该程序不应该处于这种状态”。
Maciej Piechotka

6
@詹姆斯我不同意投掷。这使得它看起来像是实际可能发生的事情。断言是为了那些不可能的事情。换句话说,对于代码中的实际错误。异常是指意外情况,但有可能发生的情况。异常是您可以想象得到并从中恢复的事情。无法恢复的代码中的逻辑错误,也不应该尝试。
克里斯托夫·普罗沃斯特

2
@James:根据我在嵌入式系统上的经验,软件和硬件的设计都是不变的,因此很难使设备完全无法使用。在大多数情况下,如果软件停止运行,则有一个硬件监视程序可强制完全重置设备。
Bart van Ingen Schenau,

30

如果msgSender 一定不能null,则应null仅将检查放在构造函数中。对于类中的任何其他输入也是如此-将完整性检查放在“模块”(类,函数等)的入口处。

我的经验法则是在模块边界(在这种情况下为类)之间执行完整性检查。另外,一个类应该足够小,以便可以快速地从心理上验证该类成员的生命周期的完整性-确保避免诸如错误的删除/空分配之类的错误。在您帖子中的代码中执行的空检查假定所有无效用法实际上将空值分配给了指针-并非总是如此。由于“无效用法”从本质上隐含着对常规代码的任何假设均不适用,因此我们不能确保捕获所有类型的指针错误-例如无效的删除,递增等。

另外-如果您确定参数永远不能为null,请根据类的使用情况考虑使用引用。否则,请考虑使用std::unique_ptrstd::shared_ptr代替原始指针。


11

不,检查指针的每个取消引用是不合理的NULL

空指针检查对于确保满足先决条件或在未提供可选参数的情况下采取适当的措施对函数参数(包括构造函数参数)很有价值,并且在您暴露类的内部信息之后检查类的不变性也很有价值。但是,如果指针变为NULL的唯一原因是存在错误,则检查没有任何意义。该错误很容易将指针设置为另一个无效值。

如果我遇到像您这样的情况,我会问两个问题:

  • 为什么在发布之前没有发现初级程序员的错误?例如在代码审查还是在测试阶段?将有故障的消费电子设备推向市场可能与发布安全关键型应用中使用的有故障设备一样昂贵(如果考虑到市场份额和商誉),因此我希望公司认真对待测试和其他质量检查活动。
  • 如果null检查失败,您希望我执行什么错误处理,还是我可以写assert(msgSender_)而不是null检查?如果仅执行空值检查,则可能避免了崩溃,但可能造成了更糟糕的情况,因为该软件会在执行某项操作的前提下继续运行,而实际上该操作已被跳过。这可能导致软件的其他部分变得不稳定。

9

该示例似乎与对象生存期有关,而不是输入参数是否为null†。由于您提到PowerManager必须始终具有有效值IMsgSender,因此通过指针传递参数(从而允许使用空指针)使我感到设计上的缺陷††。

在这种情况下,我希望更改接口,以便通过以下语言来强制调用者的要求:

PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}

用这种方式重写表示整个生命周期都PowerManager需要保留对其的引用IMsgSender。反过来,这也建立了一个隐含的要求,该隐含的要求的IMsgSender生存期必须大于PowerManager,并且不需要对内部的任何空指针检查或断言进行操作PowerManager

您还可以使用智能指针(通过boost或c ++ 11)编写相同的内容,以显式强制IMsgSender其寿命超过PowerManager

PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}

如果有可能IMsgSender无法保证的寿命比的寿命长PowerManager(即),则首选此方法x = new IMsgSender(); p = new PowerManager(*x);

†关于指针:猖null的空检查使代码更难阅读,并且不能提高稳定性(它会改善稳定性的外观,这更糟)。

在某个地方,有人获得了用于保存内存的地址IMsgSender。该函数的责任是确保分配成功(检查库返回值或正确处理std::bad_alloc异常),以免传递无效的指针。

由于PowerManager不拥有IMsgSender(只是借用了一段时间),因此它不负责分配或破坏该内存。这是我更喜欢该参考文献的另一个原因。

††由于您是新手,所以我希望您能使用现有的代码。因此,所谓设计缺陷,是指该缺陷存在于您正在使用的代码中。因此,那些因为不检查空指针而标记您的代码的人实际上是在标记自己编写需要指针的代码:)


1
有时使用原始指针实际上没有任何问题。Std :: shared_ptr不是灵丹妙药,并且在参数列表中使用它可以解决草率的工程,尽管我意识到在任何可能的情况下都可以使用它。如果您愿意,请使用Java。
詹姆斯

2
我并不是说原始指针不好!他们肯定有自己的位置。然而,这是Ç + +,参考(和共享指针,现在)是有原因的语言的一部分。使用它们,它们将使您成功。
赛斯

我喜欢智能指针的想法,但这在特定的演出中不是一个选择。+1推荐使用引用(例如@Max和其他一些引用)。有时候这是不可能注入的参考,不过,由于鸡还是先有蛋的依赖问题,即,如果一个对象,否则将用于注射的候选人需要有一个指针(或引用)其持有注入到。有时这可能表明设计紧密耦合。但是它通常是可以接受的,例如在对象与其迭代器之间的关系中,在没有迭代器的情况下使用后者就没有任何意义。
evadeflow 2013年

8

像异常一样,保护条件仅在您知道从错误中恢复时或要给出更有意义的异常消息时才有用。

吞下一个错误(无论是作为异常还是作为防护检查而捕获),仅在错误无关紧要时才应该做。对于我来说,最常见的错误被吞噬的地方是错误记录代码-您不想因为无法记录状态消息而使应用程序崩溃。

每当调用一个函数(这不是可选行为)时,它都应该大声地而不是静默地失败。

编辑:在思考您的初级程序员故事时,听起来好像发生了这样的事情:当一个私人成员被设置为null时,该成员永远不会被允许发生。他们有写不当的问题,并试图通过阅读验证来解决。这是倒退。在您识别它时,该错误已经发生。用于代码审查/编译器的可执行解决方案不是保护条件,而是获取器,设置器或const成员。


7

正如其他已指出的,这取决于是否msgSender合法 NULL。以下内容假定它绝不能为NULL。

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

团队中其他人提议的“修复”违反了“无用程序不说谎”原则。确实很难发现错误。一种基于较早出现的问题以静默方式更改其行为的方法,不仅使查找第一个bug变得困难,而且还增加了第二个bug。

初中生由于不检查null而造成严重破坏。如果这段代码通过继续在未定义状态下运行(设备处于打开状态但程序“认为”处于关闭状态)造成了严重破坏,该怎么办?也许程序的另一部分将执行某些仅在设备关闭时才安全的操作。

这些方法中的任何一种都可以避免无提示的失败:

  1. 按照此答案的建议使用断言,但请确保在生产代码中将它们打开。当然,如果在假定其他断言不在生产中的情况下编写其他断言,则可能会导致问题。

  2. 如果为null,则抛出异常。


5

我同意将null捕获在构造函数中。此外,如果该成员在标头中声明为:

IMsgSender* const msgSender_;

然后,指针在初始化后将无法更改,因此,如果构造良好,则在包含它的对象的整个生命周期内都可以。(指向的对象是const。)


那是很好的建议,谢谢!事实上,它太好了,很抱歉我不能再投票了。这不是所陈述问题的直接答案,但它是消除指针“偶然”变成的任何可能的方法NULL
evadeflow 2013年

@evadeflow我以为“我同意所有人”回答了问题的主旨。虽然加油!;-)
Grimm The Opiner 2013年

考虑到问题的表达方式,此时更改接受的答案对我来说有点不礼貌。但是我真的认为这条建议非常出色,很抱歉我自己没有想到。使用const指针,不需要assert()在构造函数之外,因此,这个答案似乎比@Kristof Provost的答案还要好(它也很出色,但也间接地解决了这个问题。)我希望其他人对此表示赞成,因为它assert当您仅可以创建指针时,对于每种方法来说确实没有任何意义const
evadeflow 2013年

@evadeflow人们只访问Questins以获得代表,现在被回答没有人会再看它。;-)我之所以加入,是因为这是一个有关指针的问题,并且我一直在监视着血腥的“对所有事物始终使用智能指针”大队。
格林(Grimm The Opiner)

3

这是绝对危险的!

我在C代码库中的一位高级开发人员的陪同下,使用最拙劣的“标准”来推动相同的事情,盲目地检查所有指针是否为null。开发人员最终将执行以下操作:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

我曾经尝试一次在此类函数中取消对前提条件的此类检查,然后将其替换为,assert以查看会发生什么。

令我震惊的是,我在代码库中发现了成千上万的代码行,这些代码行将null传递给该函数,但是在这里开发人员(可能很困惑)解决了这个问题,只是添加了更多代码,直到一切正常为止。

令我更加恐惧的是,我发现此问题在代码库中检查空值的所有地方都很普遍。几十年来,代码库已经成长为依赖于这些检查,以便能够默默地违反甚至是最明确记录的前提条件。通过消除这些致命的检查而支持asserts,代码库中几十年来所有的逻辑人为错误都会被发现,我们会淹没它们。

这样的时间只花了两行看似无辜的代码,然后一个团队最终掩盖了上千个累积的错误。

这些都是使错误依赖于其他错误才能使软件运行的实践。这是一场噩梦。这也使与违反此类先决条件有关的每个逻辑错误都神秘地显示出与发生错误的实际位置相距一百万行的代码,因为所有这些null检查只会隐藏错误并隐藏错误,直到我们到达一个忘记的地方为止隐藏错误。

对我来说,在每个空指针违反先决条件的地方简单地盲目检查空值,这对我来说是非常疯狂的,除非您的软件对断言失败和生产崩溃的任务至关重要,因此这种情况的可能性是可取的。

编码标准要求首先检查函数中解引用的每个指针是否为NULL(甚至是私有数据成员)是否合理?

所以我要说,绝对不是。这甚至都不是“安全的”。可能恰好相反,并且掩盖了整个代码库中的所有错误,这些错误多年来可能导致最可怕的情况。

assert是去这里的方法。不允许违反先决条件,否则墨菲定律很容易就可以生效。


1
“断言”是必经之路-但是请记住,在任何现代系统上,通过指针进行的每个内存访问都在硬件中内置了一个空指针断言。因此,在“断言(p!= NULL); return * p;”中,即使断言也几乎没有意义。
gnasher729

@ gnasher729嗯,这是一个很好的观点,但是我倾向于将其assert视为既确立先决条件的一种文档编制机制,又不仅仅是强迫中止的一种方式。但是我也很喜欢断言失败的性质,它确切地向您显示导致失败的断言和断言条件失败的代码(“记录崩溃”)。如果最终在调试器之外使用调试版本并措手不及,可以派上用场。

@ gnasher729否则我可能误会了一部分。这些现代系统是否显示断言失败的源代码行?我通常会以为他们那时会丢失信息,可能只是显示段错误/访问冲突,但是我离“现代”还差得很远,在我的大多数开发工作中,我仍然顽固地使用Windows 7。

例如,在MacOS X和iOS上,如果您的应用在开发过程中由于空指针访问而崩溃,则调试器会告诉您确切的代码行。还有另一个奇怪的方面:如果您做可疑的事情,编译器将尝试向您发出警告。如果将指针传递给函数并对其进行访问,则编译器不会警告您该指针可能为NULL,因为它经常发生,因此您将被警告淹没。但是,如果比较if(p == NULL)...,则编译器会假设p很有可能是NULL,
gnasher729

因为您为什么还要对其进行测试,因此,如果您在未经测试的情况下使用它,将会从那时起发出警告。如果您使用自己的类似断言的宏,则必须以某种巧妙的方式编写它,以便“ my_assert(p!= NULL,“这是一个空指针,愚蠢!”); * p = 1;“不会给您警告。
gnasher729

2

例如,Objective-C会将nil对象上的每个方法调用都视为无操作,其结果为零值。出于您的问题所建议的原因,在Objective-C中进行该设计决策有一些优势。如果它得到了广泛的宣传和始终如一的应用,则对每个方法调用进行空保护的理论概念将具有一定的优势。

也就是说,代码存在于生态系统中,而不是真空中。在C ++中,空保护行为是非习惯性的并且令人惊讶,因此应视为有害的。总之,没有人以这种方式编写C ++代码,所以请不要这样做!不过,作为一个反例,请注意,在C和C ++ 中调用free()delete在a上进行调用NULL保证是无操作的。


在您的示例中,可能值得在msgSender非null 的构造函数中放置一个断言。如果构造称为方法msgSender马上,然后没有这样的说法是必要的,因为它会崩溃就在那里呢。但是,由于它只是存储 msgSender以供将来使用,因此从查看SignalShutdown()值的堆栈跟踪信息来看并不会很明显NULL,因此构造函数中的断言将大大简化调试过程。

更好的是,构造函数应接受一个const IMsgSender&引用,该引用不可能是NULL


1

要求您避免空取消引用的原因是为了确保您的代码健壮。很久以前的初级程序员的例子只是例子。任何人都可能会偶然破坏代码并导致空取消引用-尤其是对于全局变量和类全局变量。在C和C ++中,具有直接内存管理功能的可能性更大。您可能会感到惊讶,但是这种事情经常发生。即使是由知识渊博,经验丰富且非常资深的开发人员。

您无需对所有内容都进行null检查,但是您确实需要防止出现可能为null的取消引用。在不同功能中分配,使用和取消引用它们时,通常会发生这种情况。其他功能之一可能会被修改并破坏您的功能。也有可能其他函数之一被无序调用(例如,如果您有一个可以与析构函数分开调用的析构函数)。

我更喜欢您的同事告诉您的结合使用assert的方法。在测试环境中崩溃,因此更明显的是在生产中需要修复并正常失败的问题。

您还应该使用健壮的代码正确性工具,例如Coverity或Fortify。并且您应该解决所有编译器警告。

编辑:正如其他人所提到的,像几个代码示例一样,无声地失败通常也是错误的事情。如果您的函数无法从null值恢复,则应向调用者返回错误(或引发异常)。然后,调用方负责固定其调用顺序,恢复或向其调用方返回错误(或引发异常),等等。最终,函数要么能够正常恢复并继续运行,要么正常恢复并失败(例如,由于一个用户的内部错误导致数据库使事务失败,但没有主动退出),或者该函数确定应用程序状态已损坏并且无法恢复,应用程序退出。


+1表示有勇气支持(看似)不受欢迎的位置。如果没有人为我的同事辩护,我会很失望(他们是非常聪明的人,他们多年来一直在推出优质产品)。我也喜欢这样的想法,即应用程序应该“在测试环境中崩溃……并在生产中正常失败”。我不相信强制执行“死记硬背” NULL检查是做到这一点的方法,但是我很高兴听到工作场所外某人的意见,他基本上是在说:“哦,看起来并不太合理。”
evadeflow 2013年

当我看到人们在malloc()之后测试NULL时,我很生气。它告诉我他们不了解现代操作系统如何管理内存。
克里斯托弗·普罗沃斯特

2
我的猜测是,@ KristofProvost谈论的是使用过量使用的操作系统,为此,malloc总是成功的(但是,如果实际上没有足够的可用内存,则该操作系统以后可能会杀死您的进程)。但是,这不是跳过对malloc进行空检查的好理由:过度提交在各个平台上不是通用的,即使启用了它,也可能有其他原因导致malloc失败(例如,在Linux上,使用ulimit设置的进程地址空间限制)。
John Bartholomew

1
约翰怎么说。我确实在谈论过度使用。默认情况下,在Linux上启用过量使用(这是我的帐单)。我不知道,但我怀疑,如果Windows上也是如此。在大多数情况下,无论如何,您最终都将崩溃,除非您准备编写很多从未有过的代码,将经过测试以处理几乎肯定不会发生的错误……
Kristof Provost

2
让我补充说,我也从未见过能正确处理内存不足情况的代码(在linux内核之外,实际上可能发生内存不足)。我看到的所有代码尝试都会在稍后崩溃。完成的所有工作就是浪费时间,使代码更难以理解并隐藏真正的问题。空取消引用很容易调试(如果您有核心文件)。
克里斯托弗·普罗沃斯特

1

使用的指针,而不是一个参考会告诉我,msgSender唯一的选择,在这里空检查将是正确的。该代码段太慢而无法确定。也许其中还有其他PowerManager有价值的(或可测试的)要素...

在指针和参考之间进行选择时,我会仔细权衡这两个选项。如果必须为成员(甚至是私有成员)使用指针,则if (x)每次取消引用它时都必须接受该过程。


正如我在某处的另一条评论中所提到的,有时无法注入引用。我
经常

@evadeflow:好的,我承认,这取决于我在取消引用之前是否进行检查,并且取决于类大小和指针成员的可见性(不能被引用)。在班级小而普通且指针是私有的情况下,我不会...
Wolf

0

这取决于您要发生的事情。

您写道您正在开发“消费类电子设备”。如果,不知何故,一个bug通过设置介绍msgSender_NULL,你想

  • SignalShutdown继续操作的设备,跳过但继续其余操作,或者
  • 设备崩溃,迫使用户重新启动它?

根据未发送关闭信号的影响,选项1 可能是一个可行的选择。如果用户可以继续听他的音乐,但是显示屏仍然显示前一首曲目的标题,那么这可能比设备完全崩溃更可取。

当然,如果您选择选项1,那么assert(在其他人的建议下)对于减少这种错误在开发过程中被忽视的可能性至关重要。该if空后卫只是有减缓在生产中使用的故障。

就个人而言,我也更喜欢“早期崩溃”方法进行生产构建,但是我正在开发可以在发生错误时轻松修复和更新的商业软件。对于消费电子设备,这可能并不容易。

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.