Object classes
Object-oriented programming is currently the dominant programming paradigm. At the core of this paradigm is the object class. Objects allow us to encapsulate data and functionality, which can then be stored and passed around.
Getting ready
Let's build some class objects. Then, we'll break down the components of the class to understand how it is defined and used.
How to do it...
Let's start by entering the following code into the playground:
class Person { let givenName: String let middleName: String let familyName: String var countryOfResidence: String = "UK" init(givenName: String, middleName: String, familyName: String) { self.givenName = givenName self.middleName = middleName self.familyName = familyName } var displayString: String { return "\(fullName()) - Location: \(countryOfResidence)" } func fullName() -> String { return "\(givenName) \(middleName) \(familyName)" } } final class Friend: Person { var whereWeMet: String? override var displayString: String { return "\(super.displayString) - \(whereWeMet ?? "Don't know where we met")" } } final class Family: Person { let relationship: String init(givenName: String, middleName: String, familyName: String = "Moon", relationship: String) { self.relationship = relationship super.init(givenName: givenName, middleName: middleName, familyName: familyName) } override var displayString: String { return "\(super.displayString) - \(relationship)" } } let steve = Person(givenName: "Steven", middleName: "Paul", familyName: "Jobs") let dan = Friend(givenName: "Daniel", middleName: "James", familyName: "Woodel") dan.whereWeMet = "Worked together at BBC News" let finnley = Family(givenName: "Finnley", middleName: "David", relationship: "Son") let dave = Family(givenName: "Dave", middleName: "deRidder", familyName: "Jones", relationship: "Father-In-Law") dave.countryOfResidence = "US" print(steve.displayString) // Steven Paul Jobs print(dan.displayString) // Daniel James Woodel - Worked together at BBC News print(finnley.displayString) // Finnley David Moon - Son
How it works...
Classes are defined with the class
keyword, class names start with a capital letter by convention, and the implementation of the class is contained, or "scoped", within curly brackets:
class Person { //... }
An object can have property values, which are contained within the object. These properties can have initial values, as countryOfResidence
does in the following code, although bear in mind that constants (defined with let
) cannot be changed once the initial value has been set:
class Person { let givenName: String let middleName: String let familyName: String var countryOfResidence: String = "UK" //... }
If your class were to just have the preceding property definitions, the compiler would raise a warning, as givenName
, middleName
, and familyName
are defined as non-optional strings, but we have not provided any way to populate those values.
The compiler needs to know how the object will be initialized, so that we can be sure that all the non-optional properties will indeed have values:
class Person { let givenName: String let middleName: String let familyName: String var countryOfResidence: String = "UK" init(givenName: String, middleName: String, familyName: String) { self.givenName = givenName self.middleName = middleName self.familyName = familyName } //... }
The init
is a special method (functions defined within objects are called methods) that's called when the object is initialized. In the Person
object of the preceding code, we expect givenName
, middleName
, and familyName
to be passed in when the object is initialized, and we assign those provided values to the object's properties. The self.
prefix is used to differentiate between the property and the value passed in as they have the same name.
We do not need to pass in a value for countryOfResidence
as we defined an initial value when the property was defined. This isn't ideal, though, as when we initialize a Person
object, it will always have the countryOfResidence
variable set to "UK"
, and we will have to change that value after initializing. Another way to do this would be to use a default parameter value, as seen in the previous recipe. Amend the Person
object initialization to the following:
class Person { let givenName: String let middleName: String let familyName: String var countryOfResidence: String init(givenName: String, middleName: String, familyName: String, countryOfResidence: String = "UK") { self.givenName = givenName self.middleName = middleName self.familyName = familyName self.countryOfResidence = countryOfResidence } //... }
Now, you can provide a country of residence in the initialization or omit it to use the default value:
class Person { //... var displayString: String { return "\(fullName()) - Location: \(countryOfResidence)" } //... }
The property declaration for displayString
is different from the others. Rather than having a value assigned to it, it is followed by an expression contained within curly braces. This is a computed property; its value is not static, but is determined by the given expression every time the property is accessed. Any valid expressions can be used to compute the property, but must return a value that matches the property type that is declared. The compiler will enforce this, and you can't omit the variable type for computed properties.
Since the value of the property is determined at the time of access, it follows that computed properties are read-only:
class Person { //... func fullName() -> String { return "\(givenName) \(middleName) \(familyName))" } //... }
Objects can do work based on the information they contain, and this work can be defined in methods. Methods are just functions that are contained within classes and have access to all the object's properties. All the abilities of a function are available, which we explored in the last recipe, including optional inputs and outputs, default parameter values, and parameter overloading:
final class Friend: Person { var whereWeMet: String? //... }
Having defined a Person
object, we want to extend the concept of Person
to define a friend. A friend is also a person, so it stands to reason that anything a Person
object can do, a Friend
object can also do. We model this inherited behavior by defining Friend
as a subclass of Person
. We define the class that our Friend
class inherits from after the class name, separated by :
.
By inheriting from Person
, our Friend
object inherits all the properties and methods from its superclass. We can add any extra functionality we require--in this case, a property holding details of where we met this friend.
The final
prefix tells the compiler that we don't intend for this class to be subclassed; it is the final class in the inheritance hierarchy. This allows the compiler to make some optimizations as we know it won't be extended:
final class Friend: Person { //... override var displayString: String { return "\(super.displayString) - \(whereWeMet ?? "Don't know where we met")" } }
In addition to implementing new functionalities, we can override functionalities from the superclass using the override
keyword. In the preceding code, we override the displayString
computed property from Person
as we want to add the "where we met"
information. Within the computed property, we want to get the superclass's implementation; we do this by referencing super
and .
, and then referencing the property. We can do the same to access the superclass's methods:
final class Family: Person { let relationship: String init(givenName: String, middleName: String, familyName: String = "Moon", relationship: String) { self.relationship = relationship super.init(givenName: givenName, middleName: middleName, familyName: familyName) } //... }
Our Family
class also inherits from Person
, and we want to add a relationship
property, which we want to form part of the initialization, so we can declare a new init
that also takes a relationship value.
There's more...
Class objects are reference types that refer to the way they are stored and referenced internally. To see how these reference type semantics work, consider the following code:
class MovieReview { let movieTitle: String var starRating: Int // Rating out of 5 init(movieTitle: String, starRating: Int) { self.movieTitle = movieTitle self.starRating = starRating } } // Write a review let shawshankReviewOnYourWebsite = MovieReview(movieTitle: "Shawshank Redemption", starRating: 3) // Post it to social media let reviewLinkOnTwitter = shawshankReviewOnYourWebsite let reviewLinkOnFacebook = shawshankReviewOnYourWebsite print(reviewLinkOnTwitter.starRating) // 3 print(reviewLinkOnFacebook.starRating) // 3 // Reconsider my review shawshankReviewOnYourWebsite.starRating = 5 // The change visible from anywhere with a reference to the object print(reviewLinkOnTwitter.starRating) // 5 print(reviewLinkOnFacebook.starRating) // 5
We created a review object and assigned that review to two separate constants. As an object is a reference type, it is a reference to the object that is stored in the constant, rather than a new copy of the object. Therefore, when we reconsider our review and rightly give The Shawshank Redemption five stars, we are changing the underlying object, and all references that access that underlying object will see that the starRating
property has changed.
See also
- Further information about classes can be found in Apple's documentation of the Swift language at http://swiftbook.link/docs/classes-and-structures.
- In Chapter 8, Performance and Responsiveness in Swift, we will examine reference semantics in more detail, and see how it affects performance.