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?
CodePudding user response:
Why "type check" behaves differently on Swift and Objective-C?
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 languages compatibility between Swift and Objective-C. 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:
Main().perform(NSSelectorFromString("printValue:"), with: B(value: 2))
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 with performSelector:
family of methods:
#pragma mark -
@interface TDWA : NSObject {
@public
int _value;
}
- (instancetype)initWithValue: (int)value;
@end
@implementation TDWA
- (instancetype)initWithValue:(int)value {
self = [super init];
if (self) {
_value = value;
}
return self;
}
@end
#pragma mark -
@interface TDWB : NSObject {
long _another_value;
const char *_c_string;
}
- (instancetype)initWithValue: (int)value;
@end
@implementation TDWB
- (instancetype)initWithValue:(int)value {
self = [super init];
if (self) {
_another_value = value;
_c_string = "Another imp";
}
return self;
}
@end
#pragma mark -
@interface TDWMain : NSObject
(void)printValue: (TDWA *)instance;
@end
@implementation TDWMain
(void)printValue:(TDWA *)instance {
NSLog(@"Value: %d", instance->_value);
NSLog(@"Is A? %s", [instance isKindOfClass:[TDWA class]] ? "Yes" : "No");
}
@end
int main(int argc, const char * argv[]) {
TDWB *instance = [[TDWB alloc] initWithValue:20];
[TDWMain printValue: instance];
/* Prints:
* Value: 20
* Is A? No
*/
return 0;
}
Moreover, despite types of ivars in the classes don't match, and for TDWB
the ivar is not public, it's still accessible through TDWA
interface. I call it the C-legacy, where if you know the template of something, you can either mimic it or deduce what is inside of it. With Swift the same would never be possible, because you cannot pass an argument to a method which expects different type of parameter:
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 class func printValue(_ instanceA: A) {
print("Value: \(instanceA.value)")
print("instanceA is A? \(instanceA is A)")
}
}
let instance = B(value: 20)
Main.printValue(instance) // Compile-time error - Cannot convert value of type 'B' to expected argument type 'A'
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 A
is A
doesn't make much sense. Swift does not allow to pass an incompatible value as argument, so i think that is
operator does nothing here and the compiler just optimises it to a true
expression at compile time. However if you try to perform this check against a subclass of A
, it would fail, because Swift would actually have to go through the class hierarchy:
class C: A {}
class Main: NSObject {
@objc class func printValue(_ instanceA: A) {
// prints true only if instanceA is of type C
print("instanceA is A? \(instanceA is C)")
}
}
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):
@objc func printValue(_ instanceA: A) {
let fooBar = instanceA is A
}
This results in the following SIL code for the function:
// Main.printValue(_:)
sil hidden @$s4main4MainC10printValueyyAA1ACF : $@convention(method) (@guaranteed A, @guaranteed Main) -> () {
// %0 "instanceA" // user: %2
// %1 "self" // user: %3
bb0(%0 : $A, %1 : $Main):
debug_value %0 : $A, let, name "instanceA", argno 1 // id: %2
debug_value %1 : $Main, let, name "self", argno 2, implicit // id: %3
br bb1 // id: %4
bb1: // Preds: bb0
%5 = integer_literal $Builtin.Int1, -1 // user: %7
br bb2 // id: %6
bb2: // Preds: bb1
%7 = struct $Bool (%5 : $Builtin.Int1) // user: %8
debug_value %7 : $Bool, let, name "fooBar" // id: %8
%9 = tuple () // user:
return %9 : $() // id:
} // end sil function '$s4main4MainC10printValueyyAA1ACF'
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 block bb1
with 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:
class C: A {}
class Main: NSObject {
@objc func printValue(_ instanceA: A) {
let fooBar = instanceA is C
}
}
Here is what the Swift compiler now emits for the SIL code of the function:
// Main.printValue(_:)
sil hidden @$s4main4MainC10printValueyyAA1ACF : $@convention(method) (@guaranteed A, @guaranteed Main) -> () {
// %0 "instanceA" // users: , %5, %4, %2
// %1 "self" // user: %3
bb0(%0 : $A, %1 : $Main):
debug_value %0 : $A, let, name "instanceA", argno 1 // id: %2
debug_value %1 : $Main, let, name "self", argno 2, implicit // id: %3
strong_retain %0 : $A // id: %4
checked_cast_br %0 : $A to C, bb1, bb2 // id: %5
// %6 // user: %8
bb1(%6 : $C): // Preds: bb0
%7 = integer_literal $Builtin.Int1, -1 // user: %9
strong_release %6 : $C // id: %8
br bb3(%7 : $Builtin.Int1) // id: %9
bb2: // Preds: bb0
strong_release %0 : $A // id:
= integer_literal $Builtin.Int1, 0 // user:
br bb3( : $Builtin.Int1) // id:
// // user:
bb3( : $Builtin.Int1): // Preds: bb2 bb1
= struct $Bool ( : $Builtin.Int1) // user:
debug_value : $Bool, let, name "fooBar" // id:
= tuple () // user:
return : $() // id:
} // end sil function '$s4main4MainC10printValueyyAA1ACF'
As you can see bb0
is now actually terminated with a conditional type checking checked_cast_br
. And it either goes to bb1
or bb2
block depending on the results.