Error handling in Swift is inherited from O-c, but Swift has changed dramatically since the 1.0. Important changes occurred at Swift 2, which pioneered the use of "handling unusual states and conditions" to make your app easier.
Similar to other programming languages, in Swift, choosing which error-handling technology to use depends on the specific type of error and the overall architecture of the app.
This tutorial will demonstrate a "magic", in this case, not only wizards, witches and bats, but also toads to demonstrate best practices in common error handling. You can also see how to upgrade the error handling that was written with an earlier version of Swift, and eventually use your crystal ball to see what will happen to future Swift's error handling.
Note: This tutorial assumes that you are familiar with the Swift 2 syntax-especially enumerations and nullable. If you don't know these concepts, please read Greg Heo's What ' New in Swift 2 post.
Well, let's begin to appreciate the charm of SWIFT2 's error handling.
Begin
This tutorial has two start items (playground). One section, respectively: Avoiding-errors-with-nil-starter.playground and Avoiding-errors-with-custom-handling-starter.playground.
Open the first playground file.
Reading the code, you will find several classes, structs, and enumerations.
Note the following code:
Protocol Magicaltutorialobject {
var avatar:string {Get}
}
This protocol is used by all classes and structs in the tutorial and is used to provide a String object that can print each object to the console.
Enum Magicwords:string {
Case Abracadbra = "Abracadabra"
Case Alakazam = "Alakazam"
Case HocusPocus = "Hocus Pocus"
Case Prestochango = "Presto Chango"
}
This enumeration is used to denote a "spell", which will be "read" (spell).
struct Spell:magicaltutorialobject {
var magicwords:magicwords =. Abracadbra
var avatar = "*"
}
This structure is used to "read" the spell. By default, the initial value of its Magicwords property is Abracadabra.
You already know the basics of this magical world and you can start practicing spells.
Why do I have to do error handling?
"Error handling is an art that makes mistakes elegant." ”
–swift Apprentice, 21st chapter (Error handling)
Good error handling enhances the user experience, making it easier for software maintainers to spot problems, understand the cause of the error, and the severity of the error. Diagnosing problems becomes easier when the error processing in the code is ubiquitous. Error handling also allows the system to terminate execution in the right way, avoiding unnecessary trouble for users.
Of course, not all errors need to be addressed. When errors are not processed, the language attribute also carries out some level of error handling. Generally, if you can avoid mistakes, try to avoid them. If you can't avoid it, the best thing to do is to handle it incorrectly.
Avoid Swift references as null errors
As Swift has an elegant air handling mechanism, it is similar to this error: there is no value in the place where you think it is worth-it can be avoided altogether. As a smart programmer, you can take advantage of this feature to deliberately return a nil in the event of an error. This approach works well if you don't want to take any action in the event of an error, such as taking a failure action at the time of the accident.
The two typical examples of avoiding Swift references are the initialization methods that allow failure, and the guard statement.
Initialization method that allows failed
Allow failed initialization methods to prevent you from creating objects that do not fully meet the creation criteria. This method is usually used in the factory method design pattern before Swift 2 (which is already in other languages).
This design pattern in Swift is embodied in the Createwithmagicwords:
static func createwithmagicwords (words:string)-> Spell? {
If Let incantation = Magicwords (rawvalue:words) {
var spell = spell ()
Spell.magicwords = incantation
return spell
}
else {
return Nil
}
}
The above initialization method attempts to create a Spell object with the specified spell, and returns a nil object if the words argument supplied to it is not a valid spell.
At the bottom of this tutorial, check the Spell object's creation statement, and you'll see:
The first statement successfully created a Spell object with "Abracadabra", but the second sentence failed with "Ascendio" and returned a nil object. (Well, wizards don't always succeed in reciting spells.)
The factory approach is an antiquated programming style. In fact, we can have a better choice in Swift. You can modify the factory method in Spell to "allow failed initialization methods."
Delete Createwithmagicwords (_:) and replaced by:
Init? (words:string) {
If Let incantation = Magicwords (rawvalue:words) {
Self.magicwords = incantation
}
else {
return Nil
}
}
Here, in this method declaration, we do not explicitly create and return a Spell object.
Oh, there's a compilation error in these two sentences:
let-i = Spell.createwithmagicwords ("Abracadabra")
Let second = Spell.createwithmagicwords ("Ascendio")
You need to modify them to invoke the new method. Modify the above statement to:
let-i = Spell (words: "Abracadabra")
Let second = Spell (words: "Ascendio")
In this way, the error disappears and the playground compiles successfully. This change makes your code cleaner--but you have a better solution!
Guard statement
A guard statement is a better way to assert that some cases are true: for example, to determine if a value is greater than 0, or to determine whether a value can be decompressed. If this situation is not satisfied, you can execute the statement block.
Guard statements are introduced in Swift 2 and are typically used for bubbling error handling in the call stack, in which errors are processed at the end. Guard statements can exit from a method/function as early as possible, which is simpler than the need to judge a condition to satisfy the rest of the logic.
Modify the Spell initialization method that is allowed to fail to the guard statement:
Init? (words:string) {
Guard Let incantation = Magicwords (rawvalue:words) Else {
return Nil
}
Self.magicwords = incantation
}
Here, we do not need to place else on a separate line, and the processing of the assertion failure becomes conspicuous because it is placed more at the head of the method. At the same time, the "Golden path" indents least. "Golden Path" refers to the execution path when everything is expected to occur without errors. The least indentation makes it easier to see.
Note that the code becomes more rationalized, although there will be no change in the final value of the second.
Customization of Errors
After completing the Spell initialization method and using nil to avoid certain errors, you will learn some of the more advanced error handling.
For the second part of this tutorial, open avoiding Errors with Custom handling–starter.playground.
Take a look at the code:
struct Spell:magicaltutorialobject {
var magicwords:magicwords =. Abracadbra
var avatar = "*"
Init? (words:string) {
Guard Let incantation = Magicwords (rawvalue:words) Else {
return Nil
}
Self.magicwords = incantation
}
Init? (magicwords:magicwords) {
Self.magicwords = Magicwords
}
}
This is the initialization method of the Spell, which is modified on the basis of the first part of the content. Note the use of the Magicaltutorialobject protocol, and the second initialization method that allows failure, for the convenience of adding it.
Protocol Familiar:magicaltutorialobject {
var noise:string {Get}
var name:string? {Get Set}
Init ()
Init (name:string?)
}
The familiar agreement will be used in a variety of animals (such as bats and toads).
Note: Familiar means that the servant, the wizard or the Witch of the Animal Elves, has the characteristics of the people. For example, "Harry Potter" in the Owl (named Hedwig), or "The Wizard of Oz," the Flying monkeys.
Although it's not hewig, it's still pretty, isn't it?
struct Witch:magicalbeing {
var avatar = "*"
var name:string?
var familiar:familiar?
var spells: [Spell] = []
var hat:hat?
Init (name:string?, Familiar:familiar?) {
Self.name = Name
Self.familiar = Familiar
If let S = Spell (magicwords:. Prestochango) {
Self.spells = [s]
}
}
Init (name:string, Familiar:familiar, hat:hat?) {
Self.init (Name:name, Familiar:familiar)
Self.hat = Hat
}
Func turnfamiliarintotoad ()-> Toad {
If let hat = hat {
If hat.ismagical {//When have you ever seen a witch perform a spell without her magical hat on?:]
If let familiar = familiar {//Check if witch has a familiar
If let Toad = familiar as? Toad {//Check if familiar is already a toad-no magic required
Return Toad
} else {
If Hasspelloftype (. Prestochango) {
If let name = familiar.name {
Return Toad (Name:name)
}
}
}
}
}
}
Return Toad (Name: "New Toad")//The is a entirely New Toad.
}
Func Hasspelloftype (type:magicwords)-> Bool {//Check if witch currently has appropriate spell in their spellbook
return Spells.contains {$0.magicwords = = = Type}
}
}
Finally, a witch. Please look below:
• Witches ' initialization requires a name and a genie, or a name, an elf and a hat.
The witch will recite many spells, save with a spells, that is, a Spell array.
The witch has a hobby, when she reads the spell: "Prestochango", her elf will be turned into a toad, this action with Turnfamiliarintotoad () method
Note the indentation in the Turnfamiliarintotoad () method. In this method, if any errors are encountered, a new toad is returned. This looks a little strange (it's wrong!) )。 In the next section, you will use custom error handling to solve this problem.
Refactoring with Swift error
Swift provides support for run-time throwing, capturing, passing, and manipulating recoverable type errors.
-"The Swift programming Language (Swift 2.2)"
Unlike the "Temple of Death", in Swift or other languages, the "Pyramid of Doom" is another model of the opposite. Using this model, multilevel nesting is used in the control flow. The Turnfamiliarintotoad () method above, for example, uses 6} Symbols to end nesting, basically forming a diagonal line. Such code is quite laborious to read.
Pyramid of Doom
The "Pyramid of Doom" code can be avoided by using the previously mentioned guard statement and the nullable binding. The Do-catch mechanism can decouple error handling from the control flow, from reducing the emergence of the "Doom Pyramid".
Key words commonly used in do-catch mechanisms include:
throws
do
catch
try
defer
ErrorType
To try the Do-catch mechanism, you will throw multiple custom errors. First, you need to define an enumeration that lists all the states you want to process, and these States may indicate that something is wrong in a place.
Add the following code above the Witch class definition:
Enum Changospellerror:errortype {
Case Hatmissingornotmagical
Case Nofamiliar
Case Familiaralreadyatoad
Case Spellfailed (reason:string)
Case Spellnotknowntowitch
}
There are two points to note about Changospellerror:
• It uses the ErrorType protocol, which is necessary. In Swift, ErrorType shows that this is a mistake.
• In the case branch of spellfailed, you can specify a custom reason why the spell is wrong.
Note: Changospellerror's name comes from the spell "Presto chango!" -a witch's mantra when she turns the genie into a toad.
All right, honey, let's Get your magic. Very good. Adds a throws keyword to the method signature, indicating that the method invocation may throw an error:
Func turnfamiliarintotoad () throws-> Toad {
Update it as the "magicalbeing" protocol:
Protocol Magicalbeing:magicaltutorialobject {
var name:string? {Get Set}
var spells: [Spell] {Get Set}
Func turnfamiliarintotoad () throws-> Toad
}
Now that you have the list of error states, you will need to rewrite the Turnfamiliarintotoad () method to write different processing statements for each error type.
The mistake of handling the hat
First, modify the following statement to make sure the witch has worn the magic hat she never leaves.
To modify the previous code:
If let hat = hat {
Modified code:
Guard Let hat = Hat else {
Throw changospellerror.hatmissingornotmagical
}
Note: Do not forget to delete the corresponding} at the bottom of the method. Otherwise playground will compile the error!
The next sentence is to check a Boolean value, which is also related to the Sorcerer's hat:
If hat.ismagical {
You can check again with a guard statement, or you can combine two checks into a guard statement-which is clearly much clearer and simpler. So the first guard statement is modified to:
Guard Let hat = hat where hat.ismagical else {
Throw changospellerror.hatmissingornotmagical
}
Then remove the If hat.ismagical {.
In the next section, you will continue to crack the "pyramid" problem.
Handling the wizard's errors
Then, judge whether the Sorcerer has an elf:
If let familiar = familiar {
Use this sentence to throw one. Nofamiliar Error to replace:
Guard let familiar = familiar else {
Throw Changospellerror.nofamiliar
}
Ignore any errors that occur at this time, because the next code will make them disappear.
Dealing with Toad's mistakes
Next, if the witch is trying to use the Turnfamiliarintotoad () method to discover that her elf is actually a toad, then return the existing toad. But the better thing here is to use a mistake to show the situation. The following code:
If let Toad = familiar as? Toad {
Return Toad
}
Modified to:
If familiar is Toad {
Throw Changospellerror.familiaralreadyatoad
}
Notice that we will be as? Changed to IS. This is more concise when you need to check whether an object can be converted to a protocol, but without the need to use the transformation results. The IS keyword can also be a more generic way of comparing types. Read the "Type conversion" section of the Swift programming Language if you want more information.
It is useless to move the code within else to an else, and then delete the Else statement.
The mistake of handling spells
Finally, the Hasspelloftype (type:) is invoked. method to check the witch's Magic Book does have a corresponding spell. The following code:
If Hasspelloftype (. Prestochango) {
If let Toad = f as? Toad {
Return Toad
}
}
Modified to:
Guard Hasspelloftype (. Prestochango) Else {
Throw Changospellerror.spellnotknowntowitch
}
Guard Let name = Familiar.name else {
Let reason = "familiar doesn ' t have a name."
Throw changospellerror.spellfailed (Reason:reason)
}
Return Toad (Name:name)
Now, delete the last line of unsafe code. Which is the line:
Return Toad (name: "New Toad")
Now, your approach has become clearer and neater, and has been able to use it. I added a comment in the above code to explain the work done by this method:
Func turnfamiliarintotoad () throws-> Toad {
When have your ever seen a witch perform a spell without her magical hat on? :]
Guard Let hat = hat where hat.ismagical else {
Throw changospellerror.hatmissingornotmagical
}
Check if Witch has a familiar
Guard let familiar = familiar else {
Throw Changospellerror.nofamiliar
}
Check If familiar is already a toad-if so, why are you casting the spell?
If familiar is Toad {
Throw Changospellerror.familiaralreadyatoad
}
Guard Hasspelloftype (. Prestochango) Else {
Throw Changospellerror.spellnotknowntowitch
}
Check if the familiar has a name
Guard Let name = Familiar.name else {
Let reason = "familiar doesn ' t have a name."
Throw changospellerror.spellfailed (Reason:reason)
}
It All Checks out! Return a toad with the same name as the witch ' s familiar
Return Toad (Name:name)
}
You have returned a nullable expression in the Turnfamiliarintotoad () method to indicate "errors in the mantra", but use custom errors to express the state of the error more clearly so that you can take the corresponding action according to these States.
What are the benefits of a custom error?
Now that you have a way to throw a custom Swift error, you need to deal with them. The next standard action is to use the Do-catch statement, which is like the Try-catch statement in languages such as Java.
Add the following code at the bottom of the playground:
Func Exampleone () {
Print ("")//Add an empty line in the debug area
1
Let Salem = Cat (name: "Salem Saberhagen")
Salem.speak ()
2
Let Witchone = Witch (name: "Sabrina", Familiar:salem)
do {
3
Try Witchone.turnfamiliarintotoad ()
}
4
Catch let error as changospellerror {
Handlespellerror (Error)
}
5
catch {
Print ("Something went wrong, are you feeling OK?")
}
}
Here is an explanation of this method:
• The creation of the Witch's Elf, its name is Salem.
• Create a witch named Sabrina.
• Try to turn the cat into a toad.
• Capture Changospellerror and handle accordingly.
• Capture other errors and print friendly information.
When you've finished writing the code, you'll see a compilation error--Let's take care of it.
The Handlespellerror () method is not yet defined, adding this method above the Exampleone () method:
Func Handlespellerror (error:changospellerror) {
Let prefix = "Spell Failed."
Switch Error {
Case. Hatmissingornotmagical:
Print ("\ (prefix) Did You forget your hat, or does it need batteries?")
Case. Familiaralreadyatoad:
Print ("\ (prefix) Why are you trying to change a Toad into a Toad?")
Default
Print (prefix)
}
}
Finally, this method is finally implemented in playground:
Exampleone ()
Click the up arrow in the lower left corner of the Xcode workspace to open the Debug console and you will see the playground output:
Catch error
The following is a brief discussion of each syntax feature in the preceding code.
Catch
You can use Swift's template matching to handle some kind of error, or to group the type of error.
The preceding code illustrates two uses of catch: one for catching Changospell errors, and one for catching the rest of the error.
Try
Try is used with the Do-catch statement to clearly locate which line of statements or blocks of code will throw an error.
The try statement has several different uses, one of which is used:
try: Standard usage, used in simple, immediate do-catch statements. Is the usage in the preceding code.
Try: Handles errors to ignore the way the error is, and if there are errors thrown, the result of this statement is nil.
try!: Similar to forced unpack, this prefix creates the desired object, which theoretically throws an error, but in practice this error never happens. try! Can be used to perform the actions of loading files, especially if you know clearly that the file is definitely there. As with the previous unpack, the use of this structure requires special care.
Let's experience a try? of use. Copy and paste the following code to the bottom of the playgournd:
Func Exampletwo () {
Print ("")//Add an empty line in the debug area
Let Toad = Toad (name: "Mr Toad")
Toad.speak ()
Let hat = Hat ()
Let Witchtwo = Witch (name: "Elphaba", Familiar:toad, Hat:hat)
Let Newtoad = try? Witchtwo.turnfamiliarintotoad ()
If Newtoad!= nil {//Same logic as:if Let _ = Newtoad
Print ("Successfully changed familiar into Toad.")
}
else {
Print ("Spell failed.")
}
}
Pay attention to different places with Exampleone. Here we do not need to know what the specific error output is, just when they are thrown to capture them. The Toad object will eventually create a failure, so the newtoad value should be nil.
Delivery error
Throws
In Swift, if an error is thrown in a method or function code, you must use the throws keyword. The thrown error is automatically passed in the call stack, but it is not a good idea to let the error bubble up too much from the scene. Flooding a large number of errors in the code base increases the likelihood that errors will not be handled correctly, so throws is mandatory to ensure that the error is recorded by the code-and is obvious to programmers.
Rethrows
Are the examples you see at the moment about throws, without its brother rethrows?
Rethrows tells the compiler that the function throws an error, and its arguments throw an error. Here's an example (no need to add it to your playground):
Func dosomethingmagical (magicaloperation: () throws-> Magicalresult) rethrows-> Magicalresult {
Return try Magicaloperation ()
}
This method only throws the error that the Magicaloperation argument throws. If successful, it returns a Magicalresult object.
Manipulating the behavior of error handling
Defer
While in most cases, we allow errors to propagate automatically, in some cases you may want to control the behavior of the app when the error is passed in the call stack.
The defer statement provides a mechanism for you to perform some "cleanup" actions at the end of the current scope, such as when a method or function returns. It can clean up some resources, regardless of whether the action succeeds or fails, especially in the context of error handling.
To test this behavior, add the following methods to the witch structure:
Func speak () {
Defer {
Print ("*cackles*")
}
Print ("Hello my Pretties.")
}
Add code at the bottom of the playground:
Func Examplethree () {
Print ("")//Add an empty line in the debug area
Let Witchthree = Witch (name: "Hermione", Familiar:nil, Hat:nil)
Witchthree.speak ()
}
Examplethree ()
At the Debug console, you'll see the witch "giggle" after every word you say (cackles).
Interestingly, the order of execution of the defer statements is the reverse of the writing order.
Add another defer statement in the Speak () method so that when the witch finishes a sentence, it screams first and then sends out "giggle" laughter.
Func speak () {
Defer {
Print ("*cackles*")
}
Defer {
Print ("*screeches*")
}
Print ("Hello my Pretties.")
}
is the print order as you think? Oh, the magical defer!.
Other things related to the mistake
In short, Swift has stood on the same line as other mainstream languages, and Swift is no longer using the O-c nserror error-handling mechanism. O-c errors are often converted, and the static parser in the compiler helps you do a good job of injecting to catch what bugs and errors happen when.
Although Do-catch and related features have very little overhead in other languages, they are considered exactly the same as other statements in Swift. This keeps them economical and efficient.
Although you can create custom errors and throw them at will, it doesn't mean you should. In each project, when you need to throw and catch errors, you should follow a certain development guide. I suggest:
• Regardless of the code base, your error type needs to be named clearly.
• When there is only one error state, use the nullable.
• Use custom errors in more than one error state.
• Don't let errors pass from the wrong scene to too far away.
Future error handling by Swift
In each of the big Swift forums, there are many ideas about the future of error handling. One of the most discussed is the delivery of no type.
“... We feel that support for no type delivery should be added to the current processing model to address common-type errors. Doing this, especially without adding code size and performance costs, requires sufficient determination and foresight. Therefore, this is seen as a task after Swift 2.0. ”
–from Swift 2.x Error handling
Whether you like it or not, the error handling in Swift 3 is bound to make a big difference, or you just care about what's in front of you, and you need to know that the clear error-handling mechanism is being hotly discussed and improved as the language evolves.