- Proposal: SE-0116
- Author: Joe Groff
- Status: Active review July 5...11
- Review manager: Chris Lattner
Objective-C interfaces that use id
and untyped collections should be imported
into Swift as taking the Any
type instead of AnyObject
.
Swift-evolution thread: Importing Objective-C id
as Swift Any
Objective-C's id
type is currently imported into Swift as AnyObject
. This
is the intuitive thing to do, but creates a growing tension between idiomatic
Objective-C and Swift code. One of Swift's defining features is its value
types, such as String
, Array
, and Dictionary
, which allow for efficient
mutation without the hazards of accidental state sharing prevalent with mutable
classes. To interoperate with Objective-C, we transparently bridge value
types to matching idiomatic Cocoa classes when we know the static types of
method parameters and returns. However, this doesn't help with polymorphic
Objective-C interfaces, which to this day are frequently defined in terms of
id
. These interfaces come into Swift as AnyObject
, which doesn't naturally
work with value types. To keep the Swift experience using value types with
Cocoa feeling idiomatic, we've papered over this impedance mismatch via various
language mechanisms:
- Bridgeable types implicitly convert to their bridged object type. This
makes it convenient to use bridgeable types with polymorphic Objective-C
interfaces, for example, to build a heterogeneous property list as an
[AnyObject]
of bridged objects (which in turn bridges to anNSArray
). - Given a dynamically-typed object of static type
AnyObject
, the value can be dynamically cast back to a Swift value type usingis
,as?
, andas!
.
While often convenient, these features are inconsistent with the rest of the language and have in practice been a common source of problems and surprising behavior. We have popular proposals in flight to remove the special cases:
- SE-0072
(accepted) removes the implicit conversion, requiring one to explicitly write
x as NSString
orx as AnyObject
to use a bridgeable value as an object. - SE-0083
(deferred for later consideration) removes the dynamic casting behavior and
overloading of
as
coercion, requiring one to use normal constructors to convert between value types and object types.
Meanwhile, Foundation has extensively adopted value types in Swift 3, making
this a bigger problem in scope than a handful of standard library types. Swift
and Foundation are also being ported to non-Apple platforms that don't ship an
Objective-C runtime, and we want to provide a consistent interface to
Foundation between Darwin and other platforms. This means that, even
independent of Objective-C, Foundation is still forced to express abstractions
in terms of AnyObject
. Our current status quo
pits the goal of providing a more consistent and predictable standalone
language against the goal of providing a portable set of core libraries--if we
chip away at the implicit bridging behavior to make the language more
predictable, the parts of the standard library and Foundation that are designed
to take most advantage of Swift's features become harder and less attractive to
use, and the less idiomatic NS
container classes need to be interacted with
more frequently.
The fundamental tension here is that, whereas ObjC's polymorphism is centered
on objects, Swift opens up polymorphism to all types. Rather than treat
bridging as something only a set of preordained types can partake in, we can
say that all Swift types can bridge to an Objective-C object. By doing
this, we can import Objective-C APIs in terms of Swift's Any
,
making them interoperate seamlessly with Swift value types without special-case
language behavior. If we achieve this, we can move nearly all of the bridging
glue "below the fold" into the compiler implementation, allowing users to
work with value types and have them just work with Cocoa APIs without relying
on special language rules.
- We change the behavior of Objective-C APIs imported into Swift so that the
id
type is imported asAny
in bridgeable positions. At compile time and runtime, the compiler introduces a universal bridging conversion operation when a Swift value or object is passed into Objective-C as anid
parameter. - When
id
values are brought into Swift asAny
, the resulting existentials support ambivalent dynamic casting to bridge back to either class references or Swift value types. - Untyped Cocoa collections come in as collections of
Any
.NSArray
imports as[Any]
,NSDictionary
as[AnyHashable: Any]
, andNSSet
asSet<AnyHashable>
(using anAnyHashable
type erasing container to be designed in a follow-up proposal).
To describe what bridging an Any
to id
means, we need to establish a
universal bridging conversion from any Swift type to an Objective-C object.
There are several cases to consider:
- Classes are the easiest case—they exist in both Objective-C and Swift and play many of the same roles. A Swift class reference can be brought into Objective-C as is.
- Bridged value types with established bridging behavior, such as
String
,Array
,Dictionary
,Set
, etc., should continue to bridge to instances of their corresponding idiomatic Cocoa classes, using the existing internal_ObjectiveCBridgeable
protocol. The set of bridged types can be extended in the Swift implementation (and hopefully, eventually, by third parties too) by adding conformances to that protocol. This proposal does not address adding or removing any new bridging behavior, though that would be a natural follow-up proposal. - Unbridged value types without an obvious Objective-C analog can still be
boxed in an instance of an immutable class. The name and functionality of
this class doesn't need to exposed in the language model, beyond being
minimally
id
-compatible to round-trip through Objective-C code, and being dynamically castable back into the original Swift type from Swift code when anAny
value contains a reference to a box.
SE-0083
seeks to simplify the behavior of dynamic casts in pure Swift code by taking
away the runtime's current ability to dynamically apply bridging conversions.
However, this functionality is necessary when working with an Any
value
that has come from an id
value in Objective-C, since it is impossible to
know locally whether the object is intended to be consumed in Swift as
a bridged value or as a class instance. We can still simplify the dynamic
casting behavior for "pure" Swift code by making ambivalence a per-value
property of existentials. Native Swift existentials will not bridge in dynamic
casts, as specified by SE-0083:
var x: Any = "foo" as String
x as? String // => String "foo"
x as? NSString // => nil
When an id
reference is brought into Swift as an Any
, at that point we mark
the Any
value as having ambivalent casts enabled, so that dynamic casts on
the Any
succeed for either its class type or a Swift type that would bridge
to that class:
// Objective-C
@implementation Foo
+ (id)foo {
return @"foo";
}
@end
// Swift
var x /*: Any*/ = Foo.foo()
x as? String // => String "foo" by bridging
x as? NSString // => NSString "foo"
Ambivalent Any
s would have to be produced from any context where objects of
unknown type are brought into Swift, including not only return types but
accesses into untyped NS
collections of Any
or AnyHashable
,
block parameters, and method override parameters as well.
If we take the class constraint away from singular id
values, it also makes
sense to do so for collections, for instance, bridging an untyped NSArray
from Objective-C to a Swift [Any]
. This also implies that we would need to
lift the current class restriction on covariant Array conversions—[T]
would
need to be supported as a subtype of [Any]
.
Dictionary
and Set
require their keys to be Hashable
at minimum, so we
would need a way to represent a heterogeneous Hashable
type to bridge an
untyped NSDictionary
or NSSet
. The Hashable
protocol type cannot itself be used due to limitations in Swift 3; namely,
Hashable
refines the Equatable
protocol, which demands Self
constraints
of its ==
requirement, and beyond that, we do not support protocol types
conforming to their own protocols in general. As a stopgap, we will likely need
an AnyHashable
type-erased container in the standard library.
For most code, the combination of this proposal with
SE-0072
should have the net effect of most Swift 2 style code working as it does today,
allowing value types to be passed into untyped Objective-C APIs without
requiring explicit bridging or unbridging operations. There will definitely
be edge cases that may behave slightly different, where the AnyObject
constraint nudges overload resolution or implicit conversion in
a different direction from what it would take without context.
There are several moving parts involved in making id-as-Any work well, and several of them deserve independent consideration as separate proposals.
We need a type-erased container to represent a heterogeneous hashable type
that is itself Hashable
, for use as the upper-bound type of heterogeneous
Dictionary
s and Set
s. The user model for this type would ideally align
with our long-term goal of supporting Hashable
existentials directly, so
the type deserves some short-term compiler support to help us get there.
Aside from AnyObject
, another way unnecessary @objc
-isms intrude themselves
into Swift code is through NSObjectProtocol
requirements. In practice, nearly
every class in Swift on an Apple platform conforms to this protocol--native
Swift classes inherit from a common Objective-C SwiftObject
base class
internal to the Swift runtime that implements the NSObjectProtocol
methods,
and almost all Cocoa classes inherit either NSObject
or NSProxy
. We can
also make the box class used to bridge Swift values provide NSObjectProtocol
functionality. Eliminating NSObjectProtocol
as a formal requirement in Swift
will allow native Swift classes, and often value types too, to interoperate
more smoothly with Cocoa code with less explicit @objc
interop glue.
Removing the AnyObject
constraint and special typing rules makes it more
important for the Any
-to-id
to do the right thing for as many types as
possible. Some obvious candidates include:
- Extending
NSNumber
bridging to cover not onlyInt
andDouble
, but all[U]IntNN
andFloatNN
numeric types, as well as theDecimal
struct from Foundation. - Bridging Foundation and CoreGraphics structs like
CGRect
andNSRange
toNSValue
, the idiomatic box class for those types. - When an
Optional
is passed as a non-nullableid
, we might consider bridging the optional'snil
value toNSNull
. This would allow containers of optional such as[Foo?]
to bridge idiomatically toNSArray
s ofFoo
andNSNull
elements.
Once we've established a universal bridging mechanism for all Swift types, this enables further closing of the expressivity gap with value types and the Objective-C bridge:
We could lift the AnyObject
constraint on imported ObjC generic type
parameters, allowing ObjC generics to work with Swift value types.
If we can bridge arbitrary Swift values to Objective-C objects, then we could
conceivably implement @objc
protocol conformance for Swift value types as
well, by setting up the bridged Objective-C class to conform to the protocol
and respond to the necessary messages in the Objective-C runtime. This would
allow Foundation to vend protocols that work with its value types without
compromising portability between Darwin and corelibs platforms. If we wanted to
make this work, it would inform some tradeoffs in the potential implementation:
- We would probably need to produce a unique boxing Objective-C class for every
type that conformed to an Objective-C protocol, where we might otherwise be
able to share one class (or
NSValue
for C types). - For value types with custom bridging, like
String
/NSString
, does an@objc
conformance automatically apply to the bridged class, if not at compile time, at least at runtime? - Many Objective-C protocols are intended to be class-constrained,
particularly delegate protocols, which are idiomatically weak-referenced from
the delegatee class. If
@objc
no longer implies a class constraint in Swift, it wouldn't be possible for a property of@objc
protocol type to beweak
, unless we underwent an annotation or heuristic effort to distinguish Objective-C protocols that are supposed to be class-constrained.
We currently bestow the AnyObject
existential type with the special ability
to look up any @objc
method dynamically, in order to ensure id
-based ObjC
APIs remain fluent when used in Swift. This is another special, unprincipled,
nonportable feature that relies on the Objective-C runtime. If we change id
to bridge to Any
, it definitely no longer makes sense to apply to
AnyObject
. A couple of possibilities to consider:
-
We could transfer the existing
AnyObject
behavior verbatim toAny
. -
We could attempt to eliminate the behavior as a language feature. An approximation of AnyObject's magic behavior can be made using operators and unapplied method references, in a way that also works for Swift types:
/// Dynamically dispatch a method on Any. func => <T, V>(myself: Any, method: (T) -> V) -> V? { if let myself = myself as? T { return method(myself) } return nil }
though that's not quite the right thing for
id
lookup, since you want arespondsToSelector
rather thanisKindOfClass
check. -
We could narrow the scope of the behavior. Jordan has suggested allowing only property and subscript lookup off of
AnyObject
orAny
, as a way of allowing easy navigation of property lists, one of the most common sources ofid
in Foundation. -
If we're confident that the SDK will be sufficiently Swiftified that
id
s become relatively rare, maybe we could get away without a replacement at all.