Skip to content

Latest commit

 

History

History
206 lines (166 loc) · 8.95 KB

0140-bridge-optional-to-nsnull.md

File metadata and controls

206 lines (166 loc) · 8.95 KB

Warn when Optional converts to Any, and bridge Optional As Its Payload Or NSNull

Introduction

Optionals can be used as values of Any type. After SE-0116, this means you can pass an Optional to an Objective-C method expecting nonnull id:

// Objective-C
@interface ObjCClass : NSObject
- (void)imported: (id _Nonnull)value;
@end
let s1: String? = nil, s2: String? = "hello"
// works, should warn, currently produces an opaque object type
ObjCClass().imported(s1)
// works, should warn, currently produces an opaque object type
ObjCClass().imported(s2)

This is often a mistake, and we should raise a warning when it occurs, but is occasionally useful. When an Optional is intentionally passed into Objective-C as a nonnull object, we should bridge some value by bridging the wrapped value, and bridge nones to a singleton such as NSNull:

let s1: String? = nil, s2: String? = "hello"
// proposed to bridge to NSNull.null
ObjCClass().imported(s1)
// proposed to bridge to NSString("hello")
ObjCClass().imported(s2)

Swift-evolution thread: here

Motivation

SE-0116 changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass Swift value types such as String and Array into ObjC. One unfortunate effect is that, since Any in Swift can hold anything, it is now possible to pass an Optional to an Objective-C API that expects a nonnull id. This is not a new issue in Swift--it is possible to use an Optional anywhere there's unconstrained polymorphism, for example, in string interpolations, or as the element type of a collection, such as an Array<T?>, but bridging id as Any makes this problem much more prevalent. Since Cocoa APIs traffic heavily in Optionals, it's very easy to accidentally take an Optional result from one API and pass it as Any to another API without unwrapping it first. We can introduce a warning when an Optional is implicitly converted to Any.

However, since this is dynamic behavior, it is impossible to prevent Optionals ending up in Anys in all cases, nor would it be desirable to completely prevent it, since it is sometimes useful to keep an Optional inside an Any. Containers and other generic types with Optional members are also useful. Because Optional does not currently have any special bridging behavior, it will currently be bridged to Objective-C as an opaque object, which will be unusable by most Objective-C API. In Objective-C, Cocoa provides NSNull as a standard, non-nil singleton to represent missing values inside collections, since NSArray, NSDictionary, and NSSet are unable to hold nil elements. If we bridge Optionals so that, when they contain some value, we bridge the wrapped value, or use NSNull to represent none, then we get several advantages over the current behavior:

  • Passing a wrapped Optional value into Objective-C will work more consistently with how Optionals inside Anys work in Swift. Swift considers T to be a subtype of T?, so even if an Any contains an optional, casting to the nonoptional type will succeed if the Any contains an optional with a value. By analogy in Objective-C, we would want an Optional passed into ObjC as id to be an instance of the unwrapped class type, so that isKindOfClass: and respondsToSelector: queries succeed if a valid value is passed in.
  • Passing Optional.none to Objective-C APIs that idiomatically expect NSNull will do the right thing. Swift collections such as [T?] will automatically map to NSArrays containing NSNull sentinels, their closest idiomatic analogue in ObjC.
  • Passing Optional.none to Objective-C APIs that expect neither nil nor NSNull will fail in more obvious ways, usually with an NSNull does not respond to selector exception of some kind. id-based Objective-C APIs fundamentally cannot catch all misuses at compile time, so runtime errors on user error are unavoidable.

NSNull is rare in Cocoa, and perhaps not that much more useful than an arbitrary sentinel object or opaque box, but is the object most likely to have a useful meaning to existing ObjC APIs.

Proposed solution

Converting an Optional<T> to an Any should raise a warning unless the conversion is made explicit. When an Optional<T> value does end up in an Any, and gets bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

Detailed design

Warning when Optional is converted to Any

When we put an Optional into an Any, we should warn on the implicit conversion:

let x: Int? = 3
let y: Any = x // warning: Optional was put in an Any without being unwrapped

// `print` takes parameters of type Any
print(x) // warning: Optional was passed as an argument of type Any without
         // being unwrapped

// `NSMutableArray` has elements of type `id _Nonnull` in ObjC,
// imported as `Any` in Swift
let a = NSMutableArray()
a.add(x)  // warning: Optional was passed as an argument of type Any without
          // being unwrapped

If passing the Optional is intentional, the warning can be suppressed by making the conversion explicit with as Any:

let y: Any = x as Any
print(x as Any)
a.add(x as Any)

Bridging Optionals

Optional can conform to the implementation- internal _ObjectiveCBridgeable protocol. One subtlety is with nested optional types, such as T??; these are rare, but when they occur, we would want to preserve their value in round-trips through the Objective-C bridge, so we would need to be able to use a different sentinel to distinguish .some(.none) from .none. Since there is no idiomatic equivalent in Cocoa for a nested optional, we can use an opaque singleton object to represent each level of none nesting:

var x: String???

x = String?.none
x as AnyObject // bridges to NSNull, since it's an unnested `.none`

x = String??.none
x as AnyObject // bridges to _SwiftNull(1), since it's a double-`.none`

x = String???.none
x as AnyObject // bridges to _SwiftNull(2), since it's a triple-`.none`

Like default-bridged _SwiftValue boxes, these would be id-compatible but otherwise opaque singletons.

Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, Optionals that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from Optional to Any to id to Any and back by dynamic casting.

Alternatives considered

There are unconstrained contexts other than Any promotion where Optionals can be used by accident without unwrapping, such as String.init(describing:), which takes a generic <T>. We may want to warn in some of these cases, but there are subtleties that require deeper consideration. Extending the warning can be considered in the future.

We could do nothing, and leave Optionals to bridge by opaque boxing. Charles Srstka argues that passing Optionals into ObjC via Any is programmer error, so should fail early at runtime:

I’d say my position has three planks on it, and the above is pretty much the first plank: 1) the idea of an array of optionals is a concept that doesn’t really exist in Objective-C, and I do think that passing one to Obj-C ought to be considered a programmer error.

The other two planks would be:

  1. Bridging arrays of optionals in this manner could mask the aforementioned programmer error, resulting in unexpected, hard-to-reproduce crashes when an NSNull is accessed as if it were something else, and:
  1. Objective-C APIs that accept NSNull objects are fairly rare, so the proposed bridging doesn’t really solve a significant problem (and in the cases where it does, using a map to replace nils with NSNulls is not difficult to write).

This point of view is understandable, but is inconsistent with how Swift itself dynamically treats Optionals inside Anys:

  let a: Int? = 3
  let b = a as Any
  let c = a as! Int // Casts '3' out of the Optional as a non-optional Int

And while it's true that Cocoa uses NSNull sparingly, it is the standard sentinel used in the few places where a null-like object is expected, such as in collections and JSON serialization.