Swift Learning (11) - Classes and Structures (Improved Code Version)
Structures and classes, as general and flexible constructs, have become the foundation for building code. You can use the syntax for defining constants, variables, and functions to define properties and add methods for structures and classes.
Unlike other programming languages, Swift
does not require you to create separate files for the interface and implementation code of custom structures and classes. You only need to define a structure or class in a single file, and the system will automatically generate an external interface for other code.
Note
Typically, an instance of a class is called an object. However, compared to other languages, structures and classes in Swift are much more similar in functionality. Most of what is discussed in this chapter applies to both structures and classes. Therefore, the more general term “instance” will be used here.
Comparison of Structures and Classes
Structures and classes in Swift
have many similarities. Both can:
- Define properties to store values
- Define methods to provide functionality
- Define subscripts to access their values using subscript syntax
- Define initializers to set up their initial state
- Be extended to add functionality beyond their default implementation
- Conform to protocols to provide standard functionality
For more information, see Properties, Methods, Subscripts, Initialization, Extensions, and Protocols.
Compared to structures, classes have the following additional capabilities:
- Inheritance allows one class to inherit the characteristics of another
- Type casting enables you to check and interpret the type of a class instance at runtime
- Deinitializers allow a class instance to release any resources it has allocated
- Reference counting allows multiple references to a class instance
For more information, see Inheritance, Type Casting, Deinitialization, and Automatic Reference Counting.
The additional capabilities supported by classes come at the cost of increased complexity. As a general rule, prefer structures because they are easier to understand, and only use classes when appropriate or necessary. In practice, this means most custom data types will be structures or enumerations.
Note
Classes and actors share many features.
Type Definition Syntax
Structures and classes are defined in similar ways: use the struct
keyword to introduce a structure, and the class
keyword to introduce a class, placing their definitions within a pair of braces:
struct SomeStructure {
// Structure definition goes here
}
class SomeClass {
// Class definition goes here
}
Note
Every time you define a new structure or class, you are defining a new
Swift
type. UseUpperCamelCase
for type names (such as SomeClass and SomeStructure) to match the standard Swift type naming convention (like String, Int, and Bool). UselowerCamelCase
for property and method names (such as frameRate and incrementCount) to distinguish them from type names.
Below are examples of defining a structure and a class:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
In the example above, a structure named Resolution
is defined to describe pixel-based resolution. This structure contains two stored properties, width
and height
. Stored properties are bound to the structure or class and are stored in constants or variables. When these two properties are initialized to the integer 0, their type is inferred as Int
.
The example also defines a class named VideoMode
to describe a specific video mode for a video display. This class contains four mutable stored properties. The first, resolution
, is initialized as a new instance of the Resolution
structure, and its type is inferred as Resolution
. The new VideoMode
instance also initializes the other three properties: interlaced
(initial value false
), frameRate
(initial value 0.0
), and name
(an optional String
). Since name
is an optional type, it is automatically assigned a default value of nil
, meaning “no name value”.
Instances of Structures and Classes
The definitions of the Resolution
structure and the VideoMode
class only describe what Resolution
and VideoMode
are. They do not describe a specific resolution or video mode. To do this, you need to create an instance of the structure or class.
let someResolution = Resolution()
let someVideoMode = VideoMode()
Both structures and classes use initializer syntax to create new instances. The simplest form of initializer syntax is to follow the type name with a pair of empty parentheses, such as Resolution()
or VideoMode()
. Instances of classes or structures created in this way have their properties initialized to default values. The Initialization chapter will discuss initialization of classes and structures in more detail.
Property Access
You can access the properties of an instance using dot syntax. The syntax rule is to follow the instance with the property name, separated by a dot, with no spaces in between.
print("The width of someResolution is \(someResolution.width).") // Prints "The width of someResolution is 0."
---
output: The width of someResolution is 0.
In the example above, someResolution.width
refers to the width
property of someResolution
, returning its initial value of 0.
You can also access sub-properties, such as the width
property of the resolution
property in someVideoMode
:
print("The width of someVideoMode is \(someVideoMode.resolution.width).") // Prints "The width of someVideoMode is 0."
---
output: The width of someVideoMode is 0.
You can also use dot syntax to assign values to mutable properties:
someVideoMode.resolution.width = 1280
print("Now, the width of someVideoMode is \(someVideoMode.resolution.width).") // Prints "Now, the width of someVideoMode is 1280."
---
output: Now, the width of someVideoMode is 1280.
Memberwise Initializer for Structure Types
All structures have an automatically generated memberwise initializer, which you can use to initialize the properties of a new structure instance. The initial values for each property can be passed to the memberwise initializer by name:
let vga = Resolution(width: 640, height: 480)
print("The width of vga is \(vga.width) and the height is \(vga.height).") // Prints "The width of vga is 640 and the height is 480."
---
output: The width of vga is 640 and the height is 480.
Unlike structures, class instances do not have a default memberwise initializer. The Initialization chapter will discuss initializers in more detail.
Structures and Enumerations Are Value Types
A value type is a type whose value is copied when it is assigned to a variable, constant, or passed to a function.
In previous chapters, we have used value types extensively. In fact, all basic types in Swift
—integers, floating-point numbers, booleans, strings, arrays, and dictionaries—are value types, and are implemented as structures under the hood.
All structures and enumerations in Swift
are value types. This means that their instances, as well as any value-type properties they contain, are copied when passed around in code.
Note
Standard library collections such as arrays, dictionaries, and strings are optimized for copying to reduce performance costs. New collections do not immediately copy but instead share the same memory and elements as the original. The elements are only copied when a modification is made to a copy. To the developer, it appears as if the copy happens immediately.
Consider the following example, which uses the Resolution
structure from the previous example:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
In the example above, a constant named hd
is declared and initialized as a Resolution
instance with full HD resolution (1920 pixels wide, 1080 pixels high).
A variable named cinema
is then declared and assigned the value of hd
. Because Resolution
is a structure, a copy of the existing instance is created and assigned to cinema
. Although hd
and cinema
have the same width and height, they are completely different instances behind the scenes.
Next, to meet the requirements of digital cinema projection (2048 pixels wide, 1080 pixels high), the width
property of cinema
is modified to the slightly wider 2K standard:
cinema.width = 2048
Checking the width
property of cinema
, its value has indeed changed to 2048:
print("The width of cinema is \(cinema.width).") // Prints "The width of cinema is 2048."
---
output: The width of cinema is 2048.
However, the original width
property of hd
is still 1920:
print("The width of hd is still \(hd.width).") // Prints "The width of hd is still 1920."
---
output: The width of hd is still 1920.
When hd
is assigned to cinema
, the values stored in hd
are copied to the new cinema
instance. As a result, two completely independent instances contain the same values. Since they are independent, changing the width
of cinema
to 2048 does not affect the width
value in hd
, as shown in the diagram below:
Enumerations follow the same behavior:
enum CompassPoint {
case north, south, west, east
mutating func turnNorth() {
self = .north
}
}
var currentDirection = CompassPoint.west
let rememberDirection = currentDirection
currentDirection.turnNorth()
print("The current direction is \(currentDirection).") // Prints "The current direction is north."
print("The remember direction is \(rememberDirection).") // Prints "The remember direction is west."
---
output: The current direction is north.
The remember direction is west.
When rememberDirection
is assigned the value of currentDirection
, it actually receives a copy of the value. After the assignment, modifying the value of currentDirection
does not affect the original value stored in rememberDirection
.
Classes Are Reference Types
Unlike value types, reference types are not copied when assigned to a constant, variable, or passed to a function. Instead, the reference to an existing instance is used, not a copy.
Consider the following example, which uses the previously defined VideoMode
class:
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
In the example above, a constant named tenEighty
is declared and assigned a new instance of the VideoMode
class. Its video mode is set to a copy of the previously created HD resolution (1920 * 1080), then set to interlaced, named “1080i”, and its frame rate is set to 25.0 frames per second.
Next, tenEighty
is assigned to a new constant named alsoTenEighty
, and the frame rate of alsoTenEighty
is modified:
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
Because classes are reference types, alsoTenEighty
and tenEighty
refer to the same VideoMode
instance. In other words, they are two names for the same instance, as shown in the diagram below:
By checking the frameRate
property of tenEighty
, you can see that it correctly shows the new frame rate of 30.0 for the underlying VideoMode
instance:
print("The frameRate property of tenEighty is now \(tenEighty.frameRate).") // Prints "The frameRate property of tenEighty is now 30.0."
---
output: The frameRate property of tenEighty is now 30.0.
This example also demonstrates why reference types can be harder to understand. If tenEighty
and alsoTenEighty
are far apart in the code, it can be difficult to find all the places where the video mode is modified. Wherever you use tenEighty
, you need to consider the code for alsoTenEighty
, and vice versa. In contrast, value types are easier to understand because the code that interacts with the same value is usually close together in the source code.
Note that tenEighty
and alsoTenEighty
are declared as constants, not variables. However, you can still change tenEighty.frameRate
and alsoTenEighty.frameRate
because the values of the constants tenEighty
and alsoTenEighty
themselves have not changed. They do not “store” the VideoMode
instance, but merely reference it. Therefore, what is being changed is the frameRate
property of the underlying VideoMode
instance, not the value of the constant reference to VideoMode
.
Identity Operators
Because classes are reference types, multiple constants and variables can refer to the same class instance behind the scenes. (This is not true for structures and enumerations, which are value types and are always copied when assigned to a constant, variable, or passed to a function.)
It is useful to determine whether two constants or variables refer to the same class instance. To do this, Swift
provides two identity operators:
- Identical to (
===
) - Not identical to (
!==
)
Use these operators to check whether two constants or variables refer to the same instance:
if tenEighty === alsoTenEighty {
print("tenEighty and alsoTenEighty refer to the same VideoMode instance.") // Prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."
}
---
output: tenEighty and alsoTenEighty refer to the same VideoMode instance.
Note that “identical to” (three equals signs, ===
) is different from “equal to” (two equals signs, ==
). “Identical to” means that two class-type constants or variables refer to the same class instance. “Equal to” means that two instances have “equal” or “equivalent” values, as determined by the criteria defined by the designer.
When defining custom structures and classes, developers are responsible for deciding the criteria for determining whether two instances are “equal”. The Advanced Operators chapter will discuss how to implement custom ==
and !=
operators in detail.
Pointers
If you have experience with C
, C++
, or Objective-C
, you may know that these languages use pointers to reference memory addresses. In Swift
, constants or variables that reference an instance of a reference type are similar to pointers in C
, but they do not directly point to a memory address, nor do you need to use *
to indicate that you are creating a reference. Instead, references in Swift
are defined in the same way as other constants and variables. If you need to interact directly with pointers, you can use the pointer and buffer types provided by the standard library—see Manual Memory Management.