Swift: AnyObject as? Array casting fail if the array is empty (not nil)

This is the case: Suppose that I get a variable as an AnyObject or Any. Then, I must to cast the variable to know if it's an array with objects of a specific type.

func myFuction(receivedObject: AnyObject) {
    if let validDogs = receivedObject as? [Dog] {

        print("Received object is an array of dogs")
        // Do something with valid dogs
    }

    if let validCats = receivedObject as? [Cat] {

        print("Received object is an array of cats")
        // Do something with valid cats
    }
}

This code works if the received object is not an empty array (not nil), but fails if the received object is an empty array because my log prints this two messages:

"Received object is an array of dogs"
"Received object is an array of cats"

Which suggest that for an empty array the cast fails. So, is there a way to fix that?

Upvotes: 0

Views: 329

Answers (2)

Rob Napier
Rob Napier

Reputation: 299605

Code like this strongly suggests a deep problem in your type design, and should be resolved by getting rid of the AnyObject. It is very rare that passing AnyObject is the correct tool.

You've said something very important here:

This code works if the received object is not an empty array (not nil)

An empty array is not the same thing as nil. If you pass nil, that's an Optional. Optional<[Cat]> is not the same thing as [Cat] and you should not expect it to consistently as? cast, particularly if it's nil. If this came from ObjC, that nil is bridged to an actual Obj-C nil (which is just the value 0), and the runtime has literally nothing to work with.

You said that you receive both log lines, though. That suggests both as? casts are succeeding, not failing. If that's the case, I assume this is an NSArray, and an empty NSArray can legitimately be as? cast to an array of anything (NSArray has no element type internally). So the above is expected.

If it is really is optional, then to the question of "how do I determine that it's an optional and then unwrap it and then work out if it's [Cat]," the answer is "stop; you've gone too far with AnyObject." Start by redesigning this so you don't need that, which generally means figuring out your types earlier.

The correct way to do what you're trying to do is generally with overloads, not as? casting:

func myFuction(receivedObject: [Dog]) {
    print("Received object is an array of dogs")
    // Do something with valid dogs
}

func myFuction(receivedObject: [Cat]) {
    print("Received object is an array of cats")
    // Do something with valid cats
}

You cannot just pass AnyObject to the above functions. You need to know the types of the things you're working with. (Your comment that you're stripping the types with as! AnyObject suggests that you do know the types already, and are actively throwing them away and trying to later recover them. If this is the case, the above code is exactly the right thing. Don't throw away the types.)

While there are some corner cases where you cannot know those types (because you literally are accepting "any object at all"), in the vast majority of cases failure to know your types suggests a design problem.

Upvotes: 4

Grimxn
Grimxn

Reputation: 22507

@RobNapier's answer is correct from a good programming point of view - I see no reason whatever to be doing what you are attempting, but it shows up something interesting.

Firstly, you cannot call your function with an array of anything - arrays, as structs, do not conform to AnyObject (classes do). However, you call the function by a forced cast - as anything can be cast to AnyObject (presumably by wrapping the struct in a degenerate class), the call compiles.

Secondly, your question has the inference the wrong way round - you say "Which suggest that for an empty array the cast fails." - not so, the cast succeeds in both cases...

Let's see what actually gets passed in:

class Pet { var name: String { return "" } }
class Dog: Pet { override var name: String { return "Fido" } }
class Cat: Pet { override var name: String { return "Cfor" } }

func myFuction(receivedObject: AnyObject) {
    print("myFuction called with \(receivedObject)") // ***
    if let validDogs = receivedObject as? [Dog] {

        print("Received object is an array of dogs")
        // Do something with valid dogs
    }

    if let validCats = receivedObject as? [Cat] {

        print("Received object is an array of cats")
        // Do something with valid cats
    }
}

var a: [Cat] = [Cat()]
//myFuction(receivedObject: a) // Argument type '[Cat]' does not conform to expected type 'AnyObject'
myFuction(receivedObject: a as! AnyObject) // Forced cast from '[Cat]' to 'AnyObject' always succeeds; did you mean to use 'as'?
a = []
myFuction(receivedObject: a as! AnyObject) // Forced cast from '[Cat]' to 'AnyObject' always succeeds; did you mean to use 'as'?

Output:

myFuction called with (
    "__lldb_expr_97.Cat"
)
Received object is an array of cats
myFuction called with (
)
Received object is an array of dogs
Received object is an array of cats

So, when it's called with a non-empty array, the type is inferred from the members and the cast only succeeds if the members are of the right type. An empty array has no members, and thus could equally well be [Cat] or [Dog] or indeed [String].

Try

let b = [Cat(), Dog()]
myFuction(receivedObject: b as! AnyObject)

This prints

myFuction called with (
    "__lldb_expr_107.Cat",
    "__lldb_expr_107.Dog"
)

(and does not print a cast "success")

The long and the short of it is

A) the force as! AnyObject effectively throws away the array type and passes something resembling a tuple of elements which your let _ = as? is able to reassemble into an array from the types of the elements.

B) re-read @RobNapier's answer - that's the way to go!

Upvotes: 3

Related Questions