7 min read

In this article by Gastón Hillar, author of the book Object-Oriented Programming with Swift, we will learn how to create mutable and immutable classes in Swift.

(For more resources related to this topic, see here.)

Creating mutable classes

So far, we worked with different type of properties. When we declare stored instance properties with the var keyword, we create a mutable instance property, which means that we can change their values for each new instance we create. When we create an instance of a class that defines many public-stored properties, we create a mutable object, which is an object that can change its state.

For example, let’s think about a class named MutableVector3D that represents a mutable 3D vector with three public-stored properties: x, y, and z. We can create a new MutableVector3D instance and initialize the x, y, and z attributes. Then, we can call the sum method with the delta values for x, y, and z as arguments. The delta values specify the difference between the existing and new or desired value. So, for example, if we specify a positive value of 30 in the deltaX parameter, it means we want to add 30 to the X value. The following lines declare the MutableVector3D class that represents the mutable version of a 3D vector in Swift:

public class MutableVector3D {
   public var x: Float
   public var y: Float
   public var z: Float

   init(x: Float, y: Float, z: Float) {
       self.x = x
       self.y = y
       self.z = z
   }

   public func sum(deltaX: Float, deltaY: Float, deltaZ: Float) {
           x += deltaX
           y += deltaY
           z += deltaZ
   }

   public func printValues() {
       print("X: (self.x), Y: (self.y), Z: (self.z))")
   }
}

Note that the declaration of the sum instance method uses the func keyword, specifies the arguments with their types enclosed in parentheses, and then declares the body for the method enclosed in curly brackets. The public sum instance method receives the delta values for x, y, and z (deltaX, deltaY and deltaZ) and mutates the object, which means that the method changes the values of x, y, and z. The public printValues method prints the values of the three instance-stored properties: x, y, and z.

The following lines create a new MutableVector3D instance method called myMutableVector, initialized with the values for the x, y, and z properties. Then, the code calls the sum method with the delta values for x, y, and z as arguments and finally calls the printValues method to check the new values after the object mutated with the call to the sum method:

var myMutableVector = MutableVector3D(x: 30, y: 50, z: 70)
myMutableVector.sum(20, deltaY: 30, deltaZ: 15)
myMutableVector.printValues()

The results of the execution in the Playground are shown in the following screenshot:

The initial values for the myMutableVector fields are 30 for x, 50 for y, and 70 for z. The sum method changes the values of the three instance-stored properties; therefore, the object state mutates as follows:

  • myMutableVector.X mutates from 30 to 30 + 20 = 50
  • myMutableVector.Y mutates from 50 to 50 + 30 = 80
  • myMutableVector.Z mutates from 70 to 70 + 15 = 85

The values for the myMutableVector fields after the call to the sum method are 50 for x, 80 for y, and 85 for z. We can say that the method mutated the object’s state; therefore, myMutableVector is a mutable object and an instance of a mutable class.

It’s a very common requirement to generate a 3D vector with all the values initialized to 0—that is, x = 0, y = 0, and z = 0. A 3D vector with these values is known as an origin vector. We can add a type method to the MutableVector3D class named originVector to generate a new instance of the class initialized with all the values in 0. Type methods are also known as class or static methods in other object-oriented programming languages. It is necessary to add the class keyword before the func keyword to generate a type method instead of an instance. The following lines define the originVector type method:

public class func originVector() -> MutableVector3D {
   return MutableVector3D(x: 0, y: 0, z: 0)
}

The preceding method returns a new instance of the MutableVector3D class with 0 as the initial value for all the three elements. The following lines call the originVector type method to generate a 3D vector, the sum method for the generated instance, and finally, the printValues method to check the values for the three elements on the Playground:

var myMutableVector2 = MutableVector3D.originVector()
myMutableVector2.sum(5, deltaY: 10, deltaZ: 15)
myMutableVector2.printValues()

The following screenshot shows the results of executing the preceding code in the Playground:

Creating immutable classes

Mutability is very important in object-oriented programming. In fact, whenever we expose mutable properties, we create a class that will generate mutable instances. However, sometimes a mutable object can become a problem and in certain situations, we want to avoid the objects to change their state. For example, when we work with concurrent code, an object that cannot change its state solves many concurrency problems and avoids potential bugs.

For example, we can create an immutable version of the previous MutableVector3D class to represent an immutable 3D vector. The new ImmutableVector3D class has three immutable instance properties declared with the let keyword instead of the previously used var[SI1]  keyword: x, y, and z. We can create a new ImmutableVector3D instance and initialize the immutable instance properties. Then, we can call the sum method with the delta values for x, y, and z as arguments.

The sum public instance method receives the delta values for x, y, and z (deltaX, deltaY, and deltaZ), and returns a new instance of the same class with the values of x, y, and z initialized with the results of the sum. The following lines show the code of the ImmutableVector3D class:

public class ImmutableVector3D {
   public let x: Float
   public let y: Float
   public let z: Float

   init(x: Float, y: Float, z: Float) {
        self.x = x
       self.y = y
       self.z = z
   }

   public func sum(deltaX: Float, deltaY: Float, deltaZ: Float) -> ImmutableVector3D {
       return ImmutableVector3D(x: x + deltaX, y: y + deltaY, z: z + deltaZ)
   }

   public func printValues() {
       print("X: (self.x), Y: (self.y), Z: (self.z))")
   }

   public class func equalElementsVector(initialValue: Float) -> ImmutableVector3D {
       return ImmutableVector3D(x: initialValue, y: initialValue, z: initialValue)
   }
   public class func originVector() -> ImmutableVector3D {
      return equalElementsVector(0)
   }
}

In the new ImmutableVector3D class, the sum method returns a new instance of the ImmutableVector3D class—that is, the current class. In this case, the originVector type method returns the results of calling the equalElementsVector type method with 0 as an argument.

The equalElementsVector type method receives an initialValue argument for all the elements of the 3D vector, creates an instance of the actual class, and initializes all the elements with the received unique value. The originVector type method demonstrates how we can call another type method within a type method. Note that both the type methods specify the returned type with -> followed by the type name (ImmutableVector3D) after the arguments enclosed in parentheses. The following line shows the declaration for the equalElementsVector type method with the specified return type:

public class func equalElementsVector(initialValue: Float) -> ImmutableVector3D {

The following lines call the originVector type method to generate an immutable 3D vector named vector0 and the sum method for the generated instance and save the returned instance in the new vector1 variable. The call to the sum method generates a new instance and doesn’t mutate the existing object:

var vector0 = ImmutableVector3D.originVector()
var vector1 = vector0.sum(5, deltaX: 10, deltaY: 15)
vector1.printValues()

The code doesn’t allow the users of the ImmutableVector3D class to change the values of the x, y, and z properties declared with the let keyword. The code doesn’t compile if you try to assign a new value to any of these properties after they were initialized. Thus, we can say that the ImmutableVector3D class is 100 percent immutable.

Finally, the code calls the printValues method for the returned instance (vector1) to check the values for the three elements on the Playground, as shown in the following screenshot:

The immutable version adds an overhead compared with the mutable version because it is necessary to create a new instance of the class as a result of calling the sum method. The previously analyzed mutable version just changed the values for the attributes, and it wasn’t necessary to generate a new instance. Obviously, the immutable version has both a memory and performance overhead. However, when we work with concurrent code, it makes sense to pay for the extra overhead to avoid potential issues caused by mutable objects. We just have to make sure we analyze the advantages and tradeoffs in order to decide which is the most convenient way of coding our specific classes.

Summary

In this article, we learned how to create mutable and immutable classes in Swift.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here