Ausgabe
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))
Wenn wir den obigen Code ausführen, können wir Folgendes erhalten:
Value: 2
instanceA is A? true
instanceA is kind of A? false
wir können sehen , instanceA is A
ist anders als instanceA.isKind(of: A.self)
, weißt du warum?
Lösung
Warum verhält sich “Type Check” bei Swift und Objective-C anders?
Denn das sind zwei verschiedene Sprachen.
Die Objective-C-Klassenanalyse erfolgt über Introspection und wird zur Laufzeit durchgeführt. [NSObject isKindOfClass:]
ist eine der Introspektionsmethoden, die auch zur Laufzeit ihre Aufgabe erfüllt, daher ist das Ergebnis dieser Operation unbekannt, bis die Ausführung Ihres Programms an den Punkt gelangt, an dem diese Methode aufgerufen wird.
Swift ist im Gegensatz zu Objective-C statisch typisiert und verleiht der Sprache den Luxus einer Typprüfung zur Kompilierzeit. Alle Typen in einem Swift-Programm sind (sollten) zur Kompilierzeit bekannt sein, sodass der Code sie zur Laufzeit nicht erneut überprüfen muss (es ist jedoch immer noch erforderlich, wenn es um das Erstellen von Unterklassen geht, aber das ist irrelevant zu dem von Ihnen bereitgestellten Szenario).
Für Ihr spezifisches Beispiel würde ich sagen, dass es ein unglücklicher Nebeneffekt der Swift- und Objective-C-Interoperabilität ist. Beim Kompilieren eines Projekts mit gemischtem Swift- und Objective-C-Code wird weder Objective-C- noch Swift-Code tatsächlich in eine andere Sprache konvertiert. Beide Welten folgen weiterhin ihren eigenen Regeln und der Compiler generiert nur eine Schnittstelle, über die sie kommunizieren können. Also beim Aufruf dieser Funktion:
Main().perform(NSSelectorFromString("printValue:"), with: B(value: 2))
Sie delegieren die Ausführung tatsächlich an die Objective-C-Welt, wo die Laufzeit blind eine "printValue:"
Nachricht mit einem Zeiger auf ein Objective-C-Objekt sendet . Objective-C kann das auch ohne mit performSelector:
einer Methodenfamilie herumzuspielen:
#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;
}
Darüber hinaus ist es, obwohl Typen von ivars in den Klassen nicht übereinstimmen, und TDWB
da ivar nicht öffentlich ist, immer noch über die TDWA
Schnittstelle zugänglich. Wenn Sie das Speicherlayout einer Klasse kennen, können Sie dank C-Legacy auf die Werte ihrer Ivars zugreifen, unabhängig davon, ob sie privat sind oder nicht. Mit Swift wäre das niemals möglich, da Sie kein Argument an eine Methode übergeben können, die einen anderen Parametertyp erwartet:
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: %10
return %9 : $() // id: %10
} // 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
}
}
Hier ist, was der Swift-Compiler jetzt für den SIL-Code der Funktion ausgibt:
// Main.printValue(_:)
sil hidden @$s4main4MainC10printValueyyAA1ACF : $@convention(method) (@guaranteed A, @guaranteed Main) -> () {
// %0 "instanceA" // users: %10, %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: %10
%11 = integer_literal $Builtin.Int1, 0 // user: %12
br bb3(%11 : $Builtin.Int1) // id: %12
// %13 // user: %14
bb3(%13 : $Builtin.Int1): // Preds: bb2 bb1
%14 = struct $Bool (%13 : $Builtin.Int1) // user: %15
debug_value %14 : $Bool, let, name "fooBar" // id: %15
%16 = tuple () // user: %17
return %16 : $() // id: %17
} // end sil function '$s4main4MainC10printValueyyAA1ACF'
Wie Sie sehen können, bb0
wird das jetzt tatsächlich mit einer bedingten Typprüfung beendet checked_cast_br
. Und es geht entweder zu bb1
oder bb2
blockiert abhängig von den Ergebnissen.
Beantwortet von – The Dreams Wind
Antwort geprüft von – David Goodson (FixError Volunteer)