Swift Learning (11) - Classes and Structures (Improved Code Version)

Swift Learning (11) - Classes and Structures (Improved Code Version)

August 30, 2021·Jingyao Zhang
Jingyao Zhang

image

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. Use UpperCamelCase for type names (such as SomeClass and SomeStructure) to match the standard Swift type naming convention (like String, Int, and Bool). Use lowerCamelCase 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:

SharedState Struct

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:

SharedState Class

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.

Last updated on