.
以下是阅读Ray Wenderlich的笔记。原文链接
这个简单的例子,完美把Swift
很多的知识点都说到了。Swift guard
的引入可以让我们不再写类似金字塔的错误处理逻辑,更方便调试。
Error handling in Swift has come a long way since the patterns in Swift 1 that were inspired by Objective-C. Major improvements in Swift 2 made the experience of handling unexpected states and conditions in your application more straightforward. These benefits continue in Swift 3, but there are no significant updates to error handling made in the latest version of the language. (Phew!)
Just like other common programming languages, preferred error handling techniques in Swift can vary, depending upon the type of error encountered, and the overall architecture of your app.
This tutorial will take you through a magical example involving wizards, witches, bats and toads to illustrate how best to deal with common failure scenarios. You’ll also look at how to upgrade error handling from projects written in earlier versions of the language and, finally, gaze into your crystal ball at the possible future of error handling in Swift!
Note: This tutorial assumes you’re familiar with Swift 3 syntax – particularly enumerations and optionals. If you need a refresher on these concepts, start with the What’s New in Swift 2 post by Greg Heo, and the other materials linked.
Time to dive straight in (from the the cauldron into the fire!) and discover the various charms of error handling in Swift 3!
Getting Started
There are two starter playgrounds for this tutorial, one for each section. Download Avoiding Errors with nil – Starter.playground and Avoiding Errors with Custom Handling – Starter.playground playgrounds.
Open up the Avoiding Errors with nil starter playground in Xcode.
Read through the code and you’ll see several classes, structs and enums that hold the magic for this tutorial.
Take note the following parts of the code:
1 | protocol Avatar { |
This protocol is applied to almost all classes and structs used throughout the tutorial to provide a visual representation of each object that can be printed to the console.
1 | enum MagicWords: String { |
This enumeration denotes magic words that can be used to create a Spell
.
1 | struct Spell { |
This is the basic building block for a Spell
. By default, you initialize its magic words to .abracadabra
.
Now that you’re acquainted with the basics of this supernatural world, you’re ready to start casting some spells.
Why Should I Care About Error Handling?
“Error handling is the art of failing gracefully.”
–Swift Apprentice, Chapter 22 (Error Handing)
Good error handling enhances the experience for end users as well as software maintainers by making it easier to identify issues, their causes and their associated severity. The more specific the error handling is throughout the code, the easier issues are to diagnose. Error handling also lets systems fail in an appropriate way so as not to frustrate or upset users.
But errors don’t always need to be handled. When they don’t, language features let you avoid certain classes of errors altogether. As a general rule, if you can avoid the possibility of an error, take that design path. If you can’t avoid a potential error condition, then explicit handling is your next best option.
Avoiding Swift Errors Using nil
Since Swift has elegant(优雅的) optional-handling capabilities, you can completely avoid the error condition where you expect a value, but no value is provided. As a clever programmer, you can manipulate this feature to intentionally return nil
in an error condition. This approach works best where you should take no action if you reach an error state; i.e. where you choose inaction over emergency action.
Two typical examples of avoiding Swift errors using nil
are failable initializers and guard statements.
避免使用nil
Failable Initializers
Failable initializers prevent the creation of an object unless sufficient information has been provided. Prior to the availability of these initializers in Swift (and in other languages!), this functionality was typically achieved via the Factory Method Pattern.
An example of this pattern in Swift can be seen in create:
:
当传入的参数不满足初始化的条件的时候返回nil
以表示初始化失败。
1 | static func create(withMagicWords words: String) -> Spell? { |
The above initializer tries to create a spell using the magic words provided, but if the words are not magical you return nil
instead.
Inspect the creation of the spells at the very bottom of this tutorial to see this behavior in action:
While first
successfully creates a spell using the magic words "abracadabra"
, "ascendio"
doesn’t have the same effect, and the return value of second
is nil
. (Hey, witches can’t win all the time).
Factory methods are an old-school programming style. There are better ways to achieve the same thing in Swift. You’ll update the Spell
extension to use a failable initializer instead of a factory method.
Delete create(_words:)
and replace it with the following:Swift
有类似的解决方案failable initializer
1 | init?(words: String) { |
Here you’ve simplified the code by not explicitly creating and returning the Spell
object.
The lines that assign first
and second
now throw compiler errors:
1 | let first = Spell.create(withMagicWords: "abracadabra") |
You’ll need to change these to use the new initializer. Replace the lines above with the following:
1 | let first = Spell(words: "abracadabra") |
After this, all errors should be fixed and the playground should compile without error. With this change your code is definitely tidier – but you can do even better! :]
Guard Statements
guard
is a quick way to assert that something is true: for example, if a value is > 0, or if a conditional can be unwrapped. You can then execute a block of code if the check fails.
guard
was introduced in Swift 2 and is typically used to (bubble, toil and trouble) bubble-up error handling through the call stack, where the error will eventually be handled. Guard statements allow early exit from a function or method; this makes it more clear which conditions need to be present for the rest of the processing logic to run.
To clean up Spell
‘s failable initializer further, edit it as shown below to use guard
:guard
的加入改变代码的逻辑,但是让代码更容易易读和流畅,也不需要像if语句一样最终形成金字塔的形状。
1 | init?(words: String) { |
With this change, there’s no need need for an else
clause on a separate line and and the failure case is more evident as it’s now at the top of the intializer. Also, the “golden path” is the least indented. The “golden path” is the path of execution that happens when everything goes as expected, i.e. no error. Being least indented makes it easy to read.
Note that the values of first
and second
Spell
constants haven’t changed, but the code is more more streamlined.
Avoiding Errors with Custom Handling
Having cleaned up the Spell
initializer and avoided some errors through the clever use of nil
, you’re ready to tackle some more intricate(错综复杂) error handling.
For the next section of this tutorial, open up Avoiding Errors with Custom Handling – Starter.playground.
Take note of the following features of the code:
1 | struct Spell { |
This is the Spell
initializer, updated to match the work you completed in the first section of this tutorial. Also note the presence of the Avatar
protocol, and a second failable initializer, which has been added for convenience.
1 | protocol Familiar: Avatar { |
The Familiar
protocol will be applied to various animals (such as bats and toads) further down in the playground.
Note: For those unfamiliar with the term familiar, this is a witch’s or wizard’s magical animal sidekick, which usually has human-like qualities. Think Hedwig from Harry Potter, or the flying monkeys in the Wizard of Oz.
This clearly isn’t Hedwig, but still cute nonetheless, no?
1 | struct Witch: Magical { |
上面代码的问题是因为多种条件的判断,导致代码成为金字塔形状,非常不方便调试。
Finally, the witch. Here you see the following:
- A
Witch
is initialized with a name and a familiar, or with a name, a familiar and a hat. - A
Witch
knows a finite number of spells, stored inspells
, which is an array ofSpell
objects. - A
Witch
seems to have a penchant for turning her familiar into a toad via the use of the.prestoChango
spell, withinturnFamiliarIntoToad()
.
Notice the length and amount of indentation in turnFamiliarIntoToad()
. Also, if anything goes wrong in the method, an entirely new toad will be returned. That seems like a confusing (and incorrect!) outcome for this particular spell. You’ll clean up this code significantly with custom error handling in the next section.
Refactoring to Use Swift Errors
“Swift provides first-class support for throwing, catching, propagating, and manipulating
recoverable errors at runtime.”
– The Swift Programming Language (Swift 3)
Not to be confused with the Temple of Doom, the Pyramid of Doom is an anti-pattern found in Swift and other languages that can require many levels of nested statements for control flow. It can be seen in turnFamiliarIntoToad()
above – note the six closing parentheses required to close out all the statements, trailing down on a diagonal. Reading code nested in this way requires excessive cognitive effort.
Guard statements, as you’ve seen earlier, and multiple simultaneous optional bindings can assist with the cleanup of pyramid-like code. The use of a do-catch
mechanism, however, eliminates the problem altogether by decoupling control flow from error state handling.
do-catch
mechanisms are often found near the following, related, keywords:
throws
do
catch
try
defer
Error
To see these mechanisms in action, you are going to throw multiple custom errors. First, you’ll define the states you wish to handle by listing out everything that could possibly go wrong as an enumeration.
Add the following code to your playground above the definition of Witch
:
1 | enum ChangoSpellError: Error { |
Note two things about ChangoSpellError
:
- It conforms to the
Error
protocol, a requirement for defining errors in Swift.//继承Error - In the
spellFailed
case, you can handily specify a custom reason for the spell failure with an associated value.//spellFailed
可以传递原因
Note: The ChangoSpellError
is named after the magical utterance of “Presto Chango!” – frequently used by a Witch
when attempting to change a familiar into a Toad
).
OK, ready to make some magic, my pretties? Excellent. Add throws
to the method signature, to indicate that errors may occur as a result of calling this method:
将trunFamiliarIntoToad
改写为会抛出的错误的函数。
1 | func turnFamiliarIntoToad() throws -> Toad { |
Update it as well on the Magical
protocol:
1 | protocol Magical: Avatar { |
Now that you have the error states listed, you will rework the turnFamiliarIntoToad()
method, one clause at a time.
Handling Hat Errors
First, modify the following statement to ensure the witch is wearing her all-important hat:
1 | if let hat = hat { |
…to the following:将if
改写为guard
1 | guard let hat = hat else { |
Note: Don’t forget to remove the associated }
at the bottom of the method, or else the playground will compile with errors!
The next line contains a boolean check, also associated with the hat:
1 | if hat.isMagical { |
You could choose to add a separate guard
statement to perform this check, but it would be clearer to group the checks together on a single line. As such, change the first guard
statement to match the following:
1 | guard let hat = hat, hat.isMagical else { |
Now remove the if hat.isMagical {
check altogether.
In the next section, you’ll continue to unravel the conditional pyramid.
Handling Familiar Errors
Next up, alter the statement that checks if the witch has a familiar:
1 | if let familiar = familiar { |
…to instead throw a .noFamiliar
error from another guard
statement:
1 | guard let familiar = familiar else { |
Ignore any errors that occur for the moment, as they will disappear with your next code change.
Handling Toad Errors
On the next line, the code returns the existing toad if the Witch tries to cast the turnFamiliarIntoToad()
spell on her unsuspecting amphibian, but an explicit error would better inform her of the mistake. Change the following:
1 | if let toad = familiar as? Toad { |
…to the following:
1 | if familiar is Toad { |
主意:这里的if后面用的is
而不是as
这个变化可以让你更简洁地检查协议的一致性,而无需要使用结果。is可以让你更简要的检查当前的值是不是某种类型。
1 | if let newType = oldType as ? Type { |
Note the change from as?
to is
lets you more succinctly check for conformance to the protocol without necessarily needing to use the result. The is
keyword can also be used for type comparison in a more general fashion. If you’re interested in learning more about is
and as
, check out the type casting section of The Swift Programming Language.
Move everything inside the else
clause outside of the else
clause, and delete the else
. It’s no longer necessary!
Handling Spell Errors
Finally, the hasSpell(_ type:)
call ensures that the Witch has the appropriate spell in her spellbook. Change the code below:
1 | if hasSpell(ofType: .prestoChango) { |
…to the following:
1 | guard hasSpell(ofType: .prestoChango) else { |
And now you can remove the final line of code which was a fail-safe. Remove this line:
1 | return Toad(name: "New Toad") |
You now have the following clean and tidy method, ready for use. I’ve provided a few additional comments to the code below, to further explain what the method is doing:
改写后的代码,呈流水形,更容易调试。
1 | func turnFamiliarIntoToad() throws -> Toad { |
You could have returned an optional from turnFamiliarIntoToad()
to indicate that “something went wrong while this spell was being performed”, but using custom errors more clearly expresses the error states and lets you react to them accordingly.
What Else Are Custom Errors Good For?
怎么处理错误
Now that you have a method to throw custom Swift errors, you need to handle them. The standard mechanism for doing this is called the do-catch
statement, which is similar to try-catch
mechanisms found in other languages such as Java.
Add the following code to the bottom of your playground:
1 | func exampleOne() { |
Here’s what that function does:
- Create the familiar for this witch. It’s a cat called Salem.
- Create the witch, called Sabrina.
- Attempt to turn the feline into a toad.
- Catch a
ChangoSpellError
error and handle the error appropriately. - Finally, catch all other errors and print out a nice message.
After you add the above, you’ll see a compiler error – time to fix that.
handle(spellError:)
has not yet been defined, so add the following code above the exampleOne()
function definition:
1 | func handle(spellError error: ChangoSpellError) { |
Finally, run the code by adding the following to the bottom of your playground:
1 | exampleOne() |
Reveal the Debug console by clicking the up arrow icon in the bottom left hand corner of the Xcode workspace so you can see the output from your playground:
Catching Errors
Below is a brief discussion of each of language feature used in the above code snippet.
catch
You can use pattern matching in Swift to handle specific errors or group themes of error types together.
The code above demonstrates several uses of catch
: one where you catch a specific ChangoSpell
error, and one that handles the remaining error cases.
try
You use try
in conjunction with do-catch
statements to clearly indicate which line or section of code may throw errors.
You can use try
in several different ways:
try
: standard usage within a clear and immediatedo-catch
statement. This is used above.try?
: handle an error by essentially ignoring it; if an error is thrown, the result of the statement will benil
.try!
: similar to the syntax used for force-unwrapping, this prefix creates the expectation that, in theory, a statement could throw an error – but in practice the error condition will never occur.try!
can be used for actions such as loading files, where you are certain the required media exists. Like force-unwrap, this construct should be used carefully.
Time to check out a try?
statement in action. Cut and paste the following code into the bottom of your playground:
1 | func exampleTwo() { |
Notice the difference with exampleOne
. Here you don’t care about the output of the particular error, but still capture the fact that one occurred. The Toad
was not created, so the value of newToad
is nil
.
Propagating Errors
throws
The throws
keyword is required in Swift if a function or method throws an error. Thrown errors are automatically propagated up the call stack, but letting errors bubble too far from their source is considered bad practice. Significant propagation of errors throughout a codebase increases the likelihood errors will escape appropriate handling, so throws
is a mandate to ensure propagation is documented in code – and remains evident to the coder.
rethrows
All examples you’ve seen so far have used throws
, but what about its counterpart rethrows
?
rethrows
tells the compiler that this function will only throw an error when its function parameter throws an error. A quick and magical example can be found below (no need to add this to the playground):
1 | func doSomethingMagical(magicalOperation: () throws -> MagicalResult) rethrows -> MagicalResult { |
Here doSomethingMagical(_:)
will only throw an error if the magicalOperation
provided to the function throws one. If it succeeds, it returns a MagicalResult
instead.
Manipulating Error Handling Behavior
defer
Although auto-propagation will serve you well in most cases, there are situations where you might want to manipulate the behavior of your application as an error travels up the call stack.
The defer
statement is a mechanism that permits a ‘cleanup’ action to be performed whenever the current scope is exited, such as when a method or function returns. It’s useful for managing resources that need to be tidied up whether or not the action was successful, and so becomes especially useful in an error handling context.
To see this in action, add the following method to the Witch
structure:
1 | func speak() { |
Add the following code to the bottom of the playground:
1 | func exampleThree() { |
In the debug console, you should see the witch cackle after everything she says.
Interestingly, defer
statements are executed in the opposite order to which they are written.
Add a second defer
statement to speak()
so that a Witch screeches, then cackles after everything she says:
1 | func speak() { |
Did the statements print in the order you expected? Ah, the magic of defer
!
More Fun with Errors
The inclusion of the above statements in Swift brings the language into line with many other popular languages and separates Swift from the NSError
-based approach found in Objective-C. Objective-C errors are, for the most part, directly translated, and the static analyzer in the compiler is excellent for helping you with which errors you need to catch, and when.
Although the do-catch
and related features have significant overhead in other languages, in Swift they are treated like any other statement. This ensures they remain efficient – and effective.
But just because you can create custom errors and throw them around, doesn’t necessarily mean that you should. You really should develop guidelines regarding when to throw and catch errors for each project you undertake. I’d recommend the following:
- Ensure error types are clearly named across your codebase.
- Use optionals where a single error state exists.
- Use custom errors where more than one error state exists.
- Don’t allow an error to propagate too far from its source.
The Future of Error Handling in Swift
Swift未来错误处理的发展趋势
A couple of ideas for advanced error handling are floating around various Swift forums. One of the most-discussed concepts is untyped propagation.
“…we believe that we can extend our current model to support untyped propagation for universal errors. Doing this well, and in particular doing it without completely sacrificing code size and performance, will take a significant amount of planning and insight.”
– from Swift 2.x Error Handling
Whether you enjoy the idea of a major error handling change in a future version of Swift, or are happy with where things are today, it’s nice to know that clean error handling is being actively discussed and improved as the language develops.
Where To Go From Here?
You can download the finished set of playgrounds here for this tutorial.
For further reading, I recommend the following articles, some of which have already been referenced throughout this tutorial:
- Swift Apprentice, Chapter 22 – Error Handling
- Failable Initializers
- Factory Method Pattern
- Pyramid of Doom
If you’re keen to see what may lie ahead in Swift, I recommend reading through the currently open proposals; see Swift Language Proposals for further details. If you’re feeling adventurous, why not submit your own? :]
Hopefully by now that you’ve been truly enchanted by error handling in Swift. If you have any questions or comments on this tutorial, please join the forum discussion below!