In Swift, classes and structures are very similar. Apple describes classes and structures as follows:
Classes and structures are general-purpose, flexible constructs that become the building blocks of your program's code. You define properties and methods to add functionality to your classes and structures by using the already familiar syntax of constants, variables, and functions.
Let's begin by taking a quick look at some of the similarities between classes and structures.
Similarities between classes and structures
The following is a list of some of the features that classes and structures share:
Now let's take a quick look at some of the differences between classes and structures.
Differences between classes and structures
While classes and structures are very similar, there are also several very important differences. The following is a list of some of the differences between classes and structures in Swift:
Keep in mind that, as our value types get large, the performance cost of copying can negate the other performance gains of value types. In the Swift standard library, Apple has implemented copy-on-write behavior to reduce the overhead of copying large value types.
With copy-on-write behavior, we do not create a new copy of our value type when we assign it to a new variable. The copy is postponed until one of the instances changes the value. This means that, if we have an array of one million numbers, when we pass this array to another array we will not make a copy of the one million numbers until one of the arrays changes. This can greatly reduce the overhead incurred from copying instances of our value types.
Value types are also a lot safer than reference types, because we do not have multiple references pointing to the same instance, as we do with reference types. This really becomes apparent when we are dealing with a multithreaded environment. Value types are also safer because we do not have memory leaks caused by common programming errors, such as the strong reference cycles.
Value versus reference types
To fully understand when to use classes and structures and how to properly use them, it is important to understand the difference between value and reference types.
Structures are value types. When we pass instances of a structure within our application, we pass a copy of the structure and not the original structure. Classes are reference types, therefore when we pass an instance of a class, within our application, a reference to the original instance is passed. It is very important to understand this difference.
When we pass structures within our application, we are passing copies of the structures and not the original structures. Since the function gets its own copy of the structure, it can change it as needed without affecting the original instance of the structure.
When we pass an instance of a class within our application, we are passing a reference to the original instance of the class. Since we're passing the instance of the class to the function, the function is getting a reference to the original instance; therefore, any changes made within the function will remain once the function exits.
Creating a class or structure
We use the same syntax to define classes and structures. The only difference is that we define a class using the class
keyword and a structure using the struct
keyword. Let's look at the syntax used to create both classes and structures:
class MyClass { // MyClass definition } struct MyStruct { // MyStruct definition }
In the preceding code, we define a new class named MyClass
and a new structure named MyStruct
. This effectively creates two new Swift types named MyClass
and MyStruct
. When we name a new type, we want to use the standard naming convention set by Swift, where the name is in camel case with the first letter being uppercase. This is also known as PascalCase. Any method or property defined within the class or structure should also be named using camel case with the first letter being uppercase.
Access controls
Access controls enable us to hide implementation details and only expose the interfaces we want to expose. This feature is handled with access controls. We can assign specific access levels to both classes and structures. We can also assign specific access levels to properties, methods, and initializers that belong to our classes and structures.
In Swift, there are five access levels:
There are some limitations with access controls, but these limitations are there to ensure that access levels in Swift follow a simple guiding principle - no entity can be defined in terms of another entity that has a lower (more restrictive) access level. What this means is we cannot assign a higher (less restrictive) access level to an entity when it relies on another entity that has a lower (more restrictive) access level.
The following examples demonstrate this principle:
Properties
Properties associate values with a class or a structure. There are two types of properties:
A stored property is a variable or constant that is stored as part of an instance of a class or structure. These are defined with the var and let
keywords just like normal variables and constants. In the following code, we will create a structure named MyStruct
and a class named MyClass
. The structure and the class both contain two stored properties, c
and v
.
The stored property c
is a constant because it is defined with the let
keyword, and v
is a variable because it is defined with the var
keyword. Let's look at the following code:
struct MyStruct { let c = 5 var v = "" } class MyClass { let c = 5 var v = "" }
As we can see from the example, the syntax to define a stored property is the same for both classes and structures. Let's look at how we would create an instance of both the structure and class. The following code creates an instance of the MyStruct
structure named myStruct
and an instance of the MyClass
class named myClass
:
var myStruct = MyStruct() var myClass = MyClass()
One of the differences between structures and classes is that, by default, a structure creates an initializer that lets us populate the stored properties when we create an instance of the structure. Therefore, we could also create an instance of MyStruct like this:
var myStruct = MyStruct(v: "Hello")
In the preceding example, the initializer is used to set the variable v
, and the c
constant will still contain the number 5 that is defined in the structures.
The order in which the parameters appear in the initializer is the order that we defined them.
To set or read a stored property, we use the standard dot syntax. Let's look at how we would set and read stored properties in Swift:
var x = myClass.c myClass.v = "Howdy"
Computed properties are properties that do not have backend variables that are used to store the values associated with the property. The values of a computed property are usually computed when code requests it. You can think of a computed property as a function disguised as a property. Let's look at how we would define a read-only computed property:
var salaryWeek: Double { get{ return self.salaryYear/52 } }
We can simplify the definition of the read-only computed property by removing the get
keyword, as shown in the following example:
var salaryWeek: Double { return self.salaryYear/52 }
Computed properties are not limited to being read-only, we can also write to them. To enable the salaryWeek
property to be writeable, we will add a setter method. The following example shows how we add a setter method that will set the salaryYear
property, based on the value being passed into the salaryWeek
property:
var salaryWeek: Double { get { return self.salaryYear/52 } set (newSalaryWeek){ self.salaryYear = newSalaryWeek*52 } }
Inheritance
The concept of inheritance is a basic object-oriented development concept. Inheritance allows a class to be defined as having a certain set of characteristics and then other classes can be derived from that class. The derived class inherits all of the features of the class it is inheriting from (unless the derived class overrides those characteristics) and then usually adds additional characteristics of its own.
With inheritance, we can create what is known as a class hierarchy. In a class hierarchy, the class at the top of the hierarchy is known as the base class and the derived classes are known as subclasses. We are not limited to only creating subclasses from a base class, we can also create subclasses from other subclasses. The class that a subclass is derived from is known as the parent or superclass. In Swift, a class can have only one parent class. This is known as single inheritance.
Subclasses can call and access the properties, methods, and subscripts of their superclass. They can also override the properties, methods, and subscripts of their superclass. Subclasses can add property observers to properties that they inherit from a superclass, so they can be notified when the values of the properties change. Let's look at an example that illustrates how inheritance works in Swift.
We will start off by defining a base class named Plant
. The Plant
class will have two properties, height
and age
. It will also have one method, growHeight()
. The height
property will represent the height of the plant, the age
property will represent the age of the plant, and the growHeight()
method will be used to increase the height of the plant. Here is how we would define the Plant
class:
class Plant { var height = 0.0 var age = 0 func growHeight(inches: Double) { height += inches; } }
Now that we have our Plant
base class, let's see how we would define a subclass of it. We will name this subclass Tree
. The Tree
class will inherit the age
and height
properties of the Plant
class and add one more property named limbs
. It will also inherit the growHeight()
method of the Plant
class and add two more methods: limbGrow()
, where new limbs are grown, and limbFall()
, where limbs fall off the tree. Let's have a look at the following code:
class Tree: Plant var limbs = 0 func limbGrow() self.limbs += 1 } func limbFall() self.limbs -= 1 } }
We indicate that a class has a superclass by adding a colon and the name of the superclass to the end of the class definition. In this example, we indicated that the Tree
class has a superclass named Plant
. Now, let's look at how we could use the Tree
class that inherited the age
and height
properties from the Plant
class:
var tree = Tree() tree.age = 5 tree.height = 4 tree.limbGrow() tree.limbGrow()
Protocols
There are times when we would like to describe the implementations (methods, properties, and other requirements) of a type without actually providing any implementation. For this, we can use protocols.
Protocols define a blueprint of methods, properties, and other requirements for a class or a structure. A class or a structure can then provide an implementation that conforms to those requirements. The class or structure that provides the implementation is said to conform to the protocol.
The syntax to define a protocol is very similar to how we define a class or a structure. The following example shows the syntax used to define a protocol:
protocol MyProtocol { //protocol definition here }
We state that a class or structure conforms to a protocol by placing the name of the protocol after the type's name, separated by a colon. Here is an example of how we would state that a structure conforms to the MyProtocol
protocol:
struct MyStruct: MyProtocol { //class implementation here }
A type can conform to multiple protocols. We list the protocols that the type conforms to by separating them with commas. The following example shows how we would state that our structure conforms to multiple protocols:
struct MyStruct: MyProtocol, AnotherProtocol, ThirdProtocol { // class implementation here }
If we need a class to both inherit from a superclass and implement a protocol, we would list the superclass first, followed by the protocols. The following example illustrates this:
class MyClass: MySuperClass, MyProtocol, MyProtocol2 { // Class implementation here }
A protocol can require that the conforming type provides certain properties with a specified name and type. The protocol does not say if the property should be a stored or computed property because the implementation details are left up to the conforming type.
When defining a property within a protocol, we must specify whether the property is a read-only or a read-write property by using the get
and set
keywords. Let's look at how we would define properties within a protocol by creating a protocol named FullName
:
protocol FullName { var firstName: String { get set } var lastName: String { get set } }
The FullName
protocol defines two properties, which any type that conforms to the protocol must implement. These are the firstName
and lastName
properties and both are read-write properties. If we wanted to specify that the property is read-only, we would define it with only the get
keyword, like this:
var readOnly: String { get }
A protocol can require that the conforming class or structure provides certain methods. We define a method within a protocol exactly as we do within a class or structure, except without the curly braces or method body. Let's add a getFullName()
method to FullName
protocol and Scientist
class.
protocol FullName { var firstName: String { get set } var lastName: String { get set } func getFullName() -> String }
Now, we will need to add a getFullName()
method to Scientist
class so that it will conform to the protocol:
class Scientist: FullName { var firstName = "" var lastName = "" var field = "" func getFullName() -> String { return "\(firstName) \(lastName) studies \(field)" } }
Associated types with protocols
When defining a protocol, there are times when it is useful to define one or more associated types. An associated type gives us a placeholder name that we can use within the protocol in place of a type. The actual type to use for the associated type is not defined until the protocol is adopted. The associated type basically says: we don't know the exact type to use; therefore, when a type adopts this protocol, it will define it. As an example, if we were to define a protocol for a queue, we would want the type that adopts the protocol to define the instance types that the queue contains rather than the protocol.
To define an associated type, we use the associatedtype
keyword. Let's see how we can use associated types within a protocol. In this example, we will illustrate the Queue
protocol, which will define the requirements that are needed to implement a queue:
protocol Queue { associatedtype QueueType mutating func addItem(item: QueueType) mutating func getItem() -> QueueType? func count() -> Int }
In this protocol, we define one associated type named QueueType
. We then use this associated type twice within the protocol. First, we use it as the parameter type for the addItem()
method, and then we use it again when we define the return type of the getItem()
method as an optional type.
Any type that implements the Queue
protocol must specify the type to use for the QueueType
placeholder, and must also ensure that only items of that type are used where the protocol requires the QueueType
placeholder.
Let's look at how to implement Queue
in a non-generic class called IntQueue
. This class will implement the Queue
protocol using the integer type:
struct IntQueue: Queue { var items = [Int]() mutating func addItem(item: Int) { items.append(item) } mutating func getItem() -> Int? { if items.count > 0 { return items.remove(at: 0) } else { return nil } } func count() -> Int { return items.count } }
As we can see in the IntQueue
structure, we use the integer type for both the parameter type of the addItem()
method and the return type of the getItem()
method. In this example, we implemented the Queue
protocol in a non-generic way. Generics in Swift allow us to define the type to use at runtime rather than compile time.
The heap vs. the stack
When you create a reference type such as class
, the system stores the actual instance in a region of memory known as the heap. Instances of a value type such as a struct
resides in a region of memory called the stack, unless the value is part of a class instance, in which case the value is stored on the heap with the rest of the class instance.
Both the heap and the stack have essential roles in the execution of any program. A general understanding of what they are and how they work will help you visualize the functional differences between a class and a structure:
Structures vs. classes recap
Structures
Classes
The biggest reason for using structures (value types) over classes is the performance gain we get. Value types do not incur the additional overhead for reference counting that reference types incur. Value types are also stored on the stack, which provides better performance as compared to reference types, which are stored on the heap. It is also worth noting that copying values is relatively cheap in Swift.
Keep in mind that, as our value types get large, the performance cost of copying can negate the other performance gains of value types. In the Swift standard library, Apple has implemented copy-on-write behavior to reduce the overhead of copying large value types.