Tải bản đầy đủ
Chapter 3. Variables and Simple Types

Chapter 3. Variables and Simple Types

Tải bản đầy đủ

in the same module, because Swift files in the same module can automatically see
one another, and hence can see one another’s top levels. For example:
// File1:
let globalVariable = "global"
class Dog {
func printGlobal() {
print(globalVariable) // *
}
}
// File2:
class Cat {
func printGlobal() {
print(globalVariable) // *
}
}

Properties
A property is a variable declared at the top level of an object type declaration (an
enum, struct, or class). There are two kinds of properties: instance properties and
static/class properties.
Instance properties
By default, a property is an instance property. Its value can differ for each
instance of this object type. Its lifetime is the same as the lifetime of the
instance. Recall from Chapter 1 that an instance comes into existence
through deliberate instantiation of an object type; the subsequent lifetime of
the instance, and hence of its instance properties, depends on the lifetime of
the variable to which the instance itself is assigned.
Static/class properties
A property is a static/class property if its declaration is preceded by the key‐
word static or class. (I’ll go into detail about those terms in Chapter 4.) Its
lifetime is the same as the lifetime of the object type. If the object type is
declared at the top level of a file, the property lives as long as the program
runs.
A property is visible to code inside the object declaration. For example, an object’s
methods can see that object’s properties. Such code can refer to the property
using dot-notation with self, and I always do this as a matter of style, but self
can usually be omitted except for purposes of disambiguation. An instance prop‐
erty is also visible (by default) to other code, provided the other code has a refer‐
ence to this instance; in that case, the property can be referred to through dotnotation with the instance reference. A static/class property is visible (by default)
to other code that can see the name of this object type; in that case, it can be
referred to through dot-notation with the object type. For example:

70

|

Chapter 3: Variables and Simple Types

// File1:
class Dog {
static let staticProperty = "staticProperty"
let instanceProperty = "instanceProperty"
func printInstanceProperty() {
print(self.instanceProperty) // *
}
}
// File2:
class Cat {
func printDogStaticProperty() {
print(Dog.staticProperty) // *
}
func printDogInstanceProperty() {
let d = Dog()
print(d.instanceProperty) // *
}
}

Local variables
A local variable is a variable declared inside a function body. A local variable lives
only as long as its surrounding curly-braces scope lives: it comes into existence
when the path of execution passes into the scope and reaches the variable decla‐
ration, and it goes out of existence when the path of execution exits the scope.
Local variables are sometimes called automatic, to signify that they come into and
go out of existence automatically. A local variable can be seen only by subsequent
code within the same scope (including a subsequent deeper scope within the
same scope). For example:
class Dog {
func printLocalVariable() {
let localVariable = "local"
print(localVariable) // *
}
}

Variable Declaration
A variable is declared with let or var:
• With let, the variable becomes a constant — its value can never be changed after
the first assignment of a value (initialization).
• With var, the variable is a true variable, and its value can be changed by subse‐
quent assignment.
A variable’s type, however, can never be changed. A variable declared with var can be
given a different value, but that value must conform to the variable’s type. Thus, when
Variable Declaration

|

71

a variable is declared, it must be given a type, which it will have forever after. You can
give a variable a type explicitly or implicitly:
Explicit variable type declaration
After the variable’s name in the declaration, add a colon and the name of the
type:
var x : Int

Implicit variable type by initialization
If you initialize the variable as part of the declaration, and if you provide no
explicit type, Swift will infer its type, based on the value with which it is initial‐
ized:
var x = 1 // and now x is an Int

It is perfectly possible to declare a variable’s type explicitly and assign it an initial
value, all in one move:
var x : Int = 1

In that example, the explicit type declaration is superfluous, because the type (Int)
would have been inferred from the initial value. Sometimes, however, providing an
explicit type, even while also assigning an initial value, is not superfluous. Here are
the main situations where that’s the case:
Swift’s inference would be wrong
A very common case in my own code is when I want to provide the initial value
as a numeric literal. Swift will infer either Int or Double, depending on whether
the literal contains a decimal point. But there are a lot of other numeric types!
When I mean one of those, I will provide the type explicitly, like this:
let separator : CGFloat = 2.0

Swift can’t infer the type
Sometimes, the type of the initial value is completely unknown to the compiler
unless you tell it. A very common case involves option sets (discussed in Chap‐
ter 4). This won’t compile:
var opts = [.autoreverse, .repeat] // compile error

The problem is that the name .autoreverse is a shortcut for UIViewAnimationOptions.autoreverse (and so too for .repeat), but Swift doesn’t know that
unless we tell it. Explicitly typing the variable is an elegant way of doing that:
let opts : UIViewAnimationOptions = [.autoreverse, .repeat]

The programmer can’t infer the type
I frequently include a superfluous explicit type declaration as a kind of note to
myself. Here’s an example from my own code:
72

|

Chapter 3: Variables and Simple Types

let duration : CMTime = track.timeRange.duration

In that code, track is an AVAssetTrack. Swift knows perfectly well that the
duration property of an AVAssetTrack’s timeRange property is a CMTime. But I
don’t! In order to remind myself of that fact, I’ve shown the type explicitly.
Because explicit variable typing is possible, a variable doesn’t have to be initialized
when it is declared. It is legal to write this:
let x : Int

Now x is an empty shoebox — an Int variable without an initial value. I strongly urge
you, however, not to do that with a local variable if you can possibly avoid it. It isn’t a
disaster — the Swift compiler will stop you from trying to use a variable that has
never been assigned a value — but it’s not a good habit.
The exception that proves the rule is what we might call conditional initialization.
Sometimes, we don’t know a variable’s initial value until we’ve performed some sort of
conditional test. The variable itself, however, can be declared only once; so it must be
declared in advance and conditionally initialized afterward. This sort of thing is not
unreasonable (though there are other, possibly better ways to write it, to which I’ll
come in due course):
let timed
if val ==
timed
} else {
timed
}

: Bool
1 {
= true
= false

When a variable’s address is to be passed as argument to a function, the variable must
be declared and initialized beforehand, even if the initial value is fake. Recall this
example from Chapter 2:
var r : CGFloat = 0
var g : CGFloat = 0
var b : CGFloat = 0
var a : CGFloat = 0
c.getRed(&r, green: &g, blue: &b, alpha: &a)

After that code runs, our four CGFloat 0 values will have been replaced; they were
just momentary placeholders, to satisfy the compiler.
On rare occasions, you’ll want to call a Cocoa method that returns a value immedi‐
ately and later uses that value in a function passed to that same method. For example,
Cocoa has a UIApplication instance method declared like this:
func beginBackgroundTask(
expirationHandler handler: (() -> Void)? = nil)
-> UIBackgroundTaskIdentifier

Variable Declaration

|

73

beginBackgroundTask(expirationHandler:) returns a number (a UIBackground‐
TaskIdentifier is just an Int), and will later call the expirationHandler: function

passed to it — a function in which you will want to use the number that was returned
at the outset. Swift’s safety rules won’t let you declare the variable that holds this num‐
ber and use it in an anonymous function all in the same statement:
let bti = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(bti)
} // error: variable used within its own initial value

Therefore, you need to declare the variable beforehand; but then Swift has another
complaint:
var bti : UIBackgroundTaskIdentifier
bti = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(bti)
} // error: variable captured by a closure before being initialized

The solution is to declare the variable beforehand and give it a fake initial value as a
placeholder:
var bti : UIBackgroundTaskIdentifier = 0
bti = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(bti)
}

Instance properties of an object (at the top level of an enum, struct, or class dec‐
laration) can be initialized in the object’s initializer function rather than by
assignment in their declaration. It is legal and common for both constant (let)
and variable (var) instance properties to have an explicit type and no directly
assigned initial value. I’ll have more to say about that in Chapter 4.

Computed Initializer
Sometimes, you’d like to run several lines of code in order to compute a variable’s ini‐
tial value. A simple and compact way to express this is with an anonymous function
that you call immediately (see “Define-and-Call” on page 52). I’ll illustrate by rewrit‐
ing an earlier example:
let timed : Bool = {
if val == 1 {
return true
} else {
return false
}
}()

You can do the same thing when you’re initializing an instance property. In this class,
there’s an image (a UIImage) that I’m going to need many times later on. It makes
sense to create this image in advance as a constant instance property of the class. To
74

|

Chapter 3: Variables and Simple Types

create it means to draw it. That takes several lines of code. So I declare and initialize
the property by defining and calling an anonymous function, like this (for my imageOfSize utility, see Chapter 2):
class RootViewController : UITableViewController {
let cellBackgroundImage : UIImage = {
return imageOfSize(CGSize(width:320, height:44)) {
// ... drawing goes here ...
}
}()
// ... rest of class goes here ...
}

Indeed, a define-and-call anonymous function is often the only legal way to compute
an instance property’s initial value with multiple lines of code. The reason is that,
when you’re initializing an instance property, you can’t call an instance method,
because there is no instance yet — the instance, after all, is what you are in the pro‐
cess of creating.

Computed Variables
The variables I’ve been describing so far in this chapter have all been stored variables.
The shoebox analogy applies. The variable is a name, like a shoebox; a value can be
put into the shoebox by assigning it to the variable, and it then sits there and can be
retrieved later by referring to the variable, for as long the variable lives.
Alternatively, a variable can be computed. This means that the variable, instead of
having a value, has functions. One function, the setter, is called when the variable is
assigned to. The other function, the getter, is called when the variable is referred to.
Here’s some code illustrating schematically the syntax for declaring a computed vari‐
able:
var now : String {
get {
return Date().description
}
set {
print(newValue)
}
}

The variable must be declared with var (not let). Its type must be declared
explicitly. The type is followed immediately by curly braces.
The getter function is called get. There is no formal function declaration; the
word get is simply followed immediately by a function body in curly braces.
The getter function must return a value of the same type as the variable.
Computed Variables

|

75

The setter function is called set. There is no formal function declaration; the
word set is simply followed immediately by a function body in curly braces.
The setter behaves like a function taking one parameter. By default, this parame‐
ter arrives into the setter function body with the local name newValue.
Here’s some code that illustrates the use of our computed variable. You don’t treat it
any differently than any other variable! To assign to the variable, assign to it; to use
the variable, use it. Behind the scenes, though, the setter and getter functions are
called:
now = "Howdy" // Howdy
print(now) // 2016-06-26 17:03:30 +0000

Assigning to now calls its setter. The argument passed into this call is the assigned
value; here, that’s "Howdy". That value arrives in the set function as newValue.
Our set function prints newValue to the console.
Fetching now calls its getter. Our get function obtains the current date-time and
translates it into a string, and returns the string. Our code then prints that string
to the console.
Observe that when we set now to "Howdy" in the first line, the string "Howdy" wasn’t
stored anywhere. It had no effect, for example, on the value of now in the second line.
A set function can store a value, but it can’t store it in this computed variable; a com‐
puted variable isn’t storage! It’s a shorthand for calling its getter and setter functions.
There are a couple of variants on the basic syntax I’ve just illustrated:
• The name of the set function parameter doesn’t have to be newValue. To specify
a different name, put it in parentheses after the word set, like this:
set (val) { // now you can use "val" inside the setter function body

• There doesn’t have to be a setter. If the setter is omitted, this becomes a read-only
variable. Attempting to assign to it is a compile error. A computed variable with
no setter is the primary way to create a read-only variable in Swift.
• There must always be a getter! However, if there is no setter, the word get and
the curly braces that follow it can be omitted. Thus, this is a legal declaration of a
read-only variable:
var now : String {
return Date().description
}

A computed variable can be useful in many ways. Here are the ones that occur most
frequently in my real programming life:
76

|

Chapter 3: Variables and Simple Types

Read-only variable
A computed variable is the simplest way to make a read-only variable. Just omit
the setter from the declaration. Typically, the variable will be a global variable or
a property; there probably wouldn’t be much point in a local read-only variable.
Façade for a function
When a value can be readily calculated by a function each time it is needed, it
often makes for simpler syntax to express it as a read-only calculated variable.
Here’s an example from my own code:
var mp : MPMusicPlayerController {
return MPMusicPlayerController.systemMusicPlayer()
}

It’s no bother to call MPMusicPlayerController.systemMusicPlayer() every
time I want to refer to this object, but it’s more compact to refer to it by a simple
name, mp. And since mp represents a thing, rather than the performance of an
action, it’s nicer for mp to appear as a variable, so that to all appearances it is the
thing, rather than as a function, which returns the thing.
Façade for other variables
A computed variable can sit in front of one or more stored variables, acting as a
gatekeeper on how those stored variables are set and fetched. This is comparable
to an accessor method in Objective-C. In the extreme case, a public computed
variable is backed by a private stored variable:
private var _p : String = ""
var p : String {
get {
return self._p
}
set {
self._p = newValue
}
}

That’s a silly example, because we’re not doing anything interesting with our
accessors: we are just setting and getting the private stored variable directly, so
there’s no effective difference between p and _p. But based on that template, you
could now add functionality so that something useful happens during setting and
getting.
As the preceding example demonstrates, a computed instance property function
can refer to other instance properties; it can also call instance methods. This is
important, because in general the initializer for a stored property can do neither
of those things. The reason this is legal for a computed property is that its func‐
tions won’t be called until the instance actually exists.

Computed Variables

|

77

Here’s a practical example of a computed variable used as a façade for storage. My
class has an instance property myBigData, holding a very large stored piece of data,
which can alternatively be nil (it’s an Optional, as I’ll explain later). When my app
goes into the background, I want to reduce memory usage (because iOS kills back‐
grounded apps that use too much memory). So I plan to save the data of myBigData
as a file to disk, and then set the variable itself to nil, thus releasing its data from
memory. Now consider what should happen when my app comes back to the front
and my code tries to fetch myBigData. If it isn’t nil, we just fetch its value. But if it is
nil, this might be because we saved its value to disk. So now I want to restore its
value by reading it from disk, and then fetch its value. This is a perfect use of a com‐
puted variable façade:
private var _myBigData : Data! = nil
var myBigData : Data! {
set (newdata) {
self._myBigData = newdata
}
get {
if _myBigData == nil {
// ... get a reference to file on disk, f ...
if let d = try? Data(contentsOf:f) {
self._myBigData = d
// ... erase the file ...
}
}
return self._myBigData
}
}

Setter Observers
Computed variables are not needed as a stored variable façade as often as you might
suppose. That’s because Swift has another feature, which lets you inject functionality
into the setter of a stored variable — setter observers. These are functions that are
called just before and just after other code sets a stored variable.
The syntax for declaring a variable with a setter observer is very similar to the syntax
for declaring a computed variable; you can write a willSet function, a didSet func‐
tion, or both:
var s = "whatever" {
willSet {
print(newValue)
}
didSet {

78

|

Chapter 3: Variables and Simple Types

print(oldValue)
// self.s = "something else"
}
}

The variable must be declared with var (not let). It can be assigned an initial
value. It is then followed immediately by curly braces.
The willSet function, if there is one, is the word willSet followed immediately
by a function body in curly braces. It is called when other code sets this variable,
just before the variable actually receives its new value.
By default, the willSet function receives the incoming new value as newValue.
You can change this name by writing a different name in parentheses after the
word willSet. The old value is still sitting in the stored variable, and the willSet
function can access it there.
The didSet function, if there is one, is the word didSet followed immediately by
a function body in curly braces. It is called when other code sets this variable, just
after the variable actually receives its new value.
By default, the didSet function receives the old value, which has already been
replaced as the value of the variable, as oldValue. You can change this name by
writing a different name in parentheses after the word didSet. The new value is
already sitting in the stored variable, and the didSet function can access it there.
Moreover, it is legal for the didSet function to set the stored variable to a different
value.
Setter observer functions are not called when the stored variable is initialized or
when the didSet function changes the stored variable’s value. That would be cir‐
cular!

In practice, I find myself using setter observers, rather than a computed variable, in
the vast majority of situations where I would have used a setter override in ObjectiveC. Here’s an example. This is an instance property of a view class. Every time this
property changes, we need to change the interface to reflect it. Not only do we change
the interface, but also we “clamp” the incoming value within a fixed limit:
var angle : CGFloat = 0 {
didSet {
// angle must not be smaller than 0 or larger than 5
if self.angle < 0 {
self.angle = 0
}
if self.angle > 5 {
self.angle = 5
}

Setter Observers

|

79

// modify interface to match
self.transform = CGAffineTransform(rotationAngle: self.angle)
}
}

A computed variable can’t have setter observers. But it doesn’t need them! There’s
a setter function, so anything additional that needs to happen during setting can
be programmed directly into that setter function.

Lazy Initialization
The term lazy is not a pejorative ethical judgment; it’s a formal description of an
important behavior. If a stored variable is assigned an initial value as part of its decla‐
ration, and if it uses lazy initialization, then the initial value is not actually evaluated
and assigned until running code accesses the variable’s value.
There are three types of variable that can be initialized lazily in Swift:
Global variables
Global variables are automatically lazy. This makes sense if you ask yourself when
they should be initialized. As the app launches, files and their top-level code are
encountered. It would make no sense to initialize globals now, because the app
isn’t even running yet. Thus global initialization must be postponed to some
moment that does make sense. Therefore, a global variable’s initialization doesn’t
happen until other code first refers to that global. Under the hood, this behavior
is protected with the equivalent of dispatch_once; this makes initialization both
singular (it can happen only once) and thread-safe.
Static properties
Static properties are automatically lazy. They behave exactly like global variables,
and for basically the same reason. (There are no stored class properties in Swift,
so class properties can’t be initialized and thus can’t have lazy initialization.)
Instance properties
An instance property is not lazy by default, but it may be made lazy by marking
its declaration with the keyword lazy. This property must be declared with var,
not let. The initializer for such a property might never be evaluated, namely if
code assigns the property a value before any code fetches the property’s value.
In Swift 3, dispatch_once itself is unavailable; use a global or static variable to
obtain the same behavior.

Lazy initialization is often used to implement singleton. Singleton is a pattern where
all code is able to get access to a single shared instance of a certain class:

80

|

Chapter 3: Variables and Simple Types