可能写太多的断言吗?


33

我非常喜欢assert用C ++代码编写检查,以此来捕获开发过程中由于程序中的逻辑错误而无法发生但确实会发生的情况。通常,这是一个好习惯。

但是,我注意到,我编写的某些函数(属于复杂类的一部分)具有5个以上的断言,就可读性和可维护性而言,这似乎可能是一种不良的编程习惯。我认为它仍然很棒,因为每个人都需要我考虑函数的前置条件和后置条件,它们确实有助于捕获错误。但是,我只是想把它放在那儿,以问在需要进行大量检查的情况下,是否存在更好的范例来捕获逻辑错误。

Emacs注释:由于Emacs是我的首选IDE,因此我将它的assert语句略显灰色,这有助于减少它们可以提供的混乱感。这是我添加到.emacs文件中的内容:

; gray out the "assert(...)" wrapper
(add-hook 'c-mode-common-hook
  (lambda () (font-lock-add-keywords nil
    '(("\\<\\(assert\(.*\);\\)" 1 '(:foreground "#444444") t)))))

; gray out the stuff inside parenthesis with a slightly lighter color
(add-hook 'c-mode-common-hook
  (lambda () (font-lock-add-keywords nil
    '(("\\<assert\\(\(.*\);\\)" 1 '(:foreground "#666666") t)))))

3
我必须承认,这是我时不时想到的一个问题。有兴趣听听其他人对此的看法。
明智船长

Answers:


45

我已经看到,如果有人写了更多的断言,本来可以更快解决的数百个错误,而没有一个可以通过减少编写而更快地解决的错误。

在可读性和可维护性方面,[太多的断言]可能是不好的编程习惯。

也许可读性可能是个问题-尽管根据我的经验,写好的断言的人也会写可读的代码。它从不打扰我看到函数的开头以一个断言块开头来验证参数是否是垃圾-只需在其后放一个空行即可。

同样,根据我的经验,可维护性总是通过断言提高,就像通过单元测试一样。断言提供健全性检查,以确保按预期方式使用了代码。


1
好答案。我还对如何提高Emacs的可读性问题添加了描述。
艾伦·图灵

2
“根据我的经验,写好的断言的人也会写可读的代码” <<很好的观点。使代码具有可读性取决于单个程序员,这取决于他(她)或不被允许使用的技术。我已经看到,好的技术在错误的手中变得不可读,即使是大多数人认为的坏技术也可以通过正确使用抽象和注释而变得非常清晰甚至优雅。
格雷格·杰克逊

我有一些由于错误的断言导致的应用程序崩溃。因此,我看到如果某人(我自己)写的断言更少的话,这些错误就不会存在
CodesInChaos

@CodesInChaos可以说,除了错别字之外,这还指出了问题表达的一个错误-也就是说,该错误是设计中的错误,因此是断言和(其他)代码之间的不匹配。
劳伦斯

12

可能写太多的断言吗?

好吧,当然是。[在这里想象一下令人讨厌的示例。]但是,应用下面详细介绍的准则,您在实践中突破该限制应该不会有麻烦。我也非常主张断言,并且根据这些原则使用它们。这些建议中的大部分不是断言专用的,而是仅适用于一般的良好工程实践。

记住运行时和二进制足迹的开销

断言很棒,但是如果它们使您的程序慢得令人无法接受,那么它将非常烦人,或者您迟早将其关闭。

我想相对于包含它的函数的成本来评估一个断言的成本。请考虑以下两个示例。

// Precondition:  queue is not empty
// Invariant:     queue is sorted
template <typename T>
const T&
sorted_queue<T>::max() const noexcept
{
  assert(!this->data_.empty());
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  return this->data_.back();
}

该函数本身是O(1)操作,但断言占On)开销。我认为除非在非常特殊的情况下,否则您不希望此类检查处于活动状态。

这是另一个具有类似断言的函数。

// Requirement:   op : T -> T is monotonic [ie x <= y implies op(x) <= op(y)]
// Invariant:     queue is sorted
// Postcondition: each item x in the queue is replaced by op(x)
template <typename T>
template <typename FuncT>
void
sorted_queue<T>::apply_monotonic_function(FuncT&& op)
{
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  std::transform(std::cbegin(this->data_), std::cend(this->data_),
                 std::begin(this->data_), std::forward<FuncT>(op));
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
}

该函数本身是On)操作,因此为断言添加额外的On)开销所受的伤害要小得多。我们通常可以在调试版本中提供一个小的(在这种情况下,可能小于3)常量因子来降低功能,但在发布版本中则不能。

现在考虑这个例子。

// Precondition:  queue is not empty
// Invariant:     queue is sorted
// Postcondition: last element is removed from queue
template <typename T>
void
sorted_queue<T>::pop_back() noexcept
{
  assert(!this->data_.empty());
  return this->data_.pop_back();
}

尽管许多人可能会比较满意O(1)断言而不是两个On以前示例中)声明,但在我看来它们在道德上是等效的。每个函数都会按函数本身的复杂性顺序增加开销。

最后,有一些“非常便宜”的断言主要由它们所包含的功能的复杂性决定。

// Requirement:   cmp : T x T -> bool is a strict weak ordering
// Precondition:  queue is not empty
// Postcondition: if x is returned, then there is no y in the queue
//                such that cmp(x, y)
template <typename T>
template <typename CmpT>
const T&
sorted_queue<T>::max(CmpT&& cmp) const
{
  assert(!this->data_.empty());
  const auto pos = std::max_element(std::cbegin(this->data_),
                                    std::cend(this->data_),
                                    std::forward<CmpT>(cmp));
  assert(pos != std::cend(this->data_));
  return *pos;
}

在这里,我们在On)函数中有两个O(1)断言。即使在发行版本中,保留此开销也可能不是问题。

但是要记住,渐进复杂度并不总是能提供足够的估计,因为在实践中,我们总是要处理输入大小,该输入大小受某些由“ Big- O ” 隐藏的有限常数和常数因子的限制,这可能非常微不足道。

因此,现在我们确定了不同的方案,我们该如何处理?一种(可能也是)简单的方法是遵循诸如“不要使用支配它们所包含功能的断言”之类的规则。尽管它可能适用于某些项目,但其他项目可能需要一种更具差异性的方法。这可以通过针对不同情况使用不同的断言宏来完成。

#define MY_ASSERT_IMPL(COST, CONDITION)                                       \
  (                                                                           \
    ( ((COST) <= (MY_ASSERT_COST_LIMIT)) && !(CONDITION) )                    \
      ? ::my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, # CONDITION) \
      : (void) 0                                                              \
  )

#define MY_ASSERT_LOW(CONDITION)                                              \
  MY_ASSERT_IMPL(MY_ASSERT_COST_LOW, CONDITION)

#define MY_ASSERT_MEDIUM(CONDITION)                                           \
  MY_ASSERT_IMPL(MY_ASSERT_COST_MEDIUM, CONDITION)

#define MY_ASSERT_HIGH(CONDITION)                                             \
  MY_ASSERT_IMPL(MY_ASSERT_COST_HIGH, CONDITION)

#define MY_ASSERT_COST_NONE    0
#define MY_ASSERT_COST_LOW     1
#define MY_ASSERT_COST_MEDIUM  2
#define MY_ASSERT_COST_HIGH    3
#define MY_ASSERT_COST_ALL    10

#ifndef MY_ASSERT_COST_LIMIT
#  define MY_ASSERT_COST_LIMIT MY_ASSERT_COST_MEDIUM
#endif

namespace my
{

  [[noreturn]] extern void
  assertion_failed(const char * filename, int line, const char * function,
                   const char * message) noexcept;

}

现在,您可以使用这三个宏MY_ASSERT_LOWMY_ASSERT_MEDIUMMY_ASSERT_HIGH不是使用标准库的“一刀切” assert宏,以分别由其控制,既不由其支配,也不对其包含函数的复杂性进行支配的断言。在构建软件时,您可以预定义预处理器符号,MY_ASSERT_COST_LIMIT以选择应将其置为可执行文件的哪种断言。常量MY_ASSERT_COST_NONEMY_ASSERT_COST_ALL不与任何断言宏相对应,并且旨在用作值MY_ASSERT_COST_LIMIT,以便分别打开或关闭所有断言。

我们在此假设一个好的编译器不会为

if (false_constant_expression && run_time_expression) { /* ... */ }

并转变

if (true_constant_expression && run_time_expression) { /* ... */ }

进入

if (run_time_expression) { /* ... */ }

我认为现在是一个安全的假设。

如果您要调整上面的代码,请考虑编译器特定的注释,例如__attribute__ ((cold))on my::assertion_failed__builtin_expect(…, false)on,!(CONDITION)以减少传递的断言的开销。在发行版本中,您还可以考虑将函数调用替换为my::assertion_failed类似的方法,__builtin_trap以减少丢失诊断消息的不便之处。

这些类型的优化实际上仅与非常紧凑的函数中非常便宜的断言(例如比较两个已经作为参数给出的整数)有关,而没有考虑通过合并所有消息字符串而累积的二进制额外大小。

比较这段代码

int
positive_difference_1st(const int a, const int b) noexcept
{
  if (!(a > b))
    my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, "!(a > b)");
  return a - b;
}

被编译成以下程序集

_ZN4test23positive_difference_1stEii:
.LFB0:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L5
        movl    %edi, %eax
        subl    %esi, %eax
        ret
.L5:
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %ecx
        movl    $_ZZN4test23positive_difference_1stEiiE12__FUNCTION__, %edx
        movl    $50, %esi
        movl    $.LC1, %edi
        call    _ZN2my16assertion_failedEPKciS1_S1_
        .cfi_endproc
.LFE0:

而下面的代码

int
positive_difference_2nd(const int a, const int b) noexcept
{
  if (__builtin_expect(!(a > b), false))
    __builtin_trap();
  return a - b;
}

给这个大会

_ZN4test23positive_difference_2ndEii:
.LFB1:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L8
        movl    %edi, %eax
        subl    %esi, %eax
        ret
        .p2align 4,,7
        .p2align 3
.L8:
        ud2
        .cfi_endproc
.LFE1:

我感到很舒服。(示例在GCC 5.3.0中使用-std=c++14-O3-march=native在4.3.3-2-ARCH x86_64 GNU / Linux上的和进行了测试。上面的代码片段中未显示的声明,test::positive_difference_1sttest::positive_difference_2nd我添加__attribute__ ((hot))到的my::assertion_failed声明用进行了声明__attribute__ ((cold))。)

在依赖于它们的函数中声明前提条件

假设您具有指定合同的以下功能。

/**
 * @brief
 *         Counts the frequency of a letter in a string.
 *
 * The frequency count is case-insensitive.
 *
 * If `text` does not point to a NUL terminated character array or `letter`
 * is not in the character range `[A-Za-z]`, the behavior is undefined.
 *
 * @param text
 *         text to count the letters in
 *
 * @param letter
 *         letter to count
 *
 * @returns
 *         occurences of `letter` in `text`
 *
 */
std::size_t
count_letters(const char * text, int letter) noexcept;

而不是写作

assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);

在每个呼叫站点,将该逻辑一次放入 count_letters

std::size_t
count_letters(const char *const text, const int letter) noexcept
{
  assert(text != nullptr);
  assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
  auto frequency = std::size_t {};
  // TODO: Figure this out...
  return frequency;
}

并毫不费力地称呼它。

const auto frequency = count_letters(text, letter);

这具有以下优点。

  • 您只需要编写一次断言代码。因为函数的真正目的是经常调用它们一次以上,所以这将减少assert代码中语句的总数。
  • 它使检查先决条件的逻辑与依赖先决条件的逻辑保持接近。我认为这是最重要的方面。如果您的客户端滥用了您的接口,则也不能假定它们正确地应用了断言,因此,函数告诉他们更好。

明显的缺点是您不会将呼叫站点的源位置纳入诊断消息中。我相信这是一个小问题。一个好的调试器应该能够使您方便地追溯违约的起源。

同样的想法也适用于“特殊”功能,例如重载运算符。在编写迭代器时,通常(如果迭代器的性质允许)给它们一个成员函数

bool
good() const noexcept;

这允许询问取消迭代器的安全性。(当然,在实践中,它几乎总是只能以保证它不会是安全的提领迭代器,但我相信你仍然可以抓到很多的bug具有这样的功能。)而不是乱抛垃圾我所有的代码使用带有assert(iter.good())语句的迭代器的方法,我宁愿将一个assert(this->good())作为operator*迭代器的第一行。

如果您使用的是标准库,则不要在源代码中对其先决条件进行手动声明,而应在调试版本中打开它们的检查。他们可以进行更复杂的检查,例如测试迭代器引用的容器是否仍然存在。(有关更多信息,请参见libstdc ++libc ++的文档(正在进行中)。)

排除共同条件

假设您正在编写线性代数包。许多功能将具有复杂的前提条件,违反这些前提条件通常会导致错误的结果,因此无法立即识别出这些结果。如果这些功能声明了前提条件,那将是非常好的。如果定义一堆谓词来告诉您有关结构的某些属性,则这些断言将变得更加可读。

template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
  assert(is_square(m) && is_symmetric(m));
  // TODO: Somehow decompose that thing...
}

它还将提供更多有用的错误消息。

cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)

可以帮助很多

detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)

您首先必须在上下文中查看源代码以找出实际测试的内容。

如果您有一个class具有非平凡的不变量,那么当您弄乱内部状态并希望确保在返回时将对象保持为有效状态时,不时对它们进行断言可能是一个好主意。

为此,我发现定义一个private我通常调用的成员函数很有用class_invaraiants_hold_。假设您正在重新实现std::vector(因为我们都知道它不够好。),它可能具有这样的功能。

template <typename T>
bool
vector<T>::class_invariants_hold_() const noexcept
{
  if (this->size_ > this->capacity_)
    return false;
  if ((this->size_ > 0) && (this->data_ == nullptr))
    return false;
  if ((this->capacity_ == 0) != (this->data_ == nullptr))
    return false;
  return true;
}

注意一些有关此的事情。

  • 根据准则,断言函数本身为constnoexcept,断言不得有副作用。如果有道理,也请声明它constexpr
  • 谓词本身不会声明任何内容。它被称为内部断言,例如assert(this->class_invariants_hold_())。这样,如果断言断言,我们可以确定不会产生运行时开销。
  • 函数内部的控制流分为多个if带有Early return而不是大型表达式的语句。这使在调试器中单步执行该函数变得容易,并找出断言触发时不变式的哪一部分被破坏了。

不要对愚蠢的事情断言

断言有些事情是没有意义的。

auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2);  // silly
assert(!numbers.empty());     // silly and redundant

这些断言不会使代码更易读或难以推理。每个C ++程序员都应该足够自信如何std::vector查看上面的代码,以确保上述代码正确无误。我并不是说您永远都不能断言容器的大小。如果您已使用一些非平凡的控制流程添加或删除了元素,则此类断言可能会很有用。但是,如果仅重复上面非声明代码中编写的内容,则不会获得任何价值。

也不要断言库函数正常工作。

auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled());  // probably silly

如果您不太信任该库,最好考虑使用另一个库。

另一方面,如果该库的文档不是100%清晰的,而您通过阅读源代码对它的合同有了信心,那么断言“推断的合同”就很有意义。如果该库的将来版本中有损坏,您会很快注意到。

auto w = widget {};
// After reading the source code, I have concluded that quantum mode is
// always off by default but this isn't documented anywhere.
assert(!w.quantum_mode_enabled());

这比以下解决方案更好,后者不会告诉您您的假设是否正确。

auto w = widget {};
if (w.quantum_mode_enabled())
  {
    // I don't think that quantum mode is ever enabled by default but
    // I'm not sure.
    w.disable_quantum_mode();
  }

不要滥用断言来实现程序逻辑

断言应该永远只能用于揭示错误,是值得的,立即杀死你的应用程序。即使对于该条件的适当反应也应立即退出,否则不应使用它们来验证任何其他条件。

因此,写这个…

if (!server_reachable())
  {
    log_message("server not reachable");
    shutdown();
  }

…而不是那个。

assert(server_reachable());

也不要使用断言来验证不受信任的输入或检查std::malloc不是return您的输入nullptr。即使您知道即使在发行版本中也永远不会关闭断言,断言也会与读者交流,因为该程序没有错误且没有明显的副作用,因此断言会检查始终为真的事物。如果这不是您要传达的消息,请使用其他错误处理机制,例如throw发出异常。如果发现使用宏包装程序进行非断言检查很方便,请继续编写一个。只是不要称它为“断言”,“承担”,“要求”,“确保”或类似的东西。当然,它的内部逻辑与for相同assert,只是它从未被编译出来。

更多信息

我发现约翰洛科什讲的防御性编程立刻完成,在CppCon'14(给出1 的部分2 部分)很有启发。与我在此答案中所做的相比,他采用了自定义启用哪些断言以及如何对失败的异常进行响应的想法。


4
Assertions are great, but ... you will turn them off sooner or later.-希望更快,就像代码发布之前一样。需要使程序在生产中终止的事物应该是“真实”代码的一部分,而不是断言。
Blrfl

4

我发现随着时间的推移,我编写的断言会减少,因为其中许多断言等于“编译器在工作”和“库在工作”。一旦您开始考虑要测试的内容,我怀疑您编写的断言会更少。

例如,一种(例如)向集合中添加内容的方法不必断言该集合存在-这通常是拥有消息的类的先决条件,或者是致命错误,应将该错误返回给用户。因此,请尽早检查一次,然后再假设。

对我来说,断言是一种调试工具,通常将以两种方式使用它们:在我的办公桌前发现一个错误(并且不予检入。并在客户的办公桌上找到错误(并且确实将他们签入)。两次我都使用断言来尽可能早地强制执行异常后生成堆栈跟踪。请注意,以这种方式使用的断言很容易导致heisenbug-该错误很可能永远不会在启用了断言的调试版本中发生。


4
当您说“这通常是拥有消息的类的先决条件,或者是应该使该消息返回给用户的致命错误时,我不明白您的意思因此,请尽早检查一次,然后再假设。”如果不是为了验证您的假设,您将使用断言吗?
5gon12eder 2016年

4

断言太少:祝您更改该代码的好运充满了隐藏的假设。

断言太多:会导致可读性问题和潜在的代码异味-当在断言语句中放置许多假设时,是否正确设计了类,函数和API?

可能还有一些断言并没有真正检查任何内容,也不检查每个函数中的编译器设置之类的东西:/

力争达到最佳目标,但不要少(正如其他人已经说过的那样,“更多”的主张比没有太多或没有上帝帮助我们没有更多危害)。


3

如果您可以编写仅使用布尔型CONST方法的引用的Assert函数,那将非常好,这样,通过确保使用布尔型const方法来测试断言,可以确保断言没有副作用。

它会从可读性上吸取一点点,特别是因为我不认为您不能将lambda(在c ++ 0x中)注释为某个类的const,这意味着您不能为此使用lambda

如果您问我,那就算是杀了我,但是如果我因为断言而开始看到一定程度的污染,我会警惕两件事:

  • 确保断言中没有发生副作用(由上述解释提供)
  • 开发测试期间的性能;这可以通过向断言功能添加级别(例如日志记录)来解决;因此您可以从开发版本中禁用一些断言,以提高性能

2
神圣的废话你喜欢“某些”一词及其派生词。我算了8次。
Casey Patton

是的,对不起,我倾向于过多地使用单词-固定,谢谢
lurscher 2011年

2

我用C#编写的代码比用C ++编写的代码要多得多,但是两种语言之间的距离并不是很远。在.Net中,我确实将Asserts用于不应发生的条件,但是当无法继续运行时,我也会经常抛出异常。VS2010调试器向我展示了很多有关异常的良好信息,无论Release版本如何优化。如果可以的话,添加单元测试也是一个好主意。有时,日志记录也可以作为调试的帮助。

那么,可以有太多的主张吗?是。一分钟内选择中止/忽略/继续15次很烦。异常仅引发一次。很难量化存在过多断言的点,但是如果您的断言履行断言,异常,单元测试和日志记录的作用,那么就会出错。

我会为不应该发生的情况保留断言。您可能一开始会断言,因为断言的编写速度更快,但是在以后重新构造代码-将其中的一些变成异常,将一些变成测试,等等。如果您有足够的纪律来清理每个TODO注释,则保留一个在您计划返工的每一个旁边添加评论,并且不要忘记稍后再解决TODO。


如果您的代码每分钟失败15个断言,我认为这会涉及更大的问题。断言永远都不会在没有错误的代码中触发,而是断言,它们应该杀死应用程序以防止进一步损坏,或者使您进入调试器以查看发生了什么。
5gon12eder 2016年

2

我想和你一起工作!写很多东西的人asserts太棒了。我不知道是否有“太多”这样的事情。对我而言,更普遍的是写得很少的书,最终碰到偶尔的致命的UB问题,这种问题只在满月出现,可以用简单的重复轻松再现assert

失败讯息

我能想到的一件事是将故障信息嵌入到,assert如果您还没有这样做的话,就像这样:

assert(n >= 0 && n < num && "Index is out of bounds.");

这样,如果您还没有这样做的话,您可能不再觉得自己太多了,因为现在,您的断言在记录假设和前提条件中起着更重要的作用。

副作用

当然assert实际上可能会被滥用并引入错误,例如:

assert(foo() && "Call to foo failed!");

...如果foo()触发副作用,那么您应该对此非常小心,但是我敢肯定您已经是一个非常宽泛的断言者(“经验丰富的断言者”)。希望您的测试过程也和您对断言假设的认真关注一样好。

调试速度

虽然调试的速度通常应该放在优先级列表的底部,但有一次我确实在一个代码库中断言了这么多,然后才通过调试器运行调试版本比发布版本慢100倍以上。

这主要是因为我具有以下功能:

vec3f cross_product(const vec3f& lhs, const vec3f& rhs)
{
    return vec3f
    (
        lhs[1] * rhs[2] - lhs[2] * rhs[1],
        lhs[2] * rhs[0] - lhs[0] * rhs[2],
        lhs[0] * rhs[1] - lhs[1] * rhs[0]
    );
}

...每次调用operator[]都会进行边界检查断言。我最终用不安全的等效项替换了那些对性能至关重要的替代项,这些等效项并没有断言仅是为了以实现细节级的安全性而花费很少的代价就大大加快了调试构建的速度,而且仅仅是因为它的速度开始受到打击极大地降低了生产率(更快地进行调试所带来的好处胜于失去一些断言的代价,但仅仅是针对这种交叉乘积函数之类的函数,这种函数通常用于最关键的,可衡量的路径中,而不是operator[]一般而言)。

单一责任原则

虽然我认为使用更多的断言并不能真正出错(至少要错得太多而不是太少要好得多),但是断言本身可能不是问题,但可能表明一个问题。

例如,如果您对单个函数调用有5个断言,则可能做得太多。它的接口可能有太多的前提条件和输入参数,例如,我认为这与构成健康声明的主题无关(我通常会对此做出回应,“越多越好!”),但这可能是一个可能的危险信号(或非常可能不会)。


1
好的,理论上可以有“太多”的断言,尽管这个问题很快就会变得很明显:如果断言花费的时间比函数的时间长得多。诚然,我不记得在野外发现相反的问题普遍存在。
Deduplicator 2016年

@Deduplicator啊,是的,我在那些关键的矢量数学例程中遇到了这种情况。虽然绝对可以认为错在太多而不是太多方面要好得多!

-1

向代码中添加检查是非常合理的。对于普通的断言(内置于C和C ++编译器的断言),我的用法模式是断言失败意味着代码中存在需要修复的错误。我对此进行了慷慨的解释。如果我期待有一个web请求返回一个状态200,并断言它不处理其他案件则断言失败的确显示出在我的代码中的错误,所以断言是有道理的。

因此,当人们说只检查代码做什么的断言是多余的时,那是不正确的。该断言检查他们认为代码的作用,而断言的全部要点是检查代码中没有错误的假设是正确的。断言也可以用作文档。如果我假设执行循环后i == n,并且从代码中并非100%显而易见,那么“断言(i == n)”将很有帮助。

最好不只是在曲目中“断言”来处理不同的情况。例如,当我检查没有发生表明错误的情况,但仍然可以继续解决该情况时。(例如,如果我使用一些缓存,那么我可能会检查错误,并且如果意外发生错误,则可以通过丢弃缓存来修复错误,这是安全的。我想要的东西几乎是断言,可以在开发过程中告诉我,仍然让我继续。

另一个示例是我不希望发生任何事情的情况,我有一个通用的解决方法,但是如果发生这种情况,我想知道并进行检查。同样,在开发过程中应该告诉我一些类似于断言的东西。但不是肯定。

断言太多:如果断言在用户手中时使程序崩溃,那么您就不能因为误报否定而使该断言崩溃。


-3

这取决于。如果代码要求清楚地记录在案,则断言应始终符合要求。在这种情况下,这是一件好事。但是,如果没有任何要求或写得不好的要求,那么对于新程序员来说,无需每次都参考单元测试来确定要求是什么,就很难编辑代码。


3
这似乎并没有提供任何实质性的制作上分和8分之前解释的答案
蚊蚋
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.