8

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)

2 Answers 2

8

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).

Sign up to request clarification or add additional context in comments.

1 Comment

The for loop is not the performance problem. The problem is not allocating enough capacity beforehand.
7
  1. Am I missing an easier way to do this?

You can use map to make the conversion:

let cats: [Cat] = animals.map { $0 as! Cat }
  1. how can I pass a type as the target parameter in the function below, rather than passing an instance?

First, you need to remove the instance parameter:

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

Since you cannot specify type parameters explicitly, you need to provide the compiler with some info to deduce the type of U. In this case, all you need to do is to say that you are assigning the result to an array of Cat:

let cats: [Cat] = convertArray(animals)

3 Comments

Sweet thanks, I'll try removing U and accept this if it works for me. I'd thought about map but in practice I'll throw if the downcast fails
@Philip As far as throwing on a cast goes, you can do the same thing in a map. Operator as! with exclamation point will throw on unsuccessful cast, too.
Oh I didn't realize map could take a closure that throws. And yeah, leaving it up to as! to throw could work but I'd like to have a custom message go with it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.