Warn when Optional converts to Any, and bridge Optional As Its Payload Or NSNull
- Proposal: SE-0140
- Author: Joe Groff
- Review Manager: Doug Gregor
- Status: Implemented (Swift 3.0.1)
- Decision Notes: Rationale
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;
@endlet 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
Optionalvalue into Objective-C will work more consistently with howOptionals insideAnys work in Swift. Swift considersTto be a subtype ofT?, so even if anAnycontains an optional, casting to the nonoptional type will succeed if theAnycontains an optional with a value. By analogy in Objective-C, we would want anOptionalpassed into ObjC asidto be an instance of the unwrapped class type, so thatisKindOfClass:andrespondsToSelector:queries succeed if a valid value is passed in. - Passing
Optional.noneto Objective-C APIs that idiomatically expectNSNullwill do the right thing. Swift collections such as[T?]will automatically map toNSArrays containingNSNullsentinels, their closest idiomatic analogue in ObjC. - Passing
Optional.noneto Objective-C APIs that expect neithernilnorNSNullwill fail in more obvious ways, usually with anNSNull does not respond to selectorexception 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 unwrappedIf 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:
- 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:
- 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 IntAnd 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.