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?
CodePudding user response:
It gets even weirder
oh hey, would you look at that
import Foundation
class WrapperOfInt16: NSObject {
let value = 123
}
class TwoBytes: NSObject {
let byte1: UInt8 = 0xAB
let byte2: UInt8 = 0xCD
}
class Main: NSObject {
@objc func printValue(_ o: WrapperOfInt16) {
print("Value: \(String(o.value, radix: 16))") // => Value: cdab
}
}
Main().perform(NSSelectorFromString("printValue:"), with: TwoBytes())
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 o
is always* (unless you subvert it with runtime tricks) of type ObjectA
, so you'll always be invoking the same implementation of the value
method (property). Rather than repeatedly wasting time looking up the correct implementation at runtime (dynamic dispatching), the compiler will emit a direct call to implementation of value
for ObjectA
(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.value
is to just read out the contents of the object at a particular offset, and to interpret that sequence of bits as an Int
.
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:
// You could also have used `performSelector` print("Value: \(o.value(forKey: "value") as? Int)")
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:import Foundation class ObjectA: NSObject { @objc let value: Int @objc dynamic let x: Int = 123 init(value: Int) { self.value = value } } class ObjectB: NSObject { @objc let value: Int init(value: Int) { self.value = value } } class ObjectC: NSObject { let date = Date() @objc dynamic let value: Int init(value: Int) { self.value = value } } class Main: NSObject { @objc dynamic func printValue(_ o: ObjectA) { print("Value: \(o.value)") } } Main().perform(NSSelectorFromString("printValue:"), with: ObjectB(value: 2))
Both of these have the benefit of catching cases where the object doesn't have a value
at all:
class HasNoValue: NSObject {}
Main().perform(NSSelectorFromString("printValue:"), with: HasNoValue())
Terminating app due to uncaught exception
NSInvalidArgumentException
, reason: '-[main.HasNoValue value]
: unrecognized selector sent to instance0x600003edc070
'