Operator Overloading and User-Defined Subscripts in Swift

Operator Overloading and User-Defined Subscripts in Swift

Learn how to overload operators in Swift and add custom subscripting to access the underlying data of Structs and Classes.

Go to the profile of  Jonathan Banks
Jonathan Banks
11 min read
Time
1.5hours
Platforms
iOS & Mac
Difficulty
Medium
Technologies
Swift

If you learned how to program using C++ in your career, you'd remember a fantastic feature of the language called operator overloading.  As a refresher, the C++ language lets you overload the standard mathematical operators within your classes.  Thus, you can design classes that can add, subtract, multiply, and divide two or more instances of themselves.

It is an extremely useful feature of C++ and you'd expect many modern languages to incorporate it into their suite of features for power-users. The reality is there are only a limited number of popular programming languages that feature it.  

Luckily, Swift happens to be a popular language that took the wise route and adopted it!  Continue reading to learn how to weave this powerful feature of Swift into your code.

Project Code

Download the complete Playground with working code for this tutorial from this Bitbucket repository. Alternatively, follow along with this article and cut/paste the code into a Playground.

Operator Overloading

Swift permits the overloading of standard types of binary, ternary, and unary operators.  Unary operators function on a single operand (e.g., a++), binary operators work on two operands (e.g., a + b), and ternary operators perform with three operands (e.g. a ? b : c) .  

The operators that may be overridden include ++, --, /, =, -, + ,*, %, <, and more.  If that selection seems limiting, custom operators can be created from a subset of Unicode symbols.  

Overloading the + Operator

The class for the exercise is a simple representation of an animal shelter. The goal of overloading the + operator is to combine two distinct animal shelters' inventory of animals into one single unit.


class AnimalShelter{

    var animals:[String:Int]!
    
    init(_ a:[String:Int]){
        animals = a
    }
    
    init?(_ a:[String:Int]?){
        if a == nil{
            return nil
        }
        
        animals = a
    }
    
    init(){
        animals = [String:Int]()
    }
    
    
    static func +(leftShelter: AnimalShelter, rightShelter: AnimalShelter) -> AnimalShelter {
        
        var combinedShelters = [String:Int]()
        
        // If both shelters have animials that have been initialized
        if let leftAnimals = leftShelter.animals, let rightAnimals = rightShelter.animals{
            
            combinedShelters = [String:Int]()
            
            //Get the keys for both shelters and combine them
            let keysForBothShelters:Set<String> = Set<String>(Array(leftAnimals.keys)).union(Set<String>(Array(rightAnimals.keys)))
            
            //Add the animal counts
            keysForBothShelters.map({key in
                // If both sides have values for the key
                if let leftAnimalCnt = leftAnimals[key], let rightAnimalCnt = rightAnimals[key]{
                    combinedShelters[key] = leftAnimalCnt + rightAnimalCnt
                }else{ //add the value from the side that has the key
                    combinedShelters[key] =  leftAnimals[key] == nil ? rightAnimals[key] : leftAnimals[key]
                }
            })
        }
        
        return AnimalShelter.init(combinedShelters)
    }
}

// Test Code
var as1 = AnimalShelter.init(["Cats":5, "Dogs":10, "Birds":2, "Ferrets":3])
var as2 = AnimalShelter.init(["Cats":10, "Dogs":25, "Birds":5])
let combinedShelter = as1 + as2
if combinedShelter != nil{
    print(combinedShelter!.animals!)
    //Prints ["Dogs": 35, "Ferrets": 3, "Cats": 15, "Birds": 7]
}

Detailing the code for the overloaded + operator:

  • +: Operator name. Overloaded operators must be static functions where the name is the specific operator that's intended for overloading.
  • Addition Logic:
    • Ensure the left and right sides of the + operator have animals, otherwise return an empty shelter. This indicates to the end user of the class that one side was empty if the entire addition returns empty. Alternatively, return only the side that's not empty.
    • keysForBothShelters.map: for each key in the combined list of keys, if both sides have that key then add them otherwise take the value of whichever side has the unique animal.
  • Return value: The + operator returns a new instance of the combined AnimalShelters and does not modify either the left or right side instances.

Overloading the += Operator

The code previously written for the + operator can be re-used for the += operator. Work smart, not hard, and take advantage of the existing work by adding the following code to the playground:

static func +=(leftShelter: inout AnimalShelter, rightShelter: AnimalShelter){
        
       leftShelter = leftShelter + rightShelter    
}

At the bottom of the playground add:

var as3 = AnimalShelter.init(["Cats":19, "Dogs":33, "Birds":22, "Rabbits":16])

// below the other calculation
combinedShelter += as3
print(combinedShelter.animals!) 
// ["Rabbits": 16, "Ferrets": 3, "Cats": 34, "Birds": 29, "Dogs": 68]

There are now many more animals in the combined shelter when the code is run.

These examples thus far should indicate how overloading can simplify third-party use of classes. Using the + operator to add two instances is a natural and accepted use of the symbol. The bonus is that it is far easier to remember than a long method name.

Always overload standard operators and impose their functionality in a way that's familiar to the end user. Don't make the + operator perform subtraction even though overloading grants that power. Use the power wisely.

Custom Operators

It's also possible to create a custom operator by selecting from a large number of permitted unicode symbols. Options include ⭐,♈,⚔, ⚡, ✊, ❄, and so on. Many of these will not make sense to someone else reading the code, so make sure to proceed with common sense.

Next, create the code for a custom operator that swaps animals between the shelters.

The unicode symbol ⇄ visually represents a swap, so apply it to the AnimalShelter class as an infix operator. The operator will swap all the animals between shelters because, in this contrived example, that's been defined as a thing that is a common occurrence.

Define the infix, prefix, or postfix operator and place it at the file level, not inside a class:

infix operator ⇄

Next, add the following overloaded method to the AnimalShelter class.

static func ⇄(leftShelter: AnimalShelter, rightShelter: AnimalShelter) {
        
    let t_animals = leftShelter.animals
    leftShelter.animals = rightShelter.animals
    rightShelter.animals = t_animals
        
}

The code swaps the dictionary of animals between the two shelters in question using a temporary dictionary reference.

Finally, outside the class definition, write the following code to test the custom operator:

print(as1.animals!)  // [Bird: 2, Cat: 5, Dog: 10, Ferret: 3]
print(as2.animals!)  // [Bird: 5, Cat: 10, Dog: 25]
as1⇄as2
print(as1.animals!)  // [Bird: 5, Cat: 10, Dog: 25]
print(as2.animals!)  // [Bird: 2, Cat: 5, Dog: 10, Ferret: 3]

Run the playground and see the animals and counts swapped between the as1 and as2 shelters using the custom ⇄ operator.

Subscripts

Anyone who has taken a computer science class is going to be familiar with subscripting, as it relates to arrays. Can subscripting apply to a custom class?

Swift can help make that happen through the use of computed properties.Take a look at the signature of subscript below. The signature is that of a computed property:

subscript(index: Int) -> Int {
    get {
        // Return custom subscript value
    }
    set(newValue) {
        // Set a value
    }
}

The code does not reference square brackets. Instead, the override is on the subscript property itself.

When planning out the use of subscripting, do the following:

  1. Determine if the use of subscripts makes sense for the class. For example, subscripts do not make sense for a simple Dog class.
  2. Carefully design the way subscripts will access and set data from the class. For example, which data structures to use, what information is necessary, etc.
  3. Add the subscript property to the class using the signature above as a template.

Subscripting is particularly useful for classes that have data structures backing them. Using bracket notation needs to result in an easy to understand simplification or clarification of the usage of the class. Otherwise, it shouldn't be a consideration.

Some good candidate examples might be:

  • graphing calculator class that uses an array to store the set of functions for on-screen rendering
  • delivery scheduling system that uses a queue to assign deliveries to drivers.

Subscripting AnimalShelter

Next, apply subscripting to the AnimalShelter class. First, think of a feature where subscripting would be useful. What about an adoption queue? One that keeps animals held the longest at the front of the queue and newly adopted animals at the rear?

The syntax below could be used to select an exact animal in the queue:

var animalShelter = AnimalShelter.init(["Cats":5, "Dogs":10, "Birds":2, "Ferrets":3])

animalShelter[2] // returns the second animal in the adoption queue

Add the following new enum and classes to the playground:

enum Species:String,CustomStringConvertible{
    
    case Cat = "Cat"
    case Dog = "Dog"
    case Bird = "Bird"
    case Ferret = "Ferret"
    case Rabbit = "Rabbit"
    
    public var description:String{return "\(self.rawValue)"}
}

class Animal: CustomStringConvertible {
    
    var species:Species
    var name:String
    public var description:String{return "\(name):\(species)"}
    
    init(_ species:Species, name:String){
        self.species = species
        self.name = name
    }
    
    
}

The new classes will be used for the following purposes:

  • Species: enum type that distinguishes the kind of animal, in-place of using String values
  • Animal: class that encapsulates the Species along with the name of the animal up for adoption.

Now implement a FIFO AnimalQueue. Why FIFO? The animals that have been in the shelter the longest should be adopted first, and at the head of the queue:

class AnimalQueue: CustomStringConvertible {
    
    var queue:[Animal]
    public var description:String{return "\(queue.description)"}
    
    init(){
        queue = [Animal]()
    }
    
    
    func enqueue(animal:Animal){
        queue.append(animal)
    }
    
    func dequeue()->Animal{
        return queue.removeFirst()
    }
    
    func injectQueue(spot:Int, animal:Animal){
        queue.insert(animal, at: spot)
    }
    
    
    func removeAnimalAt(spot:Int)->Animal{
        return queue.remove(at: spot)
    }
    
    func animalAt(index:Int)->Animal?{
        if index >= 0 && index < queue.count{
            return queue[index]
        }
        
        return nil
    }
    
    func nextAdoption()->Animal?{
        return queue.removeFirst()
    }
    
    func size()->Int{
        return queue.count
    }
    

    func isEmpty()->Bool{
        return queue.isEmpty
    }
    
    
}

Breaking it down:

  • enqueue: adds an animal to the rear of the queue.
  • dequeue: removes from the front of the queue using removeFirst method call.
  • injectQueue(spot:animal) and removeAnimalAt(index:): remove and add animals at specific spots in the queue.

The AnimalQueue, Species, and Animal classes implement the CustomStringConvertible protocol. This means the description computed property can be picked up by print method calls. Each class is printable to output in a human-readable format.

Rewrite the AnimalShelter class to take advantage of the new Species and Animal and AnimalQueue classes and add the subscript method call:

class AnimalShelter{
    
    var animals:[Species:Int]!
    var adoptionQueue:AnimalQueue!
    
    init(_ a:[Species:Int]){
        animals = a
        adoptionQueue = AnimalQueue()
    }
    
    init?(_ a:[Species:Int]?){
        if a == nil{
            return nil
        }
        
        animals = a
        adoptionQueue = AnimalQueue()
    }
    
    init(){
        animals = [Species:Int]()
        adoptionQueue = AnimalQueue()
    }
    
    subscript(index: Int) -> Animal? {
        get {
            return adoptionQueue.animalAt(index: index)
        }
        set(animal) {
            // Remove the old animal from adoption queue and animals count
            let removedAnimal = adoptionQueue.removeAnimalAt(spot: index)
            modifyAnimals(species: removedAnimal.species, val: -1)
            
            // Add the new animal to animals count and adoption queue
            modifyAnimals(species: animal!.species, val: 1)
            adoptionQueue.injectQueue(spot: index, animal: animal!)
        }
    }
    
    
    func intakeNewAnimal(species:Species, name:String){
        
        let animal:Animal = Animal.init(species, name: name)
        adoptionQueue.enqueue(animal: animal)
        modifyAnimals(species: species, val: 1)

    }
    
    
    func adoptNextAnimal()->Animal{
        
        let nextAnimal = adoptionQueue.dequeue()
        modifyAnimals(species: nextAnimal.species, val: -1)
        
        return nextAnimal
    }
    
    private func modifyAnimals(species:Species, val:Int){
    
        if let _ = animals[species]{
            animals[species]! += val
        }else{
            animals[species] = 1
        }

    }
    
    
    static func +(leftShelter: AnimalShelter, rightShelter: AnimalShelter) -> AnimalShelter {
        
        var combinedShelters = [Species:Int]()
        
        // If both shelters have animials that have been initialized
        if let leftAnimals = leftShelter.animals, let rightAnimals = rightShelter.animals{
            
            combinedShelters = [Species:Int]()
            
            //Get the keys for both shelters and combine them
            let keysForBothShelters:Set<Species> = Set<Species>(Array(leftAnimals.keys)).union(Set<Species>(Array(rightAnimals.keys)))
            
            //Add the animal counts
            keysForBothShelters.map({key in
                // If both sides have values for the key
                if let leftAnimalCnt = leftAnimals[key], let rightAnimalCnt = rightAnimals[key]{
                    combinedShelters[key] = leftAnimalCnt + rightAnimalCnt
                }else{ //add the value from the side that has the key
                    combinedShelters[key] =  leftAnimals[key] == nil ? rightAnimals[key] : leftAnimals[key]
                }
            })
        }
        
        return AnimalShelter.init(combinedShelters)
    }
    

    static func +=(leftShelter: inout AnimalShelter, rightShelter: AnimalShelter){
        
       leftShelter = leftShelter + rightShelter
        
    }
    
    static func ⇄(leftShelter: AnimalShelter, rightShelter: AnimalShelter) {
        
        let t_animals = leftShelter.animals
        leftShelter.animals = rightShelter.animals
        rightShelter.animals = t_animals
        
    }
    
    
    
   
}

Animals have been replaced in the queue. It previously tracked the number of each species using a String key. It is now changed to a Species key from the enum. This change has a ripple effect throughout the entire class.

There are three new methods outside of the subscripting component. At a high-level:

  • intakeNewAnimal(species:name): adds a new instance of Animal.
  • adoptNextAnimal(): removes the first animal in the adoption queue for adoption.
  • modifyAnimals(species:val): adjusts the count of each animal based on adoption or intake.

Disassembling the subscripting portion of the code:

subscript(index: Int) -> Animal? {
        get {
            return adoptionQueue.animalAt(index: index)
        }
        set(animal) {
            // Remove the old animal from adoption queue and animals count
            let removedAnimal = adoptionQueue.removeAnimalAt(spot: index)
            modifyAnimals(species: removedAnimal.species, val: -1)
            
            // Add the new animal to animals count and adoption queue
            modifyAnimals(species: animal!.species, val: 1)
            adoptionQueue.injectQueue(spot: index, animal: animal!)
        }
    }

Breaking down the subscript code:

  • subscript- get: takes in the index from the subscript call (e.g., as4[0]), calls the adoptionQueue class method animalAt(index:) to grab the animal at the specified index.
  • animalAt(index:): AnimalQueue method that checks to ensure the index is within range and returns the animal found at that index in the array otherwise nil
  • subscript- set:
    • take the supplied index in a subscript call (e.g., as4[0] = Animal.init(.Dog, name:"Fido") ), removes the current animal in that spot and subtracts the animal from the count by calling modifyAnimals(species, val).
    • Recall that two data structures are in use to track the animals. One is the adoption queue and the other is the dictionary maintaining the counts of each species.
    • Changes in one data structure need reflection in the other immediately. Thus, modifyAnimals removes the existing and then adds the new animal and injectQueue(spot:index) to add the new animal to the adoption queue.

Remember the subscript is the interface to the class data structures. The getter and setter of the subscript is where custom logic is applied to the classes data structures.

Robust bounds checking is critical to ensure developers using the code don't crash it with an out-of-bounds call.

Add the following calls to the bottom of the Playground to test the new functionality:


// Create new animal shelter and add animals to the adoption queue
var as4 = AnimalShelter.init()
as4.intakeNewAnimal(species: .Dog, name: "Fido")
as4.intakeNewAnimal(species: .Dog, name: "Spike")
as4.intakeNewAnimal(species: .Cat, name: "Fluffy")
as4.intakeNewAnimal(species: .Dog, name: "Rex")
as4.intakeNewAnimal(species: .Cat, name: "Buttons")

print(as4.animals)  // [Cat: 2, Dog: 3]
print("\(as4[0]!.name):\(as4[0]!.species)") //Fido:Dog
print("\(as4[1]!.name):\(as4[1]!.species)") //Spike:Dog
print("\(as4[2]!.name):\(as4[2]!.species)") //Fluffy:Cat

print(as4.adoptionQueue!) //[Fido:Dog, Spike:Dog, Fluffy:Cat, Rex:Dog, Buttons:Cat]

as4[1] = Animal.init(.Ferret, name: "Althea") 
print(as4.animals) //[Cat: 2, Ferret: 1, Dog: 2]
print(as4.adoptionQueue!) //[Fido:Dog, Althea:Ferret, Fluffy:Cat, Rex:Dog, Buttons:Cat]

Notice the last two method calls. A Ferret replaces an existing Dog at index 1. That, in turn, updates the total animal count. Go ahead and print the adoption queue. The newly added ferret, Althea, is now nearly in the first spot for adoption.

Subscripts with Ranges

Can a slice of the animals in the queue for adoption be obtained like slicing a standard array? As a use-case, a developer may want to display a set of 10 animals at a time from the adoption queue to the end-user. Depending on how the end-user navigates the application, they may want to take different slices forward and back.

This can be done by adding subscript code that returns an ArraySlice. First, add the following code to AnimalQueue:

subscript(range:Range<Int>)->ArraySlice<Animal>?{
        
    if range.lowerBound >= 0 && range.upperBound < self.queue.count{
        return queue[range.startIndex..<range.endIndex]
    }

    return nil
}
    

Then, add the following to AnimalShelter

subscript(range:Range<Int>)->ArraySlice<Animal>?{
    return adoptionQueue![range.lowerBound..<range.upperBound]
}

Now add the following calls to the bottom of the Playground:

as4[0..<3] //[Fido:Dog, Althea:Ferret, Fluffy:Cat]
as4[0..<33] //nil, out of bounds

The method calls chain together. The AnimalShelter subscript(range:) method call is subscripting a range of AnimalQueue. That subscript(range:) method calls the system Array subscript(range:) call on the given ranges applied by the user to the class property queue array. Then the results are returning.

Wrapup

There is a lot more to explore with overloading and subscripting but the code and examples here are a quick start. For those hungry for more, here is a compilation of useful links on this topic: