'Swift: Cast array of objects to array of sub type

Say I have an array of Animals and I'd like to cast it to an array of Cats. Here, Animal is a protocol that Cat adopts. I'd like something like let cats: [Cat] = animals as! [Cat] but this seg faults in compilation (btw I'm on both Linux Swift 3 and Mac Swift 2.2). My workaround is to just create a function that downcasts each item individually and adds it to a new array (see small example below). It produces the desired result, but isn't as clean as I'd like.

My questions are:

  1. is this totally dumb and I'm just missing an easier way to do this?

  2. how can I pass a type as the target parameter in the function below, rather than passing an instance? (e.g. I'd like to pass Cat.self rather than Cat(id:0) but doing so causes an error saying cannot convert Cat.Type to expected argument type Cat)

Here's what I have so far:

protocol Animal: CustomStringConvertible 
{
    var species: String {get set}
    var id: Int {get set}
}

extension Animal
{
    var description: String 
    {
        return "\(self.species):\(self.id)"
    }
}

class Cat: Animal 
{
    var species = "felis catus"
    var id: Int

    init(id: Int)
    {
        self.id = id
    }
}

func convertArray<T, U>(_ array: [T], _ target: U) -> [U]
{
    var newArray = [U]()
    for element in array
    {
        guard let newElement = element as? U else
        {
            print("downcast failed!")
            return []
        }

        newArray.append(newElement)
    }

    return newArray
}

let animals: [Animal] = [Cat(id:1),Cat(id:2),Cat(id:3)]
print(animals)
print(animals.dynamicType)

// ERROR: cannot convert value of type '[Animal]' to specified type '[Cat]'
// let cats: [Cat] = animals

// ERROR: seg fault
// let cats: [Cat] = animals as! [Cat]

let cats: [Cat] = convertArray(animals, Cat(id:0))
print(cats)
print(cats.dynamicType)


Solution 1:[1]

As of Swift 4.1 using compactMap would be the preferred way, assuming you don't want the method to completely fail (and actually crash) when you have any other Animal (for example a Dog) in your array.

let animals: [Animal] = [Cat(id:1),Dog(id:2),Cat(id:3)]
let cats: [Cat] = animals.compactMap { $0 as? Cat }

Because compactMap will purge any nil values, you will end up with an array like so:

[Cat(1), Cat(3)]

As a bonus, you will also get some performance improvement as compared to using a for loop with append (since the memory space is not preallocated; with map it automatically is).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1