- PVSM.RU - https://www.pvsm.ru -
This article is an example of how we can do a research of the Swift Standard Library functions behavior building our knowledge not only on the Library documentation but also on its source code.
All events which programmers call "errors" can be separated into two types.
We process events of the first type in regular control flow. For example, we react to network failure by showing a message to a user and setting app for waiting of network connection recovery.
We try to find out and exclude events of the second type as early as possible before code goes to production. One of the approaches here is to run some runtime checks terminating program execution [1] in a debuggable sate and print message with an indication of where in a code the error has happened.
For example, a programmer may terminate execution if required initializer wasn't provided but was called. That will be inevitably noticed and fixed at the first test run.
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Another example is the switching between indices (let's assume that for some reason you can't use enumeration).
switch index {
case 0:
// something is done here
case 1:
// other thing is done here
case 2:
// and other thing is done here
default:
assertionFailure("Impossible index")
}
Again programmer is going to get crash during debugging here in order to inevitably notice a bug in indexing.
There are five terminating functions from the Swift Standard Library (as for Swift 4.2).
func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func preconditionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func assertionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never
Which of five terminating functions should we prefer?
Let's look at source code [2]. We can see right away the following:
_assertionFailure(_:_:file:line:flags:).Builtin.condfail(error._value) or Builtin.int_trap().fatalError(_:file:line) calls _assertionFailure(_:_:file:line:flags:) unconditionally._isReleaseAssertConfiguration()_isDebugAssertConfiguration()_isFastAssertConfiguration()Then let's look at documentation [1]. We can see right away the following.
fatalError(_:file:line) unconditionally prints a given message and stops execution [3].-Onone, -O, -Ounchecked. For example look at preconditionFailure(_:file:line:) documentation [4].SWIFT_OPTIMIZATION_LEVEL compiler build setting.-Osize — is introduced.-Onone (don't optimize)-O (optimize for speed)-Osize (optimize for size)-Ounchecked (switch off many compiler checks)We may conclude that configuration evaluated in four terminating functions is set by those build flags.
Although configuration evaluation functions are for internal usage some of them are public for testing purposes [6] so we may try them through CLI giving the following commands in Bash.
$ echo 'print(_isFastAssertConfiguration())' >conf.swift
$ swift conf.swift
false
$ swift -Onone conf.swift
false
$ swift -O conf.swift
false
$ swift -Osize conf.swift
false
$ swift -Ounchecked conf.swift
true
$ echo 'print(_isDebugAssertConfiguration())' >conf.swift
$ swift conf.swift
true
$ swift -Onone conf.swift
true
$ swift -O conf.swift
false
$ swift -Osize conf.swift
false
$ swift -Ounchecked conf.swift
false
Those tests and source code [2] inspection lead us to the following not very strict conclusions.
There are three mutually exclusive configurations.
-O or -Osize build flag.-Onone build flag or none optimization flags at all._isFastAssertConfiguration() is evaluated to true if -Ounchecked build flag is set. And although this function has a word "fast" in its name it has nothing to do with optimizing for speed -O build flag in spite of the misleading naming.NB: Those conclusions are not the strict definition of when debug builds or release builds are taking place. It's a more complex issue. But those conclusions are correct for the context of terminating functions usage.
-OuncheckedLet's look not at what is -Ounchecked flag for (it's irrelevant here) but at what is its role in the context of terminating functions usage.
precondition(_:_:file:line:) and assert(_:_:file:line:) says: "In -Ounchecked builds, condition is not evaluated, but the optimizer may assume that it always evaluates to true. Failure to satisfy that assumption is a serious programming error."preconditionFailure(_:file:line) and assertionFailure(_:file:line:) says: "In -Ounchecked builds, the optimizer may assume that this function is never called. Failure to satisfy that assumption is a serious programming error."_isFastAssertConfiguration() to true shouldn't happen. (If it does happen peculiar _conditionallyUnreachable() is called, see 136 line and 176 lines [2].Speaking more directly, you must not allow reachability of the following four terminating functions with -Ounchecked build flag set for your program.
precondition(_:_:file:line:)preconditionFailure(_:file:line)assert(_:_:file:line:)assertionFailure(_:file:line:)Use only fatalError(_:file:line) while applying -Ounchecked and at the same time considering that the point of your program with fatalError(_:file:line) instruction is reachable.
Two of terminating functions let us check for conditions. Source code [2] inspection let us see that if condition is failed then function behavior is the same as behavior of its respective cousin:
precondition(_:_:file:line:) becomes preconditionFailure(_:file:line),assert(_:_:file:line:) becomes assertionFailure(_:file:line:).That knowledge simplifies our further analysis.
Further documentation and source code inspection let us eventually formulate the following table.

It's clear now that the most important choice for a programmer is what should be a program behavior in release when runtime check reveals an error.
The key takeaway here is that assert(_:_:file:line:) and assertionFailure(_:file:line:) make the impact of program failure less severe. For example, iOS app may have corrupted UI (since some important runtime checks were failed) but won't crash.
But that scenario may easily be not the one you wanted. You have a choice.
Never Return TypeNever is used as a return type of functions that unconditionally throw an error, traps, or otherwise, do not terminate normally. Those kinds of functions do not actually return, they never return.
Among five terminating functions only preconditionFailure(_:file:line) and fatalError(_:file:line) return Never because only those two functions unconditionally stop program execution therefore never return.
Here is a nice example of utilizing Never type in some command line app. (Although this example doesn’t use Swift Standard Library terminating functions but standard C exit() function instead).
func printUsagePromptAndExit() -> Never {
print("Usage: command directory")
exit(1)
}
guard CommandLine.argc == 2 else {
printUsagePromptAndExit()
}
// ...
If printUsagePromptAndExit() returned Void instead of Never you would get a buildtime error with the message: "'guard' body must not fall through, consider using a 'return' or 'throw' to exit the scope". Using Never you are saying in advance that you never exit the scope and therefore compiler won't give you a buildtime error. Otherwise, you should add return at the end of the guard code block, which doesn't look nice.
fatalError(_:file:line) while applying -Ounchecked and at the same time considering that the point of your program with fatalError(_:file:line) instruction is reachable.assert(_:_:file:line:) and assertionFailure(_:file:line:) if you are afraid that runtime checks may fail somehow in release. At least your app won't crash.Never to make your code look neat.SWIFT_OPTIMIZATION_LEVEL build setting (from 11 minute).NSHipster's article about nature of Never [9]-Ounchecked.Автор: DmitriyAlexeev
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/xcode/309332
Ссылки в тексте:
[1] runtime checks terminating program execution: https://developer.apple.com/documentation/swift/swift_standard_library/debugging_and_reflection
[2] source code: https://github.com/apple/swift/blob/master/stdlib/public/core/Assert.swift
[3] unconditionally prints a given message and stops execution: https://developer.apple.com/documentation/swift/1538698-fatalerror
[4] documentation: https://developer.apple.com/documentation/swift/1539374-preconditionfailure
[5] documentation: https://help.apple.com/xcode/mac/10.0/#/itcaec37c2a6
[6] public for testing purposes: https://github.com/apple/swift/commit/d0697f2ac1092f74548c4df348194a3ee9ea7cda
[7] WWDC video "What's New in Swift": https://developer.apple.com/videos/play/wwdc2018/401
[8] How Never Works Internally in Swift: https://swiftrocks.com/how-never-works-internally-in-swift.html
[9] NSHipster's article about nature of Never: https://nshipster.com/never/
[10] Swift Forums discussion: https://forums.swift.org/t/deprecating-ounchecked/6928
[11] Источник: https://habr.com/ru/post/440884/?utm_campaign=440884
Нажмите здесь для печати.