Search code examples
swiftclasstypessyntaxprotocols

Creating a loop for protocol-type array


I have this code:

protocol Animal {
    var type: String {get}
}

struct Dog: Animal {
    var name: String
    var type: String
    
    func bark() {
        print("Woof!")
    }
}

struct Cat: Animal {
    var name: String
    var type: String
       
    func meow() {
        print("Meow!")
    }
}

var bunty = Cat(name: "Bunty", type: "British Shorthair")
var nigel = Cat(name: "Nigel", type: "Russian Blue")
var percy = Cat(name: "Percy", type: "Manx")
var argos = Dog(name: "Argos", type: "Whippet")
var apollo = Dog(name: "Apollo", type: "Lowchen")

var animals: [Animal] = [bunty, nigel, percy, argos, apollo]

And the book gave me this task:

«Make a loop that iterates through the animals in our Animal array, and see if you can figure out how to call either bark or meow, depending on whether the item in the array is a Dog or a Cat.»

I've come up with such solution:

for animal in animals {
    if let dog = animal as? Dog {
        dog.bark()
    } else if let cat = animal as? Cat {
        cat.meow()
    } else {
        print("Unknown animal type")
    }
}

But I'm wondering is there any way to complete the task simpler, without as? operator?


Solution

  • There are several solutions to your problem, which might not necessarily be better or simpler. You'll find out that throughout your coding career, there is more than one way to bake a cake.

    To start with, you could improve your current for loop by implementing a switch statement:

    for animal in animals {
        switch animal {
        case let dog as Dog:
            dog.bark()
        case let cat as Cat:
            cat.meow()
        default:
            print("Unknown animal type")
        }
    }
    

    You could also make multiple loops and use yet another syntactic trick by using the for case let syntax:

    for case let dog as Dog in animals {
        dog.bark()
    }
    
    for case let cat as Cat in animals {
        cat.meow()
    }
    

    Then, since you are using a protocol approach, you could solve this by implementing a new protocol Soundable. I've only implemented this on the Dog, but you should be able to add this to any animal that can make a sound:

    /// This could also be defined as 
    /// protocol Soundable: Animal
    /// which would cause Soundable to also always be an animal
    protocol Soundable {
        func makeSound()
    }
    
    struct Dog: Animal, Soundable {
        var name: String
        var type: String
    
        func makeSound() {
            print("Woof!")
        }
    }
    
    for animal in animals {
        (animal as? Soundable)?.makeSound()
    }
    // Or again with the for case let
    for case let soundable as Soundable in animals {
        soundable.makeSound()
    }
    

    And then, lastly for this answer, subclassing. I feel like Animal also could be a class:

    class Animal {
        var type: String = ""
    
        func makeSound() {}
    }
    
    class Dog: Animal {
        var name: String
    
        init(name: String, type: String) {
            self.name = name
            self.type = type
        }
    
        override func makeSound() {
            print("Woof!")
        }
    }
    
    class Cat: Animal {
        var name: String
    
        init(name: String, type: String) {
            self.name = name
            self.type = type
        }
    
        override func makeSound() {
            print("Meow!")
        }
    }
    
    for animal in animals {
        animal.makeSound()
    }
    

    Notable extra's

    One can also use a where clause at the for loop, but this is only a boolean guard and thus does not cast the object to specified type:

    for animal in animals where animal is Dog {
        // animal still is `Animal` here
        // but one should be able to safely cast to `Dog`
    }
    

    There's also the compactMap function, that will remove nil values from an array after performing the closure provided. This way, one could transform the previously mentioned for case let loop as well:

    animals.compactMap { $0 as? Dog }.forEach { dog in
        dog.bark()
    }
    animals.compactMap { $0 as? Cat }.forEach { cat in
        cat.meow()
    }
    

    Then there's also the option of simply casting all at once, without any if or switch statement:

    for animal in animals {
        (animal as? Dog)?.bark()
        (animal as? Cat)?.meow()
    }
    

    As you can see there are plenty of ways one could solve this issue. You could even mix and match solutions I provided above. See what fits you best. I really like the switch variant, but that only works well, when the set of animals remains small and therefor maintainable.