```Assert():现代实践指南```
Assert(): A Modern How To

原始链接: https://fiberfs.io/blog/assert_a_modern_how_to

Reza Naghibi 认为,尽管 `assert()` 对于构建健壮的软件至关重要,但它经常被误解或未被充分利用。为了有效发挥断言的作用,他将其归纳为四个角色: * **正确性:** 验证输入和输出以确保 100% 的覆盖率,防止静默故障。 * **安全性:** 防止内存溢出或无效状态转换等边界情况。 * **开发辅助:** 捕获对于标准单元测试来说过于细微的逻辑错误(例如“差一错误”)。 * **文档作用:** 作为“护栏”明确开发者的意图,并为后续维护者说明隐含的前提假设。 Naghibi 强调,当使用得当时,断言对性能的影响微乎其微,并能显著改善静态分析。他提倡使用更复杂的断言框架,即能够区分生产环境检查与开发环境检查、提供描述性动态信息、包含堆栈跟踪,并能在失败时捕获应用程序状态的框架。归根结底,他主张应将断言视为维护系统完整性的关键性主动工具,而非仅仅是调试时的“事后补救”。当断言融入开发周期时,它们会形成一个重要的反馈闭环,防止系统崩溃并简化复杂的长期代码库维护工作。

抱歉。
相关文章

原文

Reza Naghibi - August 3rd 2026

Something that has always bothered me has been the assert() statement. While I personally consider the simple assertion statement as part of the bedrock of correct, safe, and supportable software, I feel that it also has come up a bit short in some regards. First, a lot of assert() implementations are simply under powered. While they do provide a bit of useful information, these implementations stop short of providing deeper and more useful context. Second, it’s really confusing when you should even use an assertion. Is it a lightweight debugging statement? Can assertions be used in production? What should I be asserting on? Can I customize how assert behaves? This confusion is compounded by the fact that a handful of implementations even allow for all assertions to be disabled with a simple compile time switch. So taken together, I would say much work still needs to be done around proper assert() understanding and usage.

The first and most important step in understanding how to better leverage assert() is to define when and where these statements should be used. I'm going to outline four areas where assertions can be effective:

  • Correctness - All possible function input and output values which do not have full value coverage should have assertions covering them.
  • Safety - When performing operations that can have unwanted, known, or unknown side effects, assertions should be used to prevent those conditions from happening.
  • Development - Use an assertion to enforce assumptions on values and state. These assertions can optionally be compiled out of code when coupled with proper testing.
  • Documentation - When writing code, use assertions as self documenting guardrails around your logic. Use assertions to enforce values and state which might be unclear from documentation or hard to decipher from reading code.

The added bonus here is when assertions are properly used across your code, static analysis of your code can now become more effective. Assertions greatly narrow the scope for data validation and invariant analysis, allowing for better results. You might be wondering, won’t all these assertions make my code slow and bloated? The simple answer here is no, when used properly, assertions have little to no overhead. We are talking about just a few CPU cycles to read a local value and then skip over a branch, which for most applications cannot even be accurately measured. When evaluated against the benefits assertions are giving you, it’s hard to argue against them.

Correctness and Safety

So let's talk about correctness, because this is probably the most important and under utilized role for assertions. When writing code, it’s easy to overlook all the different potential values a parameter or return value can have. An assertion can fix that gap, simply assert that the value is what you expect. If you support every possible value, which is usually the case, then no assertion is needed. Between the logic and your assertion, all potential values should now be covered and we can now say that coverage for this value is “correct”.

For example, let’s say we have this logic where error values are correctly covered with an assertion:

var error = system_call(...);
assert(!error);

Now, let’s say an error is generated and returned. Your program crashes. Obviously this is not ideal behavior, common values, including errors, should be handled more gracefully. However, what’s important here are two things. First, you have been made aware that this asserted value does exist. You can now choose to take action on it, like making a code change to actually handle this value or maybe spend some time better understanding what happened. Second, and this is the more important point, you started off with 100% value correctness and continue to have 100% value correctness. A failed assertion is behaving exactly as you intended, it correctly caught an edge case. Had that assertion not existed or been compiled away, your application would be in unknown and unsafe territory.

Here is another example illustrating value correctness:

var x = random();
assert(x > 0);

We set the assertion condition that we expect x to be greater than zero and as long as the logic using x supports all possible values of x greater than zero, we can say we are correct here for x. Now, let's say for whatever reason random() gives us a zero or a negative number, the assertion tells us our assumption was wrong and the program immediately terminates. We now hopefully have the opportunity to rectify this error with better handling or at least an investigation of what went wrong. Again, if we wrote this code without the assertion and we encountered a zero or negative number, we now have unexpected behavior and even worse, we may not even know it’s happening.

A few common examples of hard to handle values might be when a memory allocation call fails. While there are some valid ways of handling this, most of the time triggering an assertion and exiting is a valid thing to do. What would you do when a lock fails or a thread fails to spawn? Again, an assertion covering these cases is pretty reasonable given the extreme rarity of these events.

Another important reason to make sure you have 100% value correctness is that sometimes the environment you test on is not the same your application runs on. Subtle behavior differences between versions, APIs, libraries, and platforms can wreak havoc on an application, potentially introducing very hard to debug behavior. So having full value coverage is a way for your application to defensively and proactively combat this kind of silent breakage, immediately. And again, if your application faithfully handles all possible input and output values, then you can safely ignore this advice.

Operational safety is another big area where assertions can greatly help make things better for your application. The classic example is we have raw access to memory, how do we make sure those accesses are what we expect and want (ie: safe)? This comes down to 2 things. First, we must be able to define what the bounds of that memory are, and once we define those bounds, then we simply assert that we are within the bounds when accessing said memory. A lot of times when dealing with well defined types, these bounds and checks can be done automatically. When dealing with less defined types, like a C array or a raw memory pointer, a bound always needs to be defined and assertions applied on that bound when accessing.

Another classic example of operational safety is doing something as simple as:

var c = a + b;

How do we know that we didn’t overflow c when adding a and b together? How would your application behave if c is not a + b? Do you handle the case where c is zero? If these are unknowns, then a simple assertion can fix this (this assumes non negative values):

assert(c >= a);

If this seems overkill for something as simple as an addition statement, then it’s best to think about what bounds are in place in your application which would prevent a and b from getting to the point of potential overflow. Are these values tied to logic, a resource, or other code structure which has a limiting factor? If there are structural limits in place, then a development or documentation assertion might be in order. If no natural bounds exist, then it might make sense to add some safety assertions to your code or switch to an overflow safe library because unknowingly reaching overflow limits on variables is usually a sign that there might be some unsafe things brewing in your application.

Development and Documentation

Next up we have development asserts. These assertions typically cover logic and state errors. When coupled with proper testing, these assertions can be skipped on production code, but not always. An example of a development assertion is guaranteeing we do not have an “off by 1” error when using an index based API. This is common when dealing with strings. For example, let’s say we have a filename and we want to split out the name and extension:

var ext_position = filename.indexOf(“.”);
var name = filename.substring(0, ext_position);
var extension = filename.substring(ext_position + 1);
assert_dev(name + “.” + extension == filename);

In this example, we do a development assertion to make sure we parsed the filename correctly and at the right indexes by re-joining the parts and making sure it matches the original. As long as this is tested and the assertions don’t trigger, we are fairly certain that this logic is correct and it’s safe for this assertion to be skipped in production code. You might say this is a good candidate for a unit test and you are probably right. You could also say that this single assert statement is the unit test and no actual unit test is needed. A good rule is that development assertions usually operate at a level too granular for a full unit test. However, for a development assertion to have any value, it needs to be regularly tested. It’s worth noting that these kinds of assertions can have more execution overhead than simple value assertions, so be aware that if you are running a high concentration of expensive assertions, it can slow things down. This might be ok for testing, not great for production code.

Documentation asserts are similar to development asserts, except they act as reinforcement to document what assumptions any given piece of logic has made. They may seem redundant or even a case of over asserting, but they are really there to help aid with development. These assertions do double duty by both helping a code reader reason about (or even remember) how any piece of logic may operate and also guarding against incorrect usage. Large and evolved code bases which go through refactoring rounds and have logic spread across many source files can really benefit from these kinds of documenting asserts.

Extending the above example, we can add a documenting assert:

var ext_position = filename.indexOf(“.”);
assert_dev(ext_position > 0, “filename must have a name and extension, see validate_filename(): ” + filename);

Now if we hit this assertion, we ask ourselves, what happened to our validation?

When adding new logic to an already complicated codebase, it’s sometimes good practice to work your way through the logic with development assertions. Not only have you added a layer of documentation, but you have added guardrails to assist in future debugging and development work.

If you feel like an assertion is redundant, make it a development only assertion. If it still feels redundant and overly verbose, then you probably don’t need that assertion. Remember, development assertions are like unit tests. While they may be unsightly, I can’t ever recall someone complaining that a codebase has too many tests. When you start effectively using assertions and start building that feedback loop between correctness and your development process, you will start to get a better sense of where assertions provide value and just how valuable they are. Of course, your assertions are only as good as your ability to test them. And having too many assertions triggering in production code is never a good thing.

Looking Forward

So what does an ideal assertion API look like? First, it needs to have both a production and development API. Production assertions can never be disabled and development assertions should be easily switched on and off. Second, it needs to support dynamic messaging. An assertion like this tells us only part of the story:

assert(state == CONNECTED, “invalid state”);

This is a better way to relay the assertion:

assert(state == CONNECTED, “invalid state found: ” + state);

Third, it should support printing a stack dump along with the assertion message. If you hit an assertion deep within code, that stack dump gives you the much needed context as to how you reached that code. Finally, bonus points if you are able to store and then dump important parts of your application state, context, and/or metrics during the post assertion process. This will further assist in helping debug what went wrong that led to that assertion being triggered.

Blog

Home

联系我们 contact @ memedata.com