Uninitialised nonnull properties of Foundation types are set to sensible defaults in Swift

Hello everyone,

I was writing a unit test in Swift for a class written in Objective-C when I came across the following behaviour: nonnull properties of some Foundation types (e.g. NSString or NSArray) declared in a Objective-C class are automatically set to a default value (e.g. an empty string or an empty array) when they're bridged to Swift.

Here's a code sample demonstrating this behaviour. Given an Objective-C class:

@interface Team : NSObject

@property (nonatomic, copy, nonnull) NSString *name;

@property (nonatomic, copy, nonnull) NSArray<NSString *> *codes;

@end


@implementation Team

// Note there is no explicit initialisation here

@end

and a unit test written in Swift:

final class TeamTests: XCTestCase {

    func testExample() {
        let team = Team()

        XCTAssertNil(team.name) // fails, name is an empty string
        XCTAssertNil(team.codes) // fails, codes is en empty array

        XCTAssert(team.name.isEmpty)
        XCTAssert(team.codes.isEmpty)
    }

}

the assertions checking for nil always fail. That shouldn’t be surprising, as the types become String (not String?) and Array<String> (not Array<String>?) in Swift. The same assertions written in an Objective-C test succeed:

@interface TeamTests : XCTestCase

@end

@implementation TeamTests

- (void)testExample {
    Team *team = [[Team alloc] init];

    XCTAssertNil(team.name); // succeeds
    XCTAssertNil(team.codes); // succeeds
}

@end

Admittedly this is a programmer error. Without initialising name and codes to proper values we are indeed breaking the contract established by nonnull in the public interface.

And I would argue that this implicit initialisation makes sense. Without setting these properties to any value, it is not possible to implement the equivalent generated interface:

open class Team : NSObject {
    
    open var name: String    
    open var codes: [String]

}

An empty string and an empty array are perfect candidates for a sensible default value when bridging.

Is this expected and/or intended behaviour or rather a detail of the bridging implementation. Moreover, is it documented anywhere?

Thank you!

PS: Whether the compiler should emit a warning when using XCTAssertNil with a non optional type is another discussion.

I have no inside knowledge on this, but I would guess that this is an effect of the bridging behavior that is probably not actually intentional. Apart from trapping at runtime, there’s not much that could be done, though.

Note that this mapping to empty strings and arrays can only happen when the Objective-C representation is a class and the Swift representation is not a class.

I would argue very strongly against this statement. It is highly dependent on the situation whether empty arrays or strings are acceptable values, much less “sensible” values.


I came across this very behavior a few weeks ago while debugging some code that behaved in unexpected ways. Essentially, it was a Swift method that expected a non-optional String that was called from Objective-C and passed nil where it shouldn’t have been.

(Fun fact: the very first thing that method does is verify that the input is not an empty string :wink:)

Just like this:

@objcMembers class Greeter: NSObject {
    class func sayHello(to name: String) { … }
}

… and called from Objective-C like this:

NSString *name = nil;
[Greeter sayHelloTo:name]; // No warning here.

The surprising thing here was that the Swift method receives an empty String. If that had trapped with some assertion or something like that, it would have been much easier to debug.

1 Like

Yes, you are right. I meant they are perfect candidates in the context where such initialisation is the intended behaviour and the implementation must set a value. But I would rather have some kind of trap or compiler diagnostic as well.

I'm curious now about something in your code sample. When you pass nil to -sayHelloTo:, does the compiler emit a warning if you set CLANG_ANALYZER_NONNULL=YES?

It’s set in Xcode to Yes (Aggressive) and I get no warning at all. It looks like you only get warnings if you pass the nil literal directly, but if there’s an indirection via a variable, clang does not seem to catch it.

This is in fact intentional, both as a guard against incorrectly-annotated Objective-C APIs, and as backward compatibility support for Objective-C APIs that historically returned nil instead of empty strings/arrays but don't make a semantic distinction between those as return values. Note that this behavior is specific to bridged types that have a reasonable empty state; bridged types that don't have empty states will trap at runtime if the ObjC API tries to smuggle a null result through to Swift.

12 Likes

Ah, that makes sense. Thanks for the clarification.

If anyone reading this is interested in actual code, here’s String._unconditionallyBridgeFromObjectiveC(_:) that maps nil to String() and here’s Locale. _unconditionallyBridgeFromObjectiveC(_:) that just traps on nil.

4 Likes

@Joe_Groff Shouldn't this be documented somewhere, maybe in "Working with Foundation Types" or "Designating Nullability in Objective-C APIs"?

If so, how can I help there? Opening a radar? Since this is specific to interoperability with Objective-C I don't see a place in the Swift documentation to include it.

3 Likes

Opening a radar?

Yes. The articles you referenced are Apple documents and thus bugs should go through Apple Bug Reporter.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Reported in rdar://48666585

1 Like