Containers
Swift provides you with three container formats to build your custom types on top of: structs, enums, and classes.
Each type has different implicit guarantees and different runtime characteristics. Let’s cover each container now so we can get experience using them for real applications later.
struct
Structs in Swift are lightweight containers capable of holding multiple properties, each with potentially different types.
Here’s a struct
describing basic inventory details of supermarket items:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
With only the definition of the struct above, Swift creates a full constructor for every property. We can create SupermarketInventoryItem
structs all day long:
1 2 3 4 |
|
(The <# … #> parts are just the type of the argument; you replace those with your actual values when you create the struct.)
Let’s analyze SupermarketInventoryItem
in a bit more detail.
let
vs. var
Swift gives us two ways to define properties. A property is either variable or fixed. Variable properties (changeable more than once) are defined by var
. Fixed properties can only be set once at container initialization time. Any attempt to modify a fixed let
property later will be detected by the compiler and result in a compile time error.
Type Declarations
In Swift, types on properties are specified by appending the type to the property name after a colon. This is called annotating the variable with type information.
Note the two properties named showOnline
and leadWeeksForReordering
. Those properties don’t define a type because we gave them default values. Every time a SupermarketInventoryItem
is created, those two properties assume their default values.
Swift has very effective type inference. If you provide default values for all your properties, you do not need to type redundant type information, but you are always welcome to over-specify type information if it makes reading and understanding your data structures easier.
Also note how showOnline
is declared var
so we can update the showOnline
status after the struct is created. On the other hand, leadWeeksForReordering
defaults to 4
, but you can initialize to a new value when you create your struct. The only restriction is after initialization, any let
properties cannot be changed again. It’s perfectly fine to override the default value of 4
with something else on a per-item basis.
Create an Item
We can use the Swift-provided initializer to create a new item:
1 2 3 4 |
|
Update an Item
We can update our var
properties now too:
1 2 3 |
|
If we try to update a let
property, we get:
1 2 3 4 5 6 7 |
|
Using in Functions
As we saw in the Mars Climate Orbiter example, structs can be passed to functions and returned from functions.
Let’s define a function that takes an item and does some checking:
1 2 3 4 5 6 7 |
|
The function accepts one argument named item
with type SupermarketInventoryItem
and returns one Bool
value.
Now we can use our new function to check if we should put this item on our website:
1 |
|
Functions in Structs
But, that function isn’t part of a great data model. The property of publishToWebsite
relies entirely on data living inside the struct itself.
Swift allows us to attach functions directly to structs.
Let’s add a function named shouldPublishToWebsite()
to our struct:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Note how in the function we refer to properties of the struct directly. We don’t need to refer to them with self.showOnline
or self.inStock
because Swift knows our properties belong to the struct where the method is defined. You can still use self
if you want to though—both approaches work equally well.
Now, entirely contained within the struct, we can just ask:
1 |
|
Can we update our struct using in-struct functions too?
Let’s add a function to immediately remove an item due to a contaminated food recall:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Note how the new function disableItem()
is annotated with the word mutating
. Structs are not allowed to modify their own properties unless the method inside the struct is specified as mutating
.
Now with our fancy mutating
disable feature, we can:
1 2 3 |
|
Revisiting let
vs. var
We saw how individual properties can be set to fixed or variable values by using let
and var
to declare properties, but what about the entire struct itself?
We declared someTea
as var
, but what if we assign someTea
to a let
?
1 2 3 |
|
We immediately get an error:
1 2 3 4 5 |
|
If you declare an entire struct as let
, the entire struct acts as if each individual property is declared with let
regardless of any var
usage.
Also note: when we performed someOtherTea = someTea
, what happened is every value inside someTea
got copied to a new SupermarketInventoryItem
held by someOtherTea
.
Takeaway: All structs are value types. If you assign a struct to another name, all values of the struct get copied to the new location. Note: this includes passing structs as function arguments. Structs are always copied when they are passed around.
There is no way to have two variables refer to the same underlying struct. Since every struct is a completely isolated container of properties, you can be sure any changes you make to your current struct are isolated and will not change any other parts of your program simultaneously.
enum
Enough structs. How about some enums?
enum
is short for enumeration
. Many languages have enums, but Swift enums are especially useful for helping you write more clear, more concise, and less error-prone code.
If you are familiar with features called atoms or symbols in other languages, you can think of Swift enums as a named collection of atoms (or symbols).
You can also think of enums as one big named type holding smaller named types inside.
Create an Enum
Let’s define an enum describing the evacuation level around a nuclear reactor:
1 2 3 4 5 6 7 8 9 |
|
Swift allows you to be succinct when possible, so you could also define Evacuation
as:
1 2 3 4 |
|
Unlike enums in C, enums in Swift have no value by default. The value of Evacuation.NoDanger
isn’t 0—it has no user-visible value and can only be used for comparisons at this point.
The power of enums come from Swift’s ability to compare enum elements exactly. No two differently-named enum values can ever equal each other. Even if you define two enums with the same inner member names, the inner members of different enums will never be equal because they belong to different outer enum types.
Swift’s ability to enforce strict enum uniqueness removes an entire class of problems from other languages where enums are just shorthand for integers behind the scenes, so passing an integer is equivalent to passing the enum value (or even passing another enum with the same underlying integer value) in those languages.
Let’s play with Evacuation
:
1 2 |
|
To access an individual value of an enum, you operate on the enum type directly. Enums can take on only one value at a time.
We can easily check for equality:
1 2 |
|
Enum Default Raw Values
But, what if we want our enums to take on values? We can start off by giving our enum raw values in the definition:
1 2 3 4 5 6 7 8 9 |
|
Looks good, right? WRONG.
Swift gives us this error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
If we give our enum explicit raw values, Swift requires we declare the type for our enum. This also means every default raw value in our enum must have the same type.
1 2 3 4 5 6 7 8 9 |
|
For raw value assignment, Swift only supports built-in types.
Creating Enum Value at Runtime
Great, so we can create an enum type with guaranteed-to-be-unique members, but how do we use them? Obviously our code can’t pre-decide to use Evacuation.NoDanger
statically. We have to update our conditions during runtime.
Since we’ve defined raw values for our enum, Swift automatically attaches a method named fromRaw()
to our typed enum. Yes, enums can have functions just like structs can have functions.
Let’s retrieve the proper enum value using only a user-submitted condition string:
1 2 3 |
|
We pass the string “up”
to fromRaw()
on the Evacuation
enum and we get a value back. But, what value do we get back? We passed in a known-good value to fromRaw()
, so we should get Evacuation.DangerRising
back. But, we don’t.
What we get back is something that, when asked, will say it is an Evacuation.DangerRising
, but it’s really something more reliable.
Let’s take a step back and consider what would happen if we passed a string not matching any raw value of Evacuation
. What if we passed in string “CASE NIGHTMARE GREEN”
? What would Swift return to us then since there’s no matching raw value in the enum?
Swift solves this conundrum by using optionals. Optionals allow Swift to be type-safe against common cases of null pointer dereferences.
If we did ask for fromRaw()
using “CASE NIGHTMARE GREEN”
, since Swift can’t know ahead of time if you will pass impossible values to fromRaw()
, the return value of Evacuation.fromRaw()
is not Evacuation
, but rather, Evacuation?
. The question mark signifies the return type is an optional.
What does optional mean? Optional means the value is either the value declared, in this case an Evacuation
, or nothing at all. To check what you got back, you can go through a conditional:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The above code snippet prints “worked!”
then “other didn’t worked!”
.
You can explicitly see the difference between Evacuation
and Evacuation?
if you declare types instead of letting Swift infer types for you:
1 |
|
That one tiny line generates this big error:
1 2 3 4 5 6 |
|
The error is telling you about two choices available to you. You can either declare newLevel
as type Evacuation?
— or — you can use !
to unwrap the optional directly. If you unwrap an optional actually containing nil
, you will trigger a runtime error and your program will crash.
You can remove the error with either of these approaches:
1 2 |
|
Reading Enum Values
In your Playground, you can just enter the name of your variable and see the contents. In your program, you can’t inspect an enum that way because, well, you don’t have a Playground.
Let’s say we’re reading user input and we want to return a full description of the enum we matched:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Note how when Swift knows the context for your enum, you are not required to state the entire enum name when comparing values. You can use .[name]
syntax to address the exact property of your enum.
Shorthand enum property syntax even extends to boolean comparisons. Since Swift is strongly typed, Swift knows you are comparing an Evacuation
, and it allows you to use shorthand directly in a boolean expression:
1 |
|
Back to testing our new description function; we can now call:
1 |
|
Much like the struct example, we can incorporate the description function directly into our enum definition too:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Note how we removed the parameter to the function and now switch on self
instead of any parameter value.
Now we can run desc()
on our enum directly:
1 |
|
Also, like structs, enums are value types. If you assign an enum to another variable, the current enum value is copied and now you have two enums. Note: also like structs, this includes passing enums as function arguments. Enums are always copied when they are passed around.
There is no way to have two variables refer to the same underlying enum, but this is where classes come into play.
class
Class. Everybody’s got a bit of it, right?
In Swift, classes aren’t as special as they are in other languages. Classes serve three purposes in Swift:
- A Swift class is the only container type not copied when assigned to a new variable or passed to a method in a parameter.
- A Swift class can inherit methods and properties from other classes using familiar object oriented semantics.
- A Swift class can have a specific destruction method so your class can clean up resources or notify other pieces of code before it gets deleted.
Now, this is a book about Swift types. We’re not going to do a deep dive into an entire undergraduate course in object oriented theory and dynamic polymorphism here, but we will cover Swift-specific use cases for Swift’s class type.
Swift Class Basics
Naming
Swift containers (struct, enum, class) all share a bit of common functionality.
You can add custom methods to each container. You can write custom constructors for each container. Structs and enums can adopt protocols just like classes can.
Because everything is operationally similar, Swift doesn’t call an instantiated class an “object,” but rather “instance.”
init
The value for killsMainCharacters
is the default from JossWhedonMedia
, but the value doesn’t represent the truth for Angel
. We need a custom initializer.
Let’s add a custom init function to Angel
:
1 2 3 4 5 6 7 8 9 10 |
|
Now when we create our Angel
instance, everything is correct:
1 2 3 4 |
|
deinit
The opposite of a custom initializer is a custom de-initializer. Swift sanely names the de-initializer deinit
to act as a foil to the initializer name of init
.
If your class has a deinit
block, the code in your deinit
block will be called immediately before your object is scheduled for deletion. You can use a deinit
block to make sure any resources held by your instance are released or just to report to other instances this instance will no longer be valid.
Let’s add a deinit block to AvengersMovieI
:
1 2 3 4 5 6 7 8 |
|
Now, when an instance of AvengersMovieI
is deallocated, the instance will print to the console.
We can easily test it works with:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Note how we defined avengers
as an optional. Only optional types can be set to nil
in Swift. Using an optional also requires us to unwrap the optional using !
if we are not checking the optional for nil-ness in an if
block.
Also note deinit
does not have parens. You use deinit
as a bare word followed by a block.
Class Summary
In most object oriented languages, the class is the basic unit of encapsulation. In Swift, classes are just one of three container types available for you. You can pick the most expressive container type for your use case. Many times, for encapsulating simple values or creating custom types, classes are overkill. You can use a simple struct instead and get free a initializer handed to you too.
Unlike other programming languages, Swift classes have no concept of property visibility. Every property you create in a class is accessible by every part of your program.
In Swift, classes are reference counted so one instance may be shared among multiple variables. Structs and enums can never be shared and are always copied when used somewhere else.
In Swift, classes are the only type where inheritance is allowed. All Swift containers may implement Protocols, but only classes can inherit properties and methods from parents.
In Swift, classes are the only type allowed to have a deinit
method.
If you don’t need to inherit from a parent, share your object among multiple variables, or cleanup when your instance is destroyed, a struct may be a better choice than a class for your custom types.
Exercises
Structs
The properties of SupermarketInventoryItem
only use Swift-provided types. As we saw with the Mars Climate Orbiter, we shouldn’t use built-in types to mean different things.
Setting priceRetail
to the number 5 has no meaning directly. What does price = 5
mean?
Exercise: define custom types for every property in SupermarketInventoryItem
. To get started, you can define a Price
type instead of using Int
for prices. Then you can use an actual date type instead of strings for dates (NSDate
perhaps). Then you can create a custom aisle data type and shelf position data type for maximum expressiveness.
Try to only use built-in Swift types (numbers, strings) in base types you define, then define your own data structures and program interfaces on top of your own base types.
Enums
In the enum examples, we covered assigning default raw values to enums. Raw values on enums can be any Swift built in number, string, or character type. Default raw values for enum properties can only be set at compile time and can’t be changed after your program is compiled.
But, what if we want our enum properties to have user-defined sub-properties themselves?
In addition to compile-time default raw values for enums, Swift supports binding associated values to enum properties at runtime.
Here’s a tiny example. First, we’ll define the types we want our associated values to use, then we define a Clothing
enum in terms of our own types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Note how the Clothing
enum assigns an arbitrary number of types to each enum property. Values on associated types may not have default values since they are designed to be set at runtime (and enums can’t create arbitrary instances as default values). When you use associated values, you do not annotate the entire enum with a type declaration like we had to do when using default raw values.
Also note how we mix types of enums and structs (and classes if we used them here) interchangeably. When you define a container type, you may use the type interchangeably. Swift doesn’t care if you are using a class type or a struct type or an enum type. As long as all your types match and you only perform operations allowed under your type, you’re good to go.
We can create a couple Clothing
instances and give each property specific values:
1 2 3 |
|
How do we get the values out of our properties? You’ll notice the sub-values of each enum property have no names. To access the sub-values, we must use Swift’s positional pattern matching.
Let’s break the values out using a switch
statement for pattern matching on associated values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Swift switch
statements require every case in the enum be considered. To short circuit that limitation when we don’t care about all the inner enum types, we can use a default:
case.
Notice how the associated values of each enum type are referenced positionally in each case
. Swift automatically infers the type of your matching parameters from the enum definition itself so you’re not constantly required to repeat the same type declarations as boilerplate everywhere.
Once you’re inside a matching case
, you can perform actions based on the associated values of your enum then move on to bigger and better things.
Exercise: Define a new enum having properties of the most common locations you visit throughout the week. Home
, Work
, I-280
, I-Wanna-Go-Home
, etc. For each enum property, define useful associated values (for bonus points define custom types for all your associated values), then create some enums and extract their associated values using switch
statements.
Classes
Classes are typically hailed as super awesome amazeballs, but classes are just another way to define a type. In most object oriented languages, they teach you a “class” represents an “object,” but that’s a lie. A class represents a type and when you instantiate a class, you get an instance with the type of the class you just created. The notion of “object” existing from a static “class” is a bit backwards. When developing, think in terms of classes as types, not classes as oh-so-special-snowflake objects.
Swift’s strict compile-time type checking—with built-in defenses against null pointer dereferencing—gives you more expressability than classes in lesser languages where null pointers flow freely and clearly preventable crashes (software or physical) happen in unconscionable numbers every day.
You are probably more familiar with the concept of classes than concepts of Swift’s struct and enum containers, so just try a basic exercise to make sure you understand how Swift classes work.
Exercise: Create a class hierarchy describing modern day touchscreen devices. Instantiate a few classes representing devices around you. Make sure you understand how init
and super
work together and how default values for properties differ based on being assigned in initializers versus default raw values versus using optionals.
Here’s a quick template to get you started:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
|
Container Differences
We’ve covered the three container types mostly in isolation up to this point. Let’s compare the best uses for each type to get a better feel for when to use what 2.
struct
vs. enum
Structs and enums are easy to contrast because they have completely opposite use cases.
As we’ve seen, structs hold multiple properties where properties can have multiple types across the entire struct, and all properties of a struct are accessible at the same time.
Enums also have multiple properties, but each property has an implicit user-hidden value. Each enum property is guaranteed to be globally unique. Only one property of an enum may be active at once for a given instance of the enum you’ve created.
If you assign default raw values to every property of your enum, every default value must be of the same type, and you must annotate the type of your enum with the common type of your default values.
Quick enum default raw value review:
1 2 3 4 5 6 7 8 9 10 |
|
Structs and enums are both value types meaning every time you assign an existing struct or enum to another name, the values get copied.
Copy example:
1 2 3 4 5 6 7 8 9 10 11 |
|
You’ll want to use enums when representing states that can only take one of multiple values at a time — or — when you want to store a type-safe way to refer to common fixed-value data points.
For example, you could store constants in an enum container based on their units:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Enums are a great way to organize constants. They give you a stable namespace for common data types as well as compile-time checking for accuracy when passing the enum properties as arguments to functions.
struct
vs. class
Structs and classes are two very similar things in Swift.
All you need to remember about the differences between a struct and a class is:
-
Structs are best used to store groups of closely related properties
- struct properties don’t allow an inheritance hierarchy or sharing of data.
- Structs automatically generate a constructor so you can initialize all properties in your struct without writing boilerplate initialization setters.
-
Classes are best used if your data structures are naturally hierarchical in nature
- or if you need to share one instance of a class among various parts of code.
- or if you need to inherit methods or properties from parent classes
- Structs are copied every time they are passed to methods or assigned to new variable names. If you use large structs, the extra copying could impact your performance.
- Class instances are passed by reference. When an instance is passed to a method, only a tiny reference is given to the method.
- Classes are the only way to implement data structures requiring one instance remain accessible by multiple other instances (examples: circular doubly linked lists, various tree/graph structures, etc).
-
Classes are the only container type to have an optional
deinit
block.
class
vs. enum
Classes and enums are the most different of Swift’s containers.
Classes can have multiple properties, inheritance, and instances of classes are passed by reference.
Eunms have multiple possible properties, but only one property is ever active at a given time for a given instantiation of the enum. Enums are passed by value, so every time you use an enum as an argument, the value is copied (which is perfectly okay since your enum only holds one tiny value active at once).
Classes can approximate enum behavior by using static fixed properties with specialized accessors, but enums take care of those common situations for you.
Common Among Everything
All Swift container types have some common capabilities.
Each container type can have an init()
method to bring up your new instance. Each container type can have convenience init()
methods for helping to provide default values when creating new instances.
Each container type can adopt (conform) to protocols specifying required methods and properties on all instances of a conforming type.
Each container type can hold methods inside of it to operate on the value of the instance directly.
Each container, by definition, creates a new global Swift type with the name you give your container. Swift does not discriminate between the underlying container of a type—Swift only cares about matching types.
Every Swift container may have methods and dynamic properties extended at runtime with Swift’s extension
capability.
Try to reason about your program using custom-defined types, not built-in types or only classes you define. Classes are types first and a way to instantiate instances of themselves second.
-
We could get around this restriction by declaring every type as an optional, but we don’t want to confuse anyone with unwrapping, automatic unwrapping, or adding a bunch of
if val { val! }
clauses. Plus, there’s another entire type layer of implicitly unwrapped optionals we’re not going to cover here, though we’ve seen implicitly unwrapped optionals in action when we used an optional as anif
boolean.↩ -
We comparing three things (struct, enum, class) in pairs regardless of order (comparing
struct
vs.enum
is the same as comparingenum
vs.struct
here), so the number of comparisons we need is ${3 \choose 2} = 3$ comparisons total. If that equation isn’t rendered as math, then your e-reader doesn’t support the current specification requiring support for inline math by default in eBook containers.↩