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 touch on Twitter!
We’ve had a little over two weeks to play with the Swift programming language now, 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 of nil
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 (like Int
, Bool
, or String
) to more complex class types (such as UIView
).
When we declare variables of these types, Swift requires that they 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 use let
instead of var
, we’d find ourselves facing an error on the declaration itself, rather than on the first call site:
let x: Int // Error: 'let' declarations require an initializer expression
Naturally, we can silence this error by providing a value for x
:
let x = 42
println(x) // prints "42"
However, astute developers will notice that we can’t provide x
a nil
value at initialization time:
let x: Int = nil
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 a nil
value for variables of a non-optional type. Since x
is a plain Int
, it must have an Int
value, which nil
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 a nil
value into our x
from earlier just by tacking that ?
onto the Int
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 or nil
, and it’s just up to you to check which case your code is handling.
Little Boxes
“But wait,” you say, “Int is a value type, not an object! How can I use nil
for a value? There was no such thing for NSInteger…”
Well, you’re right. NSInteger didn’t have a nil
value (or, rather, using nil
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
, 1
, NSIntegerMin
,NSIntegerMax
, and NSNotFound
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 get NSNotFound
– but if you try to find a nonexistent row in a table view, you get -1
instead.
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:
enum Optional<T> {
case None
case Some(T)
}
The above is the actual definition of Optional
in the Swift core library (slightly abridged for clarity). Swift defines a new type called Optional that always has exactly one of two values: a defined “nothing” value called None
, or a wrapped-up value of some other type T
. 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 plain Int
type. The second and third, though, are both of type Optional<Int>
– or, for short, Int?
. Notice that the third value here is actually an “empty box” (the None
value), even though its type is Int?
.
This ability, to pass around None
anywhere an optional type can go, is how Swift can provide things like nil
for value types like Int
(or, for that matter, any type, whether value or reference). Since this value will have the same type as a “real” value wrapped up in Optional
, 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:
func double(x: Int) -> Int {
return 2 * x
}
let y: Int? = .Some(42)
println(double(y)) // error: Value of optional type 'Int?' not unwrapped
We need some way of getting at the value inside an optional’s box – and, for that matter, checking whether such a value exists at all! Thankfully, Swift has us covered with the !
operator, which extracts the value out of an optional:
let y: Int? = .Some(42)
println(double(y!)) // prints '84'
This works great for optionals that have a value. But what about those that don’t?
let y: Int? = nil // same as Optional.None
println(double(y!)) // runtime error: Can't unwrap Optional.None
The !
operator only applies to optionals that have an actual value inside them. If your optional has nil
(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 is nil
first – much like we might have done in Objective-C.
let y: Int? = nil
if y {
println(double(y!))
} else {
println("No value to double!") // prints "No value to double!"
}
But what if we got y
from another method altogether, instead of defining it locally? It would feel a bit verbose to require a new let
or var
statement wherever we intend to call a func
, then immediately check that just-declared variable.
Swift has us covered here too, with a syntax called optional binding. By combining an if
and a let
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:
if let y: Int? = someObject.someInt() {
println(double(y))
} else {
println("No value to double!") // prints "No value to double!"
}
You may have noticed that here, we don’t even need to explicitly unbox y
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 case Int
), instead of keeping the wrapping Optional
type. Since we’re sure such a value exists, we can access it directly inside the body of the if
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 be nil
. In Objective-C, trying the latter would just give you another nil
-like value right back.
Thankfully, Swift anticipated this case too. Developers can make use of optional chaining to call methods on potentially-nil
objects:
let y: SomeClass? = nil
let z = y?.someMethod() // will produce nil
By sticking a ?
between the variable name and method call, we can indicate that we want either a real answer back (in the event that y
is a valid instance) or another nil
(in the case that y
is itself nil
). 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 will always be optional, even if the method itself declares a non-optional return type. Since the value being computed could become nil
at any point along the chain (if the object being called is nil
), the return value has to be prepared for that possibility, and the only type we have in Swift capable of carrying a nil
value is an optional. Consider:
class SomeClass {
func someMethod() -> Int {
return 42
}
}
let y: SomeClass? = nil
let z = y?.someMethod() // of type Optional<Int> with value nil
Even though someMethod()
is declared to return an Int
, z
gets type Optional<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:
let y: SomeClass? = nil
if let z = y?.someMethod() {
println(double(z))
} else {
println("Couldn't get value from someMethod()")
}
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
y
isnil
orsomeMethod()
returnsnil
, the optional binding will catch that case and avoid giving us anil
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 around nil
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 surrounding if
checks will still look to see if the variable is nil
, 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-case Bool
to disallow optionals would be an exceptional change, requiring serious modifications to the language or the Optional type.
That does, however, lead to a way developers can construct a kind of three-state variable: an Optional<Bool>
can be true
, false
, or nil
. (What the latter means is rather context-dependent.) This can be very misleading, though, when combined with an if
check:
let x: Optional<Bool> = .Some(false)
if x {
println("true")
} else {
println("false")
}
// prints "true"
Since the if
in this case checks whether x
is nil
, 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 unwrap x
, 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 expose myView
to be hooked up in Interface Builder. At this point, what’s the type of myView
? 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 a nil
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 radars to encourage Apple to fix your favorite bug!
原文链接:http://lithium3141.com/blog/2014/06/19/learning-swift-optional-types/