6

可能的重复:
防御性编程

今天早上我们就防御性编程的主题进行了一场精彩的讨论。我们进行了代码审查,其中传入了一个指针,但没有检查它是否有效。

有些人认为只需要检查空指针。我质疑它是否可以在更高级别进行检查,而不是通过它通过的每个方法,并且如果另一端的对象不满足某些要求,检查 null 是一个非常有限的检查。

我理解并同意检查 null 总比没有好,但在我看来,只检查 null 会提供一种错误的安全感,因为它的范围有限。如果要确保指针可用,请检查多于 null。

你在这个问题上有什么经验?对于传递给从属方法的参数,您如何在代码中编写防御措施?

4

14 回答 14

17

在 Code Complete 2 的错误处理一章中,我被介绍了路障的概念。本质上,路障是严格验证所有输入的代码。路障内的代码可以假设任何无效输入都已被处理,并且接收到的输入是好的。在路障内,代码只需要担心路障内其他代码传递给它的无效数据。断言条件和明智的单元测试可以增加你对封闭代码的信心。通过这种方式,您在路障中非常防御性地编程,但在路障内则不那么如此。另一种思考方式是,在路障中,您始终正确处理错误,而在路障内,您只需在调试构建中断言条件。

就使用原始指针而言,通常您能做的最好的事情就是断言指针不为空。如果您知道该内存中应该包含什么,那么您可以确保内容以某种方式保持一致。这就引出了一个问题,即为什么该内存没有被包裹在一个可以验证其自身一致性的对象中。

那么,为什么在这种情况下使用原始指针?使用引用或智能指针会更好吗?指针是否包含数字数据,如果是,将其包装在一个管理该指针生命周期的对象中会更好吗?

回答这些问题可以帮助您找到一种更具防御性的方法,因为您最终会得到一个更容易防御的设计。

于 2009-12-16T18:36:02.960 回答
13

最好的防御方法不是在运行时检查指针是否为空,而是避免使用可能为空的指针

如果传入的对象不能为空,请使用引用!或者按值传递!或者使用某种智能指针。

进行防御性编程的最佳方法是在编译时捕获错误。如果对象为空或指向垃圾被认为是错误的,那么您应该使这些东西编译错误。

最终,您无法知道指针是否指向有效对象。因此,与其检查一个特定的极端情况(这比真正危险的情况要少得多,即指向无效对象的指针),不如通过使用保证有效性的数据类型来使错误成为不可能。

我想不出另一种主流语言可以让您在编译时捕获与 C++ 一样多的错误。使用该功能。

于 2009-12-16T18:44:15.883 回答
4

没有办法检查指针是否有效。

于 2009-12-16T18:40:12.000 回答
3

我可能有点极端,但我不喜欢防御性编程,我认为是懒惰引入了原理。

对于这个特定示例,断言指针不为空是没有意义的。如果你想要一个空指针,没有比使用引用更好的方法来实际执行它(并同时清楚地记录它)。它的文档实际上将由编译器强制执行,并且在运行时不会花费 ziltch!

一般来说,我倾向于不直接使用“原始”类型。让我们举例说明:

void myFunction(std::string const& foo, std::string const& bar);

foo和的可能值是bar多少?好吧,这几乎仅限于 astd::string可能包含的内容......这是非常模糊的。

另一方面:

void myFunction(Foo const& foo, Bar const& bar);

好多了!

  • 如果人们错误地颠倒了参数的顺序,它会被编译器检测到
  • 每个类单独负责检查值是否正确,用户没有负担。

我倾向于支持强类型。如果我有一个条目应该只由字母字符组成并且最多 12 个字符,我宁愿创建一个包装 a 的小类std::string,并在内部使用一个简单的validate方法来检查分配,然后传递该类。通过这种方式,我知道如果我测试验证例程 ONCE,我实际上不必担心该值可以到达我的所有路径 > 当它到达我时它将被验证。

当然,这并不是我不应该测试代码。只是我更喜欢强封装,在我看来,输入的验证是知识封装的一部分。

因为没有任何规则可以毫无例外地出现……暴露的接口必然会被验证代码臃肿,因为你永远不知道会发生什么。但是,对于 BOM 中的自我验证对象,它通常是非常透明的。

于 2009-12-16T20:01:19.293 回答
3

我是“让它崩溃”设计学院的忠实粉丝。(免责声明:我不从事医疗设备、航空电子设备或核电相关软件的工作。)如果您的程序崩溃,您启动调试器并找出原因。相反,如果您的程序在检测到非法参数后继续运行,那么当它崩溃时,您可能不知道出了什么问题。

好的代码由许多小函数/方法组成,并且在每个代码片段中添加十几行参数检查会使其更难阅读和维护。把事情简单化。

于 2009-12-16T19:32:36.517 回答
3

说真的,这取决于你想对你造成多少错误。

检查空指针绝对是我认为必要但不够充分的事情。您可以使用许多其他可靠的原则,从代码的入口点(例如,输入验证 = 该指针是否指向有用的东西)和出口点(例如,您认为指针指向的东西有用但它碰巧导致你的代码抛出异常)。

简而言之,如果您假设每个调用您的代码的人都会竭尽全力毁掉您的生活,那么您可能会发现很多最坏的罪魁祸首。

为清楚起见进行编辑:其他一些答案正在谈论单元测试。我坚信测试代码有时比它正在测试的代码有价值(取决于谁在衡量价值)。也就是说,我也认为单元测试对于防御性编码也是必要的,但还不够。

具体示例:考虑第 3 方搜索方法,该方法记录在案以返回与您的请求匹配的值的集合。不幸的是,该方法的文档中并不清楚的是,原始开发人员认为如果没有任何内容符合您的请求,则返回 null 而不是空集合会更好。

因此,现在,您将防御性和经过良好单元测试的方法称为思考(可悲的是缺少内部空指针检查)并且繁荣!NullPointerException ,如果没有内部检查,您将无法处理:

defensiveMethod(thirdPartySearch("Nothing matches me")); 
// You just passed a null to your own code.
于 2009-12-16T18:12:10.980 回答
2

“单元测试验证代码做了它应该做的事情”>“生产代码试图验证它没有做它不应该做的事情”。

我什至不会自己检查 null ,除非它是已发布 API 的一部分。

于 2009-12-16T18:15:36.440 回答
1

这在很大程度上取决于;有问题的方法是否曾被您组外部的代码调用过,还是内部方法?

对于内部方法,您可以进行足够的测试以使其成为一个有争议的问题,并且如果您正在构建目标是尽可能高的性能的代码,您可能不想花时间检查您非常确定是正确的输入。

对于外部可见的方法——如果有的话——你应该总是仔细检查你的输入。总是。

于 2009-12-16T18:16:25.687 回答
1

从调试的角度来看,最重要的是您的代码是快速失败的。代码越早失败,就越容易找到失败点。

于 2009-12-16T19:14:50.797 回答
0

许多答案解决了如何在代码中编写防御的问题,但没有太多关于“你应该如何防御?”。这是您必须根据软件组件的关键性来评估的东西。

我们正在做飞行软件,软件错误的影响范围从轻微的烦恼到飞机/机组人员的损失。我们根据影响编码标准、测试等的潜在不利影响对不同的软件进行分类。您需要评估您的软件将如何使用以及错误的影响,并设置您想要(并且可以负担)的防御级别。DO-178B 标准将此称为“设计保证级别” 。

于 2009-12-16T20:42:45.113 回答
0

对于内部方法,我们通常坚持对这些类型的检查进行断言。这确实会在单元测试中发现错误(你有很好的测试覆盖率,对吧?),或者至少在使用断言运行的集成测试中。

于 2009-12-16T18:24:04.157 回答
0

仅当您需要对指针执行某些操作时才应使用指针。比如指针算术横向一些数据结构。然后,如果可能的话,应该将其封装在一个类中。

如果将指针传递给函数以对其指向的对象执行某些操作,则改为传递引用。

防御性编程的一种方法是断言几乎所有可以断言的东西。在项目开始时它很烦人,但后来它是单元测试的一个很好的辅助。

于 2009-12-16T20:19:19.353 回答
0

检查空指针只是故事的一半,您还应该为每个未分配的指针分配一个空值
最负责任的 API 也会这样做。
检查空指针在 CPU 周期中的成本非常低,一旦交付应用程序崩溃可能会使您和您的公司损失金钱和声誉。

如果代码在您可以完全控制的私有接口中,您可以跳过空指针检查和/或通过运行单元测试或一些调试构建测试(例如断言)来检查空指针

于 2009-12-16T18:36:30.043 回答
0

在这个问题中,我想解决一些问题:

  1. 编码指南应指定您直接处理引用或值,而不是使用指针。根据定义,指针是只在内存中保存地址的值类型——指针的有效性是特定于平台的,并且意味着很多东西(可寻址内存的范围、平台等)
  2. 如果您发现自己出于任何原因(例如动态生成和多态对象)需要指针,请考虑使用智能指针。智能指针通过“普通”指针的语义为您提供了许多优势。
  3. 例如,如果一个类型具有“无效”状态,那么该类型本身应该为此提供。更具体地说,您可以实现 NullObject 模式,该模式指定“错误定义”或“未初始化”对象的行为方式(可能通过抛出异常或提供无操作成员函数)。

您可以创建一个执行 NullObject 默认值的智能指针,如下所示:

template <class Type, class NullTypeDefault>
struct possibly_null_ptr {
  possibly_null_ptr() : p(new NullTypeDefault) {}
  possibly_null_ptr(Type* p_) : p(p_) {}
  Type * operator->() { return p.get(); }
  ~possibly_null_ptr() {}
  private:
    shared_ptr<Type> p;
    friend template<class T, class N> Type & operator*(possibly_null_ptr<T,N>&);
};

template <class Type, class NullTypeDefault>
Type & operator*(possibly_null_ptr<Type,NullTypeDefault> & p) {
  return *p.p;
}

然后possibly_null_ptr<>在您支持可能指向具有默认派生“空行为”的类型的空指针的情况下使用该模板。这在设计中明确表明“空对象”存在可接受的行为,这使得您的防御实践记录在代码中 - 并且比一般准则或实践更具体。

于 2009-12-16T19:07:49.310 回答