Classes, Objects, and Inheritance - OBJECT-ORIENTED PROGRAMMING - Understanding Swift Programming: Swift 2 (2015)

Understanding Swift Programming: Swift 2 (2015)

PART 2: OBJECT-ORIENTED PROGRAMMING

12. Classes, Objects, and Inheritance

Swift is a conventional object-oriented language that encapsulates data (in the form of properties) and behavior (in the form of methods) together in individual objects.

Swift is based on classes. A class is a sort of blueprint for creating objects, or instances of a class.

The definition of a class includes the definition of properties, which are variables and constants associated with the class. A property uses the same syntax as a variable or constant and is associated with a class when the property is declared within that class's definition. In Swift a property, which can be accessed from outside the class with dot syntax, is not any different from what in other object oriented languages is known as an instance variable and can only be accessed from within the class.

A definition of a class also typically defines methods, which are functions associated with a class.

Objects in Swift have inheritance, in which a class, and its objects, inherit the properties and methods of another class. Swift has single inheritance, meaning that an object can inherit from only one other class, known as its superclass. (A class also inherits all of the properties and methods that its superclass has inherited.) A class that is derived from a superclass is known as its subclass.

Swift classes and their objects have many capabilities. In addition to the usual instance properties, which are associated with each instance of the class, Swift classes can also have class or type properties, which are associated with the class itself rather than any instance. (Note: Class/type properties don’t yet work for classes as of Swift 1.2.) They can have computed properties, which calculate values rather than retrieve them from memory. Swift classes can have lazy properties, for which values are not created until they are needed. And they can have property observers—code that is executed when a value is stored to a property). These additional properties and related capabilities are described in a later chapter, Chapter 16 on "Classes Revisited: Properties".

When a new instance of a class is created, it must be initialized. This chapter will describe how this is done in the simplest cases, but initializing classes can get quite complicated, particularly when classes inherit from other classes. Chapter 29 on "Classes Revisited Again: Initializers" describes these in detail.

Classes can also define custom subscripts, allowing information to be read or written to like this: johnsTenFavoriteMovies[3]. See Chapter 24 on "Custom Subscripts" for a description of how this works.

Objects that are created based on a class definition, that is, instances, are stored (along with their properties) in the heap part of random-access-memory as a reference type. That means that when they are accessed—assigned to a variable or passed as a parameter in a function or a return value of a function—only a reference is created to the stored information. There is only one copy kept, and if one entity that references it makes a change to the information, other entities that reference it will see that change. The alternative is a value type, which is used by structures. For more details about how this works see Chapter 14 on Structures.

Defining A Class

An example of the syntax needed for defining a new custom class that includes a few properties and a method is as follows:

class Fruit {

var skinColor:String = "red"

var fleshColor:String = "white"

var countryGrownIn = "United States"

var hasSeeds = true

func sayWhereItWasGrown () {

print("This fruit was grown in: \(countryGrownIn)")

}

}

The name of the new class is Fruit. This class does not inherit from any other class, and as such is known as a a base class.

By convention (though not required), the first letter of a class name is in upper case and the remainder of the name is in camel case. (If the name was FruitCake the camel case would be obvious.) Also by convention, the first letter of a property or method name is in lower case and the remainder of the name is in camel case.

Properties can be either variables or constants, and are defined using the keywords var and let just like variables and constants are if they are not defined in a class. Types can be explicitly defined with type annotation, or they can be inferred.

A class definition often contains an initializer, code that sets the initial values of properties when a new instance is created. In this case we don't need an initializer because we have set the initial values of properties where they are declared. We'll discuss initializers in a later chapter: Chapter 29 on Classes Revisited Again: Initializers.

This class holds information about different kinds of fruit that you might have in your kitchen, including their skin color, flesh color, whether they have seeds, and the country they were grown in.

We can create an instance of the class representing a particular piece of fruit by specifying the class name followed by a pair of empty parentheses:

let thisPieceOfFruit = Fruit()

We first declare a constant thisPieceOfFruit. We then call Fruit() to instantiate, that is, create, a new instance of, the class Fruit, based on the class definition (blueprint for an object) and assign a reference to that instance to thisPieceOfFruit. This new instance is an object.

Accessing the Properties of a Class

We can then access the properties associated with this instance (object) by using this constant and dot syntax.

Thus,

var colorOfFruit = thisPieceOfFruit.skinColor

print("The outside color of this piece of fruit is \(colorOfFruit)")

// Prints: The outside color of this piece of fruit is red

If we discover that this particular piece of fruit is green on the outside, we can change its skin color with dot syntax:

thisPieceOfFruit.skinColor = "green"

And if we run these same lines of code again we'll get a different result:

colorOfFruit = thisPieceOfFruit.skinColor

print("The outside color of this piece of fruit is \(colorOfFruit)")

// Prints: The outside color of this piece of fruit is green

We can also create two instances of the class Fruit, and set each to a different color:

let pieceOfFruit1 = Fruit()

let pieceOfFruit2 = Fruit()

pieceOfFruit2.skinColor = "yellow"

print("Colors are \(pieceOfFruit1.skinColor) & \(pieceOfFruit2.skinColor)")

// Prints: Colors are red & yellow

(The first instance was set to red as part of initializing the instance, while the second was specifically set to yellow.)

Each instance has its own copy of the instance property skinColor, and can store a different value in that property.

Using the Methods of a Class

We can execute the method associated with this class by specifying it with dot syntax:

let thisPieceOfFruit = Fruit()

thisPieceOfFruit.sayWhereItWasGrown()

// Prints: This piece of fruit was grown in: United States.

A method of a class (that is, an instance method) is executed by specifying the name of a variable or constant that contains a reference to the instance, a dot, the name of the method, and a pair of parentheses.

The method accesses the copy of the properties associated with the particular instance that called the method.

A New Class is a New Data Type

The new class we have defined is a new data type named Fruit, and we can use it like any other data type in Swift.

We earlier created an instance of the class as follows:

let thisPieceOfFruit = Fruit()

We didn't discuss the data type of the constant that was created, but in fact the constant thisPieceOfFruit was inferred to be of type Fruit.

If we like we can explicitly set the type:

let thisPieceOfFruit: Fruit = Fruit()

We can also create two instances, that is, two objects of the class Fruit, and put both into an array:

let pieceOfFruit1 = Fruit()

let pieceOfFruit2 = Fruit()

pieceOfFruit2.skinColor = "green"

let arrayWithFruit: [Fruit] = [pieceOfFruit1,pieceOfFruit2]

Here we have explicitly declared the constant arrayWithFruit to be an array that consists of objects of the class Fruit, and then initialized the array with different objects of that class. (I changed the skinColor property of the second instance to "green" so that the array would have objects that were truly different.)

The point here is that objects that you create with a new class that you have defined have their own data type as defined by that class, and this type is used when you manipulate these objects, such as by putting them into an array.

Inheritance

Now let's say we are interested in the banana we have in our kitchen. The class above is for fruits in general, and assumes that every fruit is red in color and was grown in the United States. (In a later chapter we will discuss better ways of initializing instances.)

We could just create a new instance of the class Fruit and then set the skinColor property to "yellow" and the countryGrownIn property to where it was grown.

But suppose we have lots of bananas, some purple. We'd like to have a class where the default color is yellow and the default country is somewhere that actually grows bananas. We'd also like to set the default value of the hasSeeds property for this new class to false, if most of the bananas we will be dealing with are the consumer variety, which do not have seeds.

We can create a class named Banana that is a subclass of the existing class, Fruit.

The following is a simplified version of the Fruit class:

class Fruit {

var countryGrownIn = "United States"

func sayWhereItWasGrown () {

print("This fruit was grown in: \(countryGrownIn)")

}

}

And now its subclass, Banana:

class Banana: Fruit {

}

We can see in this simple case how the inheritance of both properties and methods work. We can create an instance of the class Banana:

var aBanana = Banana()

This should have not only created an instance of the class Banana, but this instance should have inherited the property countryGrownIn. We can see if this happened:

print("Value of aBanana.countryGrownIn is \(aBanana.countryGrownIn)")

// Prints: Value of aBanana.countryGrownIn is United States

Not only did the new instance of Banana inherit the property countryGrownIn from its superclass Fruit, but it inherited a value. What happened here is that as part of the default initialization of aBanana, a call was made to initialize the properties of the superclass Fruit.

This tells us that instances of subclasses like Banana can in some circumstances inherit not only the properties of their superclasses, but also their values.

We can create an instance of the class Fruit:

var aPieceOfFruit = Fruit()

And we can change the value of the property CountryGrownIn for both the fruit and the banana:

aPieceOfFruit.countryGrownIn = "Canada"

aBanana.countryGrownIn = "Panama"

And see what happens:

print("Value of aPieceOfFruit.countryGrownIn is \(aPieceOfFruit.countryGrownIn)")

// Prints: Value of aPieceOfFruit.countryGrownIn is Canada

print("Value of aBanana.countryGrownIn is \(aBanana.countryGrownIn)")

// Prints: Value of aBanana.countryGrownIn is Panama

This tells us that when a subclass creates a new instance, that instance inherits a property from its superclass and its initial value. But thereafter the values of these properties are independent.

In addition to inheriting the property CountryGrownIn, the subclass Banana also inherits the method sayWhereItWasGrown. We can verify that this works as follows:

aBanana.sayWhereItWasGrown()

// Prints: This fruit was grown in Panama

Overriding a Method of a Superclass

In the example above, we have a method sayWhereItWasGrown() in the superclass Fruit. If we create an instance of Fruit, we can execute that method, which prints the value of the property countryGrownIn:

let aPieceOfFruit = Fruit()

aPieceOfFruit.sayWhereItWasGrown()

// Prints: This fruit was grown in United States

If we create an instance of Banana, we get the same thing, because the method sayWhereItWasGrown is inherited:

let aBanana = Banana()

aBanana.sayWhereItWasGrown()

// Prints: This fruit was grown in United States

We can if we wish override an inherited method. So we can, for example, provide the following definitions of Fruit and Banana:

class Fruit {

var color: String = "red"

var countryGrownIn = "United States"

var hasSeeds = true

func sayWhereItWasGrown () {

print("This fruit was grown in: \(countryGrownIn)")

}

}

class Banana: Fruit {

override func sayWhereItWasGrown () {

print("This BANANA was grown in: \(countryGrownIn)")

}

}

The definition of the class Banana only does two things: (1) Defines a new class Banana; and (2) Provides a method that overrides, that is, replaces, the method that it inherited from its superclass Fruit.

If we create an instance of the class Fruit, we can see that the original method is still there:

aPieceOfFruit = Fruit()

aPieceOfFruit.sayWhereItWasGrown()

// Prints: This fruit was grown in: United States

If we create an instance of the class Banana, we can execute the overridden method:

aBanana = Banana()

aBanana.sayWhereItWasGrown()

// Prints: This BANANA was grown in: United States

Note that the keyword override is used before the func keyword in the definition of the subclass’s version of the function. This is required for redefinitions of functions that are inherited. If it is not included you will get a compiler error. Furthermore, if it is included and the function was not inherited (because it was not defined in a superclass), you will also get a compiler error. These are both safety checks that are, for example, not in Objective-C.

This chapter has described the basic aspects of how classes work, including how to define classes, create instances, how to use (instance) properties and (instance) methods, and how inheritance works.

These are the most-used aspects of classes. There are also some other kinds of properties that are used a little less, as well as an alternative type of method, the class method. These are described in a later chapter, Chapter 16, on "Classes Revisited: Properties and Class or Type Methods."

In addition, initialization, particularly when inheritance is involved, can get quite complex. In the example we have seen, we just initialized properties directly in their declaration, and depended on default behavior to initialize the superclass when necessary. But it often gets more complicated. This is described in detail in Chapter 29, on "Classes Revisited Again: Initializers."

Hands-On Exercises

Go to the following web address with a Macintosh or Windows PC to do the Hands-On Exercises.

For Chapter 12 exercises, go to

understandingswiftprogramming.com/12