I'm testing perform(_:with:) with the following code
class ObjectA: NSObject {
let value: Int
init(value: Int) {
self.value = value
}
}
class ObjectB: NSObject {
let value: Int
init(value: Int) {
self.value = value
}
}
class ObjectC: NSObject {
let date = Date()
let value: Int
init(value: Int) {
self.value = value
}
}
class Main: NSObject {
@objc func printValue(_ o: ObjectA) {
print("Value: \(o.value)")
}
}
Main().perform(NSSelectorFromString("printValue:"), with: ObjectB(value: 2))
It works if I pass ObjectB instead of ObjectA.
But it doesn't work if I pass ObjectC instead of ObjectA.
we can say ObjectB is compatible with ObjectA, but ObjectC is not compatible with ObjectA.
After more testing with the following classes
class ObjectA: NSObject {
let value: Int
init(value: Int) {
self.value = value
}
}
class ObjectC: NSObject {
let date = Date()
let value: Int
init(value: Int) {
self.value = value
}
}
class ObjectD: NSObject {
let value: Int
let date = Date()
init(value: Int) {
self.value = value
}
}
class ObjectE: NSObject {
let date = Date()
let value: Int
init(value: Int) {
self.value = value
}
}
class ObjectF: NSObject {
let date = NSDate()
let value: Int
init(value: Int) {
self.value = value
}
}
class ObjectG: NSObject {
let date = NSDate()
let value: Int
init(value: Int) {
self.value = value
}
}
I found,
ObjectD is compatible with ObjectA.
ObjectC is not compatible with ObjectE
ObjectF is compatible with ObjectG
Anyone knows how the arguments are passed?
It gets even weirder
oh hey, would you look at that
Why does this happen?
You've usurped the type-system.
The optimizer assumes the types of your program to be a true reflection of what will happen at runtime. It makes its optimizations given that assumption to improve performance, without any perceptible difference in behaviour.
The issue is that types are unsound, the assumption is wrong, and the optimizations are invalid as a result.
Swift's philosophy
In Swift, the strong type system can be used to prove that
ois always* (unless you subvert it with runtime tricks) of typeObjectA, so you'll always be invoking the same implementation of thevaluemethod (property). Rather than repeatedly wasting time looking up the correct implementation at runtime (dynamic dispatching), the compiler will emit a direct call to implementation ofvalueforObjectA(static dispatch).This is much faster, and it's correct for
ObjectA, but will be incorrect if you manage to smuggle in a different type.The implementation of
ObjectA.valueis to just read out the contents of the object at a particular offset, and to interpret that sequence of bits as anInt.Objective-C
Objective-C's philosophy is much more dynamic. Many of its dynamic aspects (e.g. method swizzling, KVO, Cocoa bindings, NSProxy, etc.) involve arbitrarily replacing/modifying methods at runtime.
You can think of static dispatch as a kind of caching. It's fast, but if the underlying value changes, the static dispatch will be incorrect (it'll keep calling the old thing).
Thus, most Objective-C code is written with the understanding that objects/classes could have been modified at any time (e.g. KVO will add methods that detect changes to properties and notify observers), so dynamic dispatched is used pervasively (heck, it couldn't even do automatic static dispatch if it wanted, until recently). This is done even at the expense of performance (though there are tricks to workaround this when necessary).
The fix
To "fix" your problem, you should fix the types in your program.
...but if you want the dynamism, then you have two options.
You can manually do some dynamic dispatch:
Mark the field as
dynamic, which will make the compiler always emit dynamic dispatch calls for its lookups, even when static type information suggests only one implementation should exist:Both of these have the benefit of catching cases where the object doesn't have a
valueat all: