W1262 Assertions

From Coder Merlin
Jump to navigation Jump to search

Definition of Assertion[edit]

In Swift, assert (the keyword for assertion) is a useful testing and debugging tool to help identify errors in code, especially during development.

Meaning[edit]

According to the Cambridge dictionary the definition of the word assert is: “a statement that you strongly believe is true.”

Apple Documentation[edit]

“Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code.”

Why We Need assert[edit]

In real life programming, two segments, namely the code associated with debugging and the code associated with final application are separated with very clear distinctions. This is done because in the debugging phase many possible exceptions and stress testing is done to check and analyze the plausible pool of outcomes of the concerned code. Naturally, in every programming languages, there are certain keywords and functionalities which assist the coder with debugging.

Imagine a situation in which you have prepared a piece of software with thousands of lines of code. Now imagine that there is a bug that you need to fix at line 15, but if you run the application like a user would, it will execute its entire length of code and this process will take place again and again which will take up time, resources etc. To avoid this, many of the functionalities or methods used for debugging can focus on a certain part of code. After carrying out the examination, it will immediately 'crash' the application depending if a certain condition is met, so that the application is no longer needs to execute through the entire program.

These concepts are useful to put checks in the application code to avoid the possibility of any harmful ramification if the user by some means reaches a situation where he/she should not be. In that case, crashing the application is executed with the functions like assert.

GoingDeeperIcon.png
Going Deeper
  • As the name implies, assertion makes it possible to assert if a certain condition is true before continuing the code's execution.

How does assert function work?[edit]

With debugging it is useful to have a function that helps to confirm/assert if a particular condition is reached before execution of the code with a boolean expression value.

If the boolean condition is asserted to be “true” the program executes; and stops if the program evaluates the condition to be “false” and points to the exact location where the problem arose.

Differences between Assert and other Constructs[edit]

It may seem that the functionality done by assert is something that can be just as easily done with language constructs like if, which is true to a certain extent, but with assert we get additional benefits and control during debugging.

The biggest advantage of assert is that whenever the assertion evaluates to false, the program immediately terminates. In the case of other conditional constructs, such as if, we see that depending on the result of the conditional statement, the program execution will take its path accordingly making it harder to detect exactly where the problem occurred. In the case of assert, it becomes very easy to identify the code segment where the program got terminated. Although we can always use a debugger to identify some issues, the advantage of assert is that we can locate the troubling segment without needing to run with a debugging environment.

Another big advantage of assert is greater control of the code since you may want to remove the assert functions once you are done with the debugging phase. in Swift, we have something called the Level of Optimization which acts to instruct the compiler how to compile a certain piece of code. Depending on its mode, the assert code segments can be excluded from the final applications. In the case of lower optimizations, the assertions are always checked during building, but in the case of higher optimizations, the assertions are omitted. With this flexibility, we get more control on the overall code structure and can very easily eliminate the assertions when the code is moved to production.

Controlling Compilation Method via Optimization Level[edit]

In swift, the optimization levels are controlled with the keyword SWIFT_OPTIMIZATION_LEVEL. There are three different modes which can be selected for this keyword.

SWIFT_OPTIMIZATION_LEVEL = -Onone      // default mode or debug
SWIFT_OPTIMIZATION_LEVEL = -O          // fast mode or release
SWIFT_OPTIMIZATION_LEVEL = -Ounchecked // unchecked release mode

By default (-Onone), all of the code gets complied in debug mode. In this mode, there is no optimization, which makes debugging and tracing the code easier. The second mode is release build whose equivalent optimization is set as -O. This mode of compilation works by stripping off the variables and method names from the code and then compiling it to yield better performance. With this mode of compilation, although the performance is better, debugging is more complicated than with lower levels of optimization. Lastly comes the unchecked release mode, which is the best option in terms of performance. But with this greater level of optimization comes with a drawback: while compiling your code with this setting, the compiler will set aside a number of checks and thus it comes with some inherent risk and is meant for advanced swift coders. This level of optimization requires a greater effort for debugging.

HintIcon.png
Helpful Hint

Compiling in the default mode, or debug optimization mode, makes it easier to debug your code. When the performance is the primary concern, unchecked release level of optimization is the best option.

Description[edit]

The assert function helps to enforce the use of valid data and ensures that the program terminates more predictably otherwise, which assists with debugging. Stopping the code execution as soon as an invalid state is detected also helps limit the damage caused by that invalid state, before compilation of the code.

Similar to assert functions, preconditions are also useful to locate or handle errors in the code. The assert functions is used in programs to detect mistakes and incorrect assumptions only in debug builds, while precondition functions are used to locate the errors in the code, both in debug and production builds.

Assert Functions[edit]

Primary Assert functions[edit]

Swift has two primary assert functions:

assert(_:_:file:line:)

and

assertionFailure(_:file:line:)

Function:

assert(_:_:file:line:)

Declaration:

func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)


Function:

assertionFailure(_:file:line:)

Declaration:

func assertionFailure(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)

Debugging examples with Assertions[edit]

assert

let age = -3
assert(age >= 0, "A person's age can't be less than zero.")
// This assertion fails because -3 is not >= 0.

In this example, the code execution will continue if the expression age >= 0 evaluates to true. Otherwise, the expression evaluates to false and the assertion fails, terminating the application.

assertionFailure The assertionFailure(_:file:line:) function is used to indicate that an assertion has failed. For example:

if age > 10 {
    print("You can ride both a bicycle or a ferris wheel")
} else if age >= 0 {
    print("You can ride only the ferris wheel.")
} else {
    assertionFailure("A person's age can't be less than zero.")
}

Assert is an invaluable tool to identify and catch unrecoverable errors.

Other Assert Functions[edit]

Apart from the two functions discussed above, there are three more types of assertion functions in Swift. They are:

  • precondition()

At first glance, both the assert precondition function look very similar, and they each accept the same input parameters. When executed they may appear to work similarly, but there is a difference, which we will get to later. First, let's have a look at this example first:

precondition(2 > 3, "2 wasn't greater than 3")
// Causes an assertion failure and prints "2 wasn't greater than 3" to the console.

In this case, the same outcome could have been obtained by using assert function. The difference between the two lies in the case where we use a mode of optimization other than the default. In the default mode, both of the functions will behave similarly. But in the case of higher optimization levels, no evaluation takes place for the function assert(_:_:file:line:). For achieving higher levels of optimization but with similar circumstances, the function precondition(_:_:file:line:) is still evaluated. In simpler words, if we need to check the assertion functions in the builds other than default builds (release build), we should use precondition(_:_:file:line:) instead of assert(_:_:file:line:).


  • preconditionFailure()

If we consider the assert(_:_:file:line:) and precondition(_:_:file:line:) as a pair in form of counterparts from certain perspectives, in the same way, we will have assertionFailure(_:file:line:) and preconditionFailure(_:file:line:) as the equivalent counterparts.

Following the previous couple, assertionFailure(_:file:line:) and preconditionFailure(_:file:line:) have exactly same set of parameters and the outcomes are the same with the default debug mode. In the case of release mode, the execution of the function takes place for preconditionFailure(_:file:line:) whereas for assertionFailure(_:file:line:) no execution takes place.

  • "fatalError()"

At first glance, preconditionFailure(_:file:line:) and "fatalError(_:file:line:)" look the same. Both of them accept the same parameters with an optional message and an optional file, and both of them even get executed in the optimization levels -Onone and -O. However, there are a few differences.

The difference can be demonstrated with an example: imagine we have a function that takes an integer index and looks up a certain String value depending on the given index, but depending on other conditions, we know that the index values should never be greater than 2. In that case we may write a function like this:

let index2 : Int = Int(arc4random_uniform(4))
func stringForIndex(index: Int) -> String {
    switch index {
    case 0, 1, 2:
        return "\(index)"
    default:
        assertionFailure("Unexpected index \(index)")
    }
}
stringForIndex(index2)

The compiler will raise an error when we run the code mentioning that the function does not return a String value in the case of the switch statement. This can be solved by calling the abort() function, just after the assertionFailure(_:file:line:) making the code look like:

let index2 : Int = Int(arc4random_uniform(4))
func stringForIndex(index: Int) -> String {
    switch index {
    case 0, 1, 2:
        return "\(index)"
    default:
        assertionFailure("Unexpected index \(index)")
        abort()
    }
}
stringForIndex(index2)

With the addition of the abort() function, we have solved the issue, but it is not an efficient way to handle the problem. Instead, we can use fatalError(_:file:line:). The major difference is that in the declaration of fatalError() there is an additional Swift attribute – the @noreturn attribute. With this attribute, the compiler is told not to return to the calling function which has invoked the method within which fatalError(_:file:line:) was called. This is done because the compiler should be instructed to terminate the program when encountering this. In such cases, the compiler will not check the return value from the default switch statement. So, while taking the advantage of this feature, we can re-write the code in our example as:

let index2 : Int = Int(arc4random_uniform(4))
func stringForIndex(index: Int) -> String {
    switch index {
    case 0, 1, 2:
        return "\(index)"
    default:
        fatalError("Unexpected index \(index)")
    }
}
stringForIndex(index2)
GoingDeeperIcon.png
Going Deeper
  • For all the assertion functions, namely assert(_:_:file:line:) , precondition(_:_:file:line:), assertionFailure(_:file:line:), preconditionFailure(_:file:line:) and "fatalError(_:file:line:)", the evaluation is skipped in the highest mode of optimization level i.e. unchecked release (-Ounchecked)

General Coding Practice[edit]

As previously mentioned, the whole concept of assertions is based on helping with debugging and troubleshooting. While different kind of assertion functions can become very useful while debugging a chunk of code, it can cause unpleasant experience for the end users. Though there is no prohibition against using assertions in production, many coders prefer to use it solely during debugging and to not leave it in production.

Key Concepts[edit]

KeyConceptsIcon.png
Key Concepts
  • Assertion is a set of basic functions used primarily for debugging in the Swift language.
  • Assertion functions are similar in that the application is terminated when pre-defined conditions are met.
  • There are five kinds of assertion functions: assert(_:_:file:line:) , precondition(_:_:file:line:), assertionFailure(_:file:line:), preconditionFailure(_:file:line:) and "fatalError(_:file:line:)"
  • Almost all of the functions have similar parameters except for "fatalError(_:file:line:)", which has a special argument- @noreturn
  • There are three types of optimization level which can be selected. They are are default mode, release mode and unchecked release mode. These modes differ in terms of efficiency of code execution.
  • We can consider assert(_:_:file:line:) and precondition(_:_:file:line:) as a pair just like assertionFailure(_:file:line:) and preconditionFailure(_:file:line:), another pair which can be considered respective counterparts.

Quiz[edit]

1 Assertions can be used in:

Both Phases
Only Development phase
Only Debug Phase

2 Swift assertions has two functions:

True
False

3 How many arguments a typical assert() function has:

4
5
3
1

4 How many Optimization levels Swift compiler supports:

1
3
2

5 Release mode optimization level is more efficient than debug mode optimization level:

False
True

6 Unchecked release mode is represented as -O:

False
True

7 What is the difference between assertionFailure() and preconditionFailure():

The method is evaluated in Release Mode in case of later
The method is evaluated in Unchecked release Mode in case of later
No difference
The method is evaluated in Debug Mode in case of later

8 fatalError() always returns a value:

True
False

9 precondition() is mainly used at the time of public documentation:

False
True

10 assert() is mainly used to check your code for internal errors

True
False


References[edit]

  • [1] (Alec O'Connor)
  • [2] (Apple Documentation for Swift Assert)
  • [3] (Apple Documentation for Swift Assertfailure)
  • [4] SWIFT Documentation
  • Tjeerd in 't Veen, Swift in Depth, Publisher(s): Manning Publications ISBN: 9781617295188


Designed with pride in Silicon Valley, CA, USA