I created this Swift class:
@objc public class Tester: NSObject {
private var name: String
private var user: Users
init(string:String, user: Users) {
print(user.empId)
print(user.name)
self.user = user
self.name = string
super.init()
}
}
I call the initializer from Obj C like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
NSString * nilString = nil;
Users * nilUser = nil;
Tester * test = [[Tester alloc] initWithString:nilString user:nilUser];
return YES;
}
Here I pass nil
for the parameters to the Swift initializer. Ideally I expect this to crash as the initializer only accepts non-nil
values.
But what actually happens is, when the execution point reaches inside the initializer, new parameter objects are created:
The nil
string becomes ""
and the User
variable that was nil
is now pointing to an object.
But for a property like this
@property(nonatomic,strong) SomeClass * object;
where
object = nil;
When I call this object from swift,
let x = object.someGetter;
This crashes.
At one point, If I pass nil to some non null, it works and at another point, it crashes. Why does this weird behavior exist? If for some reasons, my parameters are nil, and passed to nonnull, I would like this to crash. So that I can fix things.
EDIT: This became so unexpected, further trying to play with this code.
The string parameter was actually as string, but the User shows uninitialized, hence manipulations on string worked well, but the user object, took the changes but did not showed them back.
So there are three questions:
nil
one,I'll answer all of them :-)
Swift uses Objective C messaging when accessing the properties of the Users
class, (I assume - Users
is an ObjC-Class as seen in the debugger output; base class NSObject
).
In the disassembly view, one can see this:
0x1000018be <+78>: movq 0x3e6e2b(%rip), %rsi ; "empId"
....
0x1000018d7 <+103>: callq 0x100361b10 ; symbol stub for: objc_msgSend
Since objc_msgSend
supports nil messaging, the call does not fail.
When calling the Swift initializer from Objective C, the bridging code creates the following:
0x100001f45 <+53>: callq 0x100021f50
; static (extension in Foundation):
;Swift.String._unconditionallyBridgeFromObjectiveC (Swift.Optional<__ObjC.NSString>) -> Swift.String
....
0x100001f5b <+75>: callq 0x100001870
; TestCalling.Tester.init (string : Swift.String, user : __ObjC.ObjCUser) -> TestCalling.Tester at SwiftClass.swift:14
The interesting part here is the _unconditionallyBridgeFromObjectiveC
call. This will internally call the Swift.String function _cocoaStringToSwiftString_NonASCII
, and checking the source code (here, line 48), you can see the following:
@inline(never) @_semantics("stdlib_binary_only") // Hide the CF dependency
func _cocoaStringToSwiftString_NonASCII(
_ source: _CocoaString
) -> String {
let cfImmutableValue = _stdlib_binary_CFStringCreateCopy(source)
let length = _stdlib_binary_CFStringGetLength(cfImmutableValue)
let start = _stdlib_binary_CFStringGetCharactersPtr(cfImmutableValue)
return String(_StringCore(
baseAddress: start,
count: length,
elementShift: 1,
hasCocoaBuffer: true,
owner: unsafeBitCast(cfImmutableValue, to: Optional<AnyObject>.self)))
}
The function always returns a new Swift.String
object, in our case an empty one! So, again no crash.
When accessing a custom property, e.g. assigning it to a variable:
let x:SomeClass = object.someGetter;
the following happens:
The return value of someGetter
will be retained (objc_retainAutoreleasedReturnValue
) -- this does not crash
The returned object will be implicitly unwrapped -- which then crashes
If x
were a weak property, or an optional, the code wouldn't crash. Even when using type inference, it does not crash (on my machine, swift 4):
let x = object.someGetter;
This is, because the inferred type of x
is an optional SomeClass?
unless the property itself is declared as nonnull
:
@property(nonatomic,strong, nonnull) SomeClass * object;
When you say the string is ""
and the user is an object, it looks like you're just relying on the debugger's variable view for this, correct? Because the console output clearly shows nil
twice.
What's happening here is the generated code for the initializer doesn't actually do anything that would crash if the values are nil
. Semantically speaking, yes, it's invalid, but the actual machine code isn't doing something that relies on the non-nil
assumption. The calls to print()
work because, I assume, the code that converts the arguments into Any
existentials happens to work even though the values are unexpectedly nil
, and produce a nil
Any
.
As for the debugger view, it's a debugger, it's just doing the best it can to interpret the variables, but it's not actually running the Swift standard library. I'm not entirely sure why the string
variable is showing up as ""
, I guess because it's always showing a string representation and it determines that it has no data (and a string with no data is ""
). As for user
, it actually shows it as 0x0000000000000000
which is correct, that means it's the null pointer (i.e. nil
).
And then finally, let x = object.someGetter
does crash, because the resulting code depends on the value being non-nil
(specifically, it's presumably calling swift_retain()
or swift_unknownRetain()
on the value, which requires the argument to be non-nil
as it dereferences the pointer).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With