Note: this post is part of a series about the Swift programming language,introduced at WWDC 2014. I’m no more experienced in Swift than anyone else outside Apple,but I learn best by coding and talking through a problem. If there’s a better way to approach some of these topics,get in touchon Twitter!
We’ve had a little over two weeks to play with theSwift programming languagenow,and one sharp edge that keeps coming up is the language’s inclusion of what they call “optional types”. The vast majority of Objective-C developers are familiar with the use ofnil
to indicate a nonexistent value,but communicating that kind of information through a variable’s type is a bit more foreign.
In this post,we’ll have an introductory discussion about how Swift provides optional types,go over a couple of implementation details,and point out a few tough spots in the optional system.
Types,Maybe?
Before we dive into code,let’s talk a bit about what it means for a type to be optional. A lot of the variables we’ll encounter will have a “regular,” non-optional type; these range from everyday value types (likeInt
,Bool
,orString
) to more complex class types (such asUIView
).
When we declare variables of these types,Swift requires thatthey always have a value. In fact,this requirement is so strict that attempting to use a variable before initializing it with a value is a compile error:
- var x: Int
- println(x) // Error: Variable 'x' used before being initialized
This may seem frustrating on the surface,but is actually incredibly helpful in the long run: by preventing this code from compiling,Swift is eliminating an entire class of potential runtime errors that would arise from using an uninitialized value. In some cases,this requirement is even more strict – if we were to uselet
instead ofvar
,we’d find ourselves facing an error on the declaration itself,rather than on the first call site:
Naturally,we can silence this error by providing a value forx
:
However,astute developers will notice that we can’t providex
anil
value at initialization time:
This kind of code produces a rather opaque error about finding an overload for__conversion
,but what it really means is simpler: we can’t provide anil
value for variables of a non-optional type. Sincex
is a plainHighlighter-rouge" style="margin:0px 3px; padding:1px 3px; background-color:rgb(221,it must have an
Int
value,whichnil
is not.
This is where optional types come in. Swift lets you make virtually every type optional by appending a question mark to the type name. For example,we can shoehorn anil
value into ourx
from earlier just by tacking that?
onto theInt
type in the declaration:
var x: Int? = nil
println(x) // prints "nil"
x = 42
println(x) // prints "42"
At this point,we’ve come around to what most developers would expect in Objective-C: object variables can have a “real” value ornil
,and it’s just up to you to check which case your code is handling.
Little Boxes
“But wait,” you say,“Int is avaluetype,not an object! How can I usenil
for a value? There was no such thing for NSInteger…”
Well,you’re right. NSInteger didn’t have a
,monospace; font-size:0.9em">NSIntegerMin,monospace; font-size:0.9em">NSIntegerMax,andnil
value (or,rather,usingnil
with the right coercion would get you an integer with a value of 0). Instead,we defined a ton of marker values that meant “no value:”0
,monospace; font-size:0.9em">1NSNotFound
all mean “nothing” in some API.
When you stop to think about it,this is really a limitation: by not having a consistent,defined way of saying “no integer value,” we’re layering a tiny bit of additional complexity around any use of such a value,then attempting to paper over it with documentation. Want to find an object in an array? Well,if that object doesn’t exist,you getNSNotFound
– but if you try to find a nonexistent row in a table view,monospace; font-size:0.9em">-1instead.
Swift provides a cleaner way around this problem with the kind of optional types we describe above. How can it work with any type,though? Well,under the hood,an optional type is,well,just a type:
The above is the actual definition ofOptional
in the Swift core library (slightly abridged for clarity). Swift defines anew typecalled Optional that always has exactly one of two values: a defined “nothing” value calledNone
,or a wrapped-up value of some other typeT
. It’s as if Swift can take regular values and place them inside a Box,which may or may not be empty:
In this example,the first integer is a plainInt
type. The second and third,though,are both of typeOptional<Int>
– or,for short,monospace; font-size:0.9em">Int?. Notice that the third value here is actually an “empty Box” (theNone
value),even though its type isInt?
.
This ability,to pass aroundNone
anywhere an optional type can go,is how Swift can provide things likenil
for value types likeInt
(or,for that matter,anytype,whether value or reference). Since this value will have the same type as a “real” value wrapped up inOptional
,they can both be represented in the same variable without trying to rely on special values to stand in for the concept of “no value.”
Unwrapping
This poses a problem of its own,though. Now that we know optionals are their own type,we realize that they can’t be passed anywhere their underlying type can:
We need some way of getting at the value inside an optional’s Box – and,checking whether such a value exists at all! Thankfully,Swift has us covered with the!
operator,which extracts the value out of an optional:
This works great for optionals that have a value. But what about those that don’t?
The!
operator only applies to optionals that have an actual value inside them. If your optional hasnil
(an alias for.None
),it can’t be unwrapped and will throw a runtime error.
Let’s make our code a bit smarter. Instead of unconditionally unwrapping our optional value,we can check whether the value isnil
first – much like we might have done in Objective-C.
But what if we goty
from another method altogether,instead of defining it locally? It would feel a bit verbose to require a newlet
orvar
statement wherever we intend to call afunc
,then immediately check that just-declared variable.
Swift has us covered here too,with a Syntax calledoptional binding. By combining anif
and alet
statement,we can write a concise one-line check for a newly-bound variable that is only conjured into existence if there’s a real value to go along with it:
You may have noticed that here,we don’t even need to explicitly unBoxy
using the!
operator. This is another convenience we get for free by using optional binding: the bound variable is of the underlying type (in this caseInt
),instead of keeping the wrappingOptional
type. Since we’re sure such a value exists,we can access it directly inside the body of theif
statement,rather than having to unwrap it by hand.
Chaining
Now we’ve built up a concise Syntax for checking and unwrapping optional variables. But what about calling methods on those variables? Your program might have some custom classes – most do,after all – and you could want to call a method on an variable that might be an instance,or might benil
. In Objective-C,trying the latter would just give you anothernil
-like value right back.
Thankfully,Swift anticipated this case too. Developers can make use ofoptional chainingto call methods on potentially-nil
objects:
By sticking a?
between the variable name and method call,we can indicate that we want either a real answer back (in the event thaty
is a valid instance) or anothernil
(in the case thaty
is itselfnil
). This should feel very familiar to Objective-C pros: it’s exactly what that language would do in this situation.
The one caveat: the type of the returned value willalwaysbe optional,even if the method itself declares a non-optional return type. Since the value being computed could becomenil
at any point along the chain (if the object being called isnil
),the return value has to be prepared for that possibility,and the only type we have in Swift capable of carrying anil
value is an optional. Consider:
Even thoughsomeMethod()
is declared to return anz
gets typeOptional<Int>
because we used optional chaining to call the method.
This might seem like a hassle,but can actually be helpful,especially when combined with optional binding from above. If we stick with the same class definition,we can try something like this:
This remains concise while still dealing with all the varIoUs concerns we might have:
- If
y
isnil
(as it is here),the optional chaining will still allow us to write this code without a type error - If
nil
orsomeMethod()
returnsHighlighter-rouge" style="margin:0px 3px; padding:1px 3px; background-color:rgb(221,the optional binding will catch that case and avoid giving us a
nil
value for non-optionalz
- In the event we do get a
z
,we’re not required to hand-unwrap it because it’s optionally bound
All in all,this is a pretty clean system for passing aroundnil
values for just about any type. We get some extra type safety out of the deal,avoid using specially defined values,and can still be just as concise as Objective-C – if not more!
Rough Edges
That’s not to say the optional system isn’t without its quirks,though. A few rough edges in early versions of Swift can lead unwary developers into unexpected situations.
Unary?
operator
It’s (currently) valid Swift to take an optional variable and throw a?
at the end. However,unlike the unwrapping operator!
,appending?
doesn’t actually affect the variable in any way: it is still optional,and surroundingif
checks will still look to see if the variable isHighlighter-rouge" style="margin:0px 3px; padding:1px 3px; background-color:rgb(221,rather than evaluating its contained truth value (if any).
This can cause extra trouble when combined with…
Optional<Bool>
Since the Optional type is defined using generics – that is,it can wrap any other type in the language – it’s possible to construct an optional boolean variable. In fact,it’s virtually mandatory the language allow this: to special-caseBool
to disallow optionals would be an exceptional change,requiring serIoUs modifications to the language or the Optional type.
. (What the latter means is rather context-dependent.) This can be very misleading,when combined with an That does,however,lead to a way developers can construct a kind of three-state variable: an
,monospace; font-size:0.9em">nilOptional<Bool>
can betrue
,monospace; font-size:0.9em">falseif
check:
Since theif
in this case checks whetherx
isHighlighter-rouge" style="margin:0px 3px; padding:1px 3px; background-color:rgb(221,not the underlying truth value of
x!
,this code snippet prints “true”. Even worse,it’s possible to write the same code with a tiny tweak:
let x: Optional<Bool> = .Some(false)
if x? {
println("true")
} else {
println("false")
}
// prints "true"
As discussed above,the unary?
operator has no effect in this context – what looks like an unwrapped optional boolean is actually still optional,leading the snippet to continue printing “true.” (To really unwrapx
,we need to use the!
operator.)
Implicit implicit unwrapping
Interface Builder is an important component of most iOS and Mac apps,and as Xcode continues to develop new features,it will only grow more so. However,not everything is possible in IB; developers will often hook up IBOutlets to gain programmatic control over different UI elements.
In Swift,the annotation to expose a variable to IB is nearly identical to its Objective-C counterpart:
class MyViewController: UIViewController {
@IBOutlet var myView: UIView
}
This will exposemyView
to be hooked up in Interface Builder. At this point,what’s the type ofmyView
?UIView
,right?
Wrong. In Swift,marking a variable with@IBOutlet
implicitly turns that variable into an implicitly unwrapped optional,even if its declared type doesn’t include the!
annotation. What’s worse,letting Xcode create this variable declaration – perhaps by control-dragging from a .xib or storyboard – will write it as shown above,without the extra!
after the type. To be extra-correct,we should instead write:
class MyViewController: UIViewController {
@IBOutlet var myView: UIView!
}
Note that this quirk is also virtually required: since IBOutlets don’t strictly need to be hooked up in a .xib or storyboard,it’s always possible that an outlet will wind up with anil
value at runtime. Using implicitly unwrapped optionals allows for this case without mandating significant changes to existing codebases or requiring use of optional binding and chaining everywhere IB comes into play.
Necessary evils
Most of these problems wind up being required by the language’s core tenets,or by Objective-C compatibility,as described above. However,developers still need to balance the good with the bad when adopting Swift – and to keep in mind that the language is still in flux. Future changes may yet obviate several of these quirks;file radarsto encourage Apple to fix your favorite bug!
原文链接:http://lithium3141.com/blog/2014/06/19/learning-swift-optional-types/