Design by contract using assertions or exceptions?

2019-01-02 19:07发布

When programming by contract a function or method first checks whether its preconditions are fulfilled, before starting to work on its responsibilities, right? The two most prominent ways to do these checks are by assert and by exception.

  1. assert fails only in debug mode. To make sure it is crucial to (unit) test all separate contract preconditions to see whether they actually fail.
  2. exception fails in debug and release mode. This has the benefit that tested debug behavior is identical to release behavior, but it incurs a runtime performance penalty.

Which one do you think is preferable?

See releated question here

14条回答
弹指情弦暗扣
2楼-- · 2019-01-02 19:40

See also this question:

I some cases, asserts are disabled when building for release. You may not have control over this (otherwise, you could build with asserts on), so it might be a good idea to do it like this.

The problem with "correcting" the input values is that the caller will not get what they expect, and this can lead to problems or even crashes in wholly different parts of the program, making debugging a nightmare.

I usually throw an exception in the if-statement to take over the role of the assert in case they are disabled

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
查看更多
冷夜・残月
3楼-- · 2019-01-02 19:42

It is not exactly true that "assert fails only in debug mode."

In Object Oriented Software Construction, 2nd Edition by Bertrand Meyer, the author leaves a door open for checking preconditions in release mode. In that case, what happens when an assertion fails is that... an assertion violation exception is raised! In this case, there is no recovery from the situation: something useful could be done though, and it is to automatically generate an error report and, in some cases, to restart the application.

The motivation behind this is that preconditions are typically cheaper to test than invariants and postconditions, and that in some cases correctness and "safety" in the release build are more important than speed. i.e. For many applications speed is not an issue, but robustness (the ability of the program to behave in a safe way when its behaviour is not correct, i.e. when a contract is broken) is.

Should you always leave precondition checks enabled? It depends. It's up to you. There is no universal answer. If you're making software for a bank, it might be better to interrupt execution with an alarming message than to transfer $1,000,000 instead of $1,000. But what if you're programming a game? Maybe you need all the speed you can get, and if someone gets 1000 points instead of 10 because of a bug that the preconditions didn't catch (because they're not enabled), tough luck.

In both cases you should ideally have catched that bug during testing, and you should do a significant part of your testing with assertions enabled. What is being discussed here is what is the best policy for those rare cases in which preconditions fail in production code in a scenario which was not detected earlier due to incomplete testing.

To summarize, you can have assertions and still get the exceptions automatically, if you leave them enabled - at least in Eiffel. I think to do the same in C++ you need to type it yourself.

See also: When should assertions stay in production code?

查看更多
情到深处是孤独
4楼-- · 2019-01-02 19:46

You should use both. Asserts are for your convenience as a developer. Exceptions catch things you missed or didn't expect during runtime.

I've grown fond of glib's error reporting functions instead of plain old asserts. They behave like assert statements but instead of halting the program, they just return a value and let the program continue. It works surprisingly well, and as a bonus you get to see what happens to the rest of your program when a function doesn't return "what it's supposed to". If it crashes, you know that your error checking is lax somewhere else down the road.

In my last project, I used these style of functions to implement precondition checking, and if one of them failed, I would print a stack trace to the log file but keep on running. Saved me tons of debugging time when other people would encounter a problem when running my debug build.

#ifdef DEBUG
#define RETURN_IF_FAIL(expr)      do {                      \
 if (!(expr))                                           \
 {                                                      \
     fprintf(stderr,                                        \
        "file %s: line %d (%s): precondition `%s' failed.", \
        __FILE__,                                           \
        __LINE__,                                           \
        __PRETTY_FUNCTION__,                                \
        #expr);                                             \
     ::print_stack_trace(2);                                \
     return;                                                \
 };               } while(0)
#define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
 if (!(expr))                                                   \
 {                                                              \
    fprintf(stderr,                                             \
        "file %s: line %d (%s): precondition `%s' failed.",     \
        __FILE__,                                               \
        __LINE__,                                               \
        __PRETTY_FUNCTION__,                                    \
        #expr);                                                 \
     ::print_stack_trace(2);                                    \
     return val;                                                \
 };               } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif

If I needed runtime checking of arguments, I'd do this:

char *doSomething(char *ptr)
{
    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                            // Goes away when debug off.

    if( ptr != NULL )
    {
       ...
    }

    return ptr;
}
查看更多
春风洒进眼中
5楼-- · 2019-01-02 19:49

you're asking about the difference between design-time and run-time errors.

asserts are 'hey programmer, this is broken' notifications, they're there to remind you of bugs you wouldn't have noticed when they happened.

exceptions are 'hey user, somethings gone wrong' notifications (obviously you can code to catch them so the user never gets told) but these are designed to occur at run time when Joe user is using the app.

So, if you think you can get all your bugs out, use exceptions only. If you think you can't..... use exceptions. You can still use debug asserts to make the number of exceptions less of course.

Don't forget that many of the preconditions will be user-supplied data, so you will need a good way of informing the user his data was no good. To do that, you'll often need to return error data down the call stack to the bits he is interacting with. Asserts will not be useful then - doubly so if your app is n-tier.

Lastly, I'd use neither - error codes are far superior for errors you think will occur regularly. :)

查看更多
千与千寻千般痛.
6楼-- · 2019-01-02 19:49

The previous answers are correct: use exceptions for public API functions. The only time you might wish to bend this rule is when the check is computationally expensive. In that case, you can put it in an assert.

If you think violation of that precondition is likely, keep it as an exception, or refactor the precondition away.

查看更多
妖精总统
7楼-- · 2019-01-02 19:55

I prefer the second one. While your tests may have run fine, Murphy says that something unexpected will go wrong. So, instead of getting an exception at the actual erroneous method call, you end up tracing out a NullPointerException (or equivalent) 10 stack frames deeper.

查看更多
登录 后发表回答