断言与异常抛出

关于如何以及何时使用断言,我已经阅读了大量的 物品(以及 StackOverflow 上发布的其他一些 相似问题) ,而且我很好地理解了它们。但是,我仍然不明白什么样的动机应该驱使我使用 Debug.Assert,而不是抛出一个普通的异常。我的意思是,进去。NET 对失败断言的默认响应是“停止世界”并向用户显示一个消息框。虽然这种行为可以被修改,但是我发现它非常烦人和多余 这样做,而我可以,只是抛出一个合适的异常。这样,我可以在抛出异常之前轻松地将错误写入应用程序的日志,而且,我的应用程序不一定会冻结。

那么,如果可以的话,为什么要使用 Debug.Assert而不是一个普通的异常呢?将断言放在不应该放置的位置可能会导致各种“不必要的行为”,所以在我看来,使用断言而不是抛出异常实际上并没有什么好处。你同意我的观点吗,还是我漏掉了什么?

注意: 我完全理解“理论上”的区别(调试与发布,使用模式等) ,但是在我看来,抛出异常比执行断言更好。因为如果在产品发布中发现了 bug,我仍然希望“断言”失败(毕竟,“开销”小得可笑) ,所以我最好还是抛出一个异常。


编辑: 在我看来,如果断言失败,就意味着应用程序进入了某种损坏的、意外的状态。那我为什么还要继续执行呢?应用程序是在调试版本还是发布版本上运行并不重要。两者都一样

20226 次浏览

Assertions are used to check the programmer's understanding of the world. An assertion should fail only if the programmer has done something wrong. For example, never use an assertion to check user input.

Asserts test for conditions that "cannot happen". Exceptions are for conditions that "should not happen but do".

Assertions are useful because at build time (or even run time) you can change their behavior. For example, often in release builds, the asserts aren't even checked, because they introduce unneeded overhead. This is also something to be wary of: your tests may not even be executed.

If you use exceptions instead of asserts, you lose some value:

  1. The code is more verbose, since testing and throwing an exception is at least two lines, while an assert is only one.

  2. Your test and throw code will always run, while asserts can be compiled away.

  3. You lose some communication with other developers, because asserts have a different meaning than product code that checks and throws. If you are really testing a programming assertion, use an assert.

More here: http://nedbatchelder.com/text/assert.html

EDIT: In response to the edit/note you made in your post: It sounds like using exceptions are the right thing to use over using assertions for the type of things you are trying to accomplish. I think the mental stumbling block you are hitting is that you are considering exceptions and assertions to fulfill the same purpose, and so you are trying to figure out which one would be 'right' to use. While there may be some overlap in how assertions and exceptions can be used, don't confuse that for them being different solutions to the same problem- they aren't. Assertions and Exceptions each have their own purpose, strengths, and weaknesses.

I was going to type up an answer in my own words but this does the concept better justice than I would have:

C# Station: Assertions

The use of assert statements can be an effective way to catch program logic errors at runtime, and yet they are easily filtered out of production code. Once development is complete, the runtime cost of these redundant tests for coding errors can be eliminated simply by defining the preprocessor symbol NDEBUG [which disables all assertions] during compilation. Be sure, however, to remember that code placed in the assert itself will be omitted in the production version.

An assertion is best used to test a condition only when all of the following hold:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

Assertions should almost never be used to detect situations that arise during software's normal operation. For example, usually assertions should not be used to check for errors in a user's input. It may, however, make sense to use assertions to verify that a caller has already checked a user's input.

Basically, use exceptions for things that need to be caught/dealt with in a production application, use assertions to perform logical checks that will be useful for development but turned off in production.

Debug.Assert by default will only work in debug builds, so if you want to catch any sort of bad unexpected behavior in your release builds you'll need to use exceptions or turn the debug constant on in your project properties (which is considered in general not to be a good idea).

Another nugget from Code Complete:

"An assertion is a function or macro that complains loudly if an assumption isn't true. Use assertions to document assumptions made in code and to flush out unexpected conditions. ...

"During development, assertions flush out contradictory assumptions, unexpected conditions, bad values passed to routines, and so on."

He goes on to add some guidelines on what should and should not be asserted.

On the other hand, exceptions:

"Use exception handling to draw attention to unexpected cases. Exceptional cases should be handled in a way that makes them obvious during development and recoverable when production code is running."

If you don't have this book you should buy it.

Though I agree that your reasoning is plausible -- that is, if an assertion is violated unexpectedly, it makes sense to halt execution by throwing -- I personally would not use exceptions in the place of assertions. Here's why:

As others have said, assertions should document situations that are impossible, in such a manner that if the allegedly impossible situation comes to pass, the developer is informed. Exceptions, by contrast, provide a control flow mechanism for exceptional, unlikely, or erroneous situations, but not impossible situations. For me, the key difference is this:

  • It should ALWAYS be possible to produce a test case which exercises a given throw statement. If it is not possible to produce such a test case then you have a code path in your program which never executes, and it should be removed as dead code.

  • It should NEVER be possible to produce a test case which causes an assertion to fire. If an assertion fires, either the code is wrong or the assertion is wrong; either way, something needs to change in the code.

That's why I would not replace an assertion with an exception. If the assertion cannot actually fire, then replacing it with an exception means you have an untestable code path in your program. I dislike untestable code paths.

I think a (contrived) practical example may help illuminate the difference:

(adapted from MoreLinq's Batch extension)

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {


// validate user input and report problems externally with exceptions


if(stuff == null) throw new ArgumentNullException("stuff");
if(doohickey == null) throw new ArgumentNullException("doohickey");
if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");


return DoSomethingImpl(stuff, doohickey, limit);
}


// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {


// validate input that should only come from other programming methods
// which we have control over (e.g. we already validated user input in
// the calling method above), so anything using this method shouldn't
// need to report problems externally, and compilation mode can remove
// this "unnecessary" check from production


Debug.Assert(stuff != null);
Debug.Assert(doohickey != null);
Debug.Assert(limit > 0);


/* now do the actual work... */
}

So as Eric Lippert et al have said, you only assert stuff that you expect to be correct, just in case you (the developer) accidentally used it wrong somewhere else, so you can fix your code. You basically throw exceptions when you have no control over or cannot anticipate what comes in, e.g. for user input, so that whatever gave it bad data can respond appropriately (e.g. the user).

Use assertions for things which ARE possible but should not happen (if it were impossible, why would you put an assertion?).

Doesn't that sound like a case to use an Exception? Why would you use an assertion instead of an Exception?

Because there should be code that gets called before your assertion that would stop the assertion's parameter being false.

Usually there is no code before your Exception that guarantees that it won't be thrown.

Why is it good that Debug.Assert() is compiled away in prod? If you want to know about it in debug, wouldn't you want to know about it in prod?

You want it only during development, because once you find Debug.Assert(false) situations, you then write code to guarantee that Debug.Assert(false) doesn't happen again. Once development is done, assuming you've found the Debug.Assert(false) situations and fixed them, the Debug.Assert() can be safely compiled away as they are now redundant.

Suppose you are a member of a fairly large team and there are several people all working on the same general code base, including overlapping on classes. You may create a method that is called by several other methods, and to avoid lock contention you do not add a separate lock to it, but rather "assume" it was previously locked by the calling method with a specific lock. Such as, Debug.Assert(RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); The other developers might overlook a comment that says the calling method must use the lock, but they cannot ignore this.