Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

protocol typed array can't be downcast to concrete type array

protocol P : class {
    var value:Int {get}
}

class X : P {
    var value = 0

    init(_ value:Int) {
        self.value = value
    }
}

var ps:[P] = [X(1), X(2)]
for p in ps {
    if let x = p as? X {   // works for a single variable
        ...
    }
}

if let xs = ps as? [X] {   // doesn't work for an array (EXC_BAD_ACCESS)
    ...
}

If P is a class instead of a protocol, than the code works correctly. What's the difference between class and protocol? They're both implemented as pointers in the heap, aren't they? The above code can be compiled successfully, but crash at runtime. What does this EXC_BAD_ACCESS error mean?


Thanks to @Antonio, but I still don't understand how this sample code works.

let someObjects: [AnyObject] = [
    Movie(name: "2001: A Space Odyssey", director: "Stanley Kubrick"),
    Movie(name: "Moon", director: "Duncan Jones"),
    Movie(name: "Alien", director: "Ridley Scott")
]
for movie in someObjects as [Movie] {
    println("Movie: '\(movie.name)', dir. \(movie.director)")
}

Is AnyObject a special case?

reference: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/TypeCasting.html#//apple_ref/doc/uid/TP40014097-CH22-XID_498


protocol P {

}

@objc class X : P {

}

@objc class Y : X {

}

var xs:[X] = [Y(), Y()]
var ps:[P] = [Y(), Y()]


xs as? [Y]  // works
ps as? [Y]  // EXC_BAD_ACCESS

I tried this code in playground. Since this is pure swift code, I think it has nothing to do with @objc.

like image 379
Ken Zhang Avatar asked Aug 31 '14 01:08

Ken Zhang


1 Answers

Ignoring the optional binding for a moment and using a direct assignment:

let x = ps as [X]

the following runtime error is reported:

fatal error: array element cannot be bridged to Objective-C

That means the downcast from array of protocols to array of adopters requires obj-c binding. This can be easily solved by declaring the protocol as objc compatible:

@objc protocol P : class {
    var value:Int {get}
}

With that simple change, the code now works and no run time exception is raised.

Now the how is solved, but leaving the why an open issue. I don't have an answer yet, but I'll try to dig deeper on that.

Addendum: figure out the "why"

I spent some time investigating on this issue, and following is what I've come with.

We have a protocol and a class adopting it:

protocol P {}
class X : P {}

We create an array of P:

var array = [P]()

Converting the empty array to [X] works:

array as [X] // 0 elements

If we add an element to the array, a runtime error occurs:

array.append(X())
array as [X] // Execution was interrupted, reason: ...

The console output says that:

fatal error: array element cannot be bridged to Objective-C

So casting an array of protocol objects to an array of its adopter requires bridging. That justifies why @objc fixes the issue:

@objc protocol P {}
class X : P {}

var array = [P]()
array.append(X())
array as [X] // [X]

Sifting the documentation, I found out the reason for that to happen.

In order to perform the cast, the runtime has to check whether X conforms to the P protocol. The documentation clearly states that:

You can check for protocol conformance only if your protocol is marked with the @objc attribute

To verify that (not that I don't trust the documentation), I've used this code in the playground:

protocol P {}
class X : P {}

let x = X()
let y = x is P

but I get a different error, stating that:

Playground execution failed: <EXPR>:18:11: error: 'is' test is always true 
let y = x is P

Writing that in a "regular" project instead we get what expected:

protocol P {}
class X {}

func test() {
    let x = X()
    let y = x is P
}

Cannot downcast from 'X' to non-@objc protocol type 'P'

Conclusion: in order for a protocol typed array to be downcast to a concrete type array, the protocol must be marked with the @objc attribute. The reason is that the runtime uses the is operator to check for protocol conformance, which accordingly to the documentation is only available for bridged protocols.

like image 149
Antonio Avatar answered Nov 15 '22 14:11

Antonio