class A: NSObject {
let value: Int
init(value: Int) {
self.value = value
}
}
class B: NSObject {
let value: Int
init(value: Int) {
self.value = value
}
}
class Main: NSObject {
@objc func printValue(_ instanceA: A) {
print("Value: \(instanceA.value)")
print("instanceA is A? \(instanceA is A)")
print("instanceA is kind of A? \(instanceA.isKind(of: A.self))")
}
}
Main().perform(NSSelectorFromString("printValue:"), with: B(value: 2))
If we run the above code, we can get this:
Value: 2
instanceA is A? true
instanceA is kind of A? false
we can see instanceA is A is different from instanceA.isKind(of: A.self), do you know why?
Because these are two different languages.
Objective-C class analysis is done via introspection, and performed at runtime.
[NSObject isKindOfClass:]is one of the introspection methods which also does its job at runtime, thus the result of this operation is unknown until execution of your program gets to the point of where this method is called.Swift, unlike Objective-C is statically typed and it gives the language the luxury of compile-time type check. All types in a Swift program are (supposed to be) known at compile time, so the code doesn't need to double-check them again when it comes to runtime (it's still required when it comes to subclassing, however, but that's irrelevant to the scenario you provided).
For your specific example, I would say it's unfortunate side effect of the Swift and Objective-C interoperability. When compiling a project with mixed Swift and Objective-C code neither Objective-C nor Swift code is actually converted to another language. Both worlds keep following their own rules and compiler just generates interface for them to communicate. Thus when calling this function:
You actually delegate the execution to the Objective-C world, where the runtime blindly sends
"printValue:"message with a pointer to some Objective-C object. Objective-C can do that even without playing around withperformSelector:family of methods:Moreover, despite types of ivars in the classes don't match, and for
TDWBthe ivar is not public, it's still accessible throughTDWAinterface. Thanks to C-legacy, if you know the memory layout of a class, you can access values of its Ivars no matter whether they are private or not. With Swift the same would never be possible, because you cannot pass an argument to a method which expects different type of parameter:Since you delegate delivering of this message to Objective-C's
-[NSObject performSelector:withObject:]it's not a problem there and the message is successfully delivered. Thanks to runtime introspection,[NSObject isKindOfClass:]is also able to properly check the class.For Swift, however, checking that parameter of type
AisAdoesn't make much sense. Swift does not allow to pass an incompatible value as argument, so i think thatisoperator does nothing here and the compiler just optimises it to atrueexpression at compile time. However if you try to perform this check against a subclass ofA, it would fail, because Swift would actually have to go through the class hierarchy:Bonus Analysis
I actually decided to look at SIL listing for the given case to confirm my assumption and here are the results:
Scenario 1. "Apparent" type check
I removed all irrelevant parts from the printValue function and left only type check operator with a variable to read it (to avoid this part be discarded altogether):
This results in the following SIL code for the function:
There is quite a lot of stuff happening here, but we are most interested in the first basic block
bb0. As you can see it transfers control to the next blockbb1with an unconditional terminator called. So the type check doesn't happen here at all.Scenario 2. Real type check
Now let's look what happens when Swift actually needs to check the type:
Here is what the Swift compiler now emits for the SIL code of the function:
As you can see
bb0is now actually terminated with a conditional type checkingchecked_cast_br. And it either goes tobb1orbb2block depending on the results.