- Proposal: SE-0083
- Author: Joe Groff
- Status: Deferred (Rationale)
- Review manager: Chris Lattner
Dynamic casts using as?
, as!
, and is
are currently able to dynamically
perform Cocoa bridging conversions, such as from String
to NSString
or from
an ErrorProtocol
-conforming type to NSError
. This functionality should be
removed to make dynamic cast behavior simpler, more efficient, and
easier to understand. To replace this functionality, initializers should be
added to bridged types, providing an interface for these conversions that's
more consistent with the conventions of the standard library.
Swift-evolution thread: Reducing the bridging magic in dynamic casts
When we introduced Swift, we wanted to provide value types for common containers, with the safety and state isolation benefits they provide, while still working well with the reference-oriented world of Cocoa. To that end, we invested a lot of work into bridging between Swift's value semantics containers and their analogous Cocoa container classes. This bridging consisted of several pieces in the language, the compiler, and the runtime:
-
Importer bridging, importing Objective-C APIs that take and return NSString, NSArray, NSDictionary and NSSet so that they take and return Swift's analogous value types instead.
-
Originally, the language allowed implicit conversions in both directions between Swift value types and their analogous classes. We've been working on phasing the implicit conversions out--we removed the object-to-value implicit conversion in Swift 1.2, and propose to remove the other direction in SE-0072 --but the conversions can still be performed by an explicit coercion
string as NSString
. These required-explicitas
coercions don't otherwise exist in the language, sinceas
generally is used to force coercions that can also happen implicitly, and value-preserving conversions are more idiomatically performed by constructors in the standard library. -
The runtime supports dynamic bridging casts. If you have a value that's dynamically of a Swift value type, and try to
as?
,as!
, oris
-cast it to its bridged Cocoa class type, the cast will succeed, and the runtime will apply the bridging conversion:// An Any that dynamically contains a value "foo": String let x: Any = "foo" // Cast succeeds and produces the bridged "foo": NSString let y = x as! NSString
Since Swift first came out, Cocoa has done a great job of "Swiftification",
aided by new Objective-C features like nullability and lightweight generics
that have greatly improved the up-front quality of importer-bridged APIs.
This has let us deemphasize and gradually remove the special case implicit
conversions from the language. I think it's time to consider extricating them
from the dynamic type system as well, making it so that as?
, as!
, and is
casts only concern themselves with typechecks, and transitioning to
using standard initializers and methods for performing bridging conversions.
The dynamic cast behavior has been a source of surprise for many users, and
unfairly privileges bridged value types and classes with nonstandard behavior.
I propose the following:
- Dynamic casts
as?
,as!
andis
should no longer perform bridging conversions between value types and Cocoa classes. - Coercion syntax
as
should no longer be used to explicitly force certain bridging conversions. - To replace this functionality, we should add initializers to bridged value types and classes that perform the value-preserving bridging operations.
Our original goal implementing this behavior into the dynamic casting machinery was to preserve some transitivity identities between implicit conversions and casts that users could reason about, including:
x as! T as! U
===x as! U
, ifx as! T
succeeds. Casting to a typeU
should succeed and give the same result for any derived cast result.x as! T as U
===x as! U
. IfT
is coercible toU
, then you should get the same result by casting toT
and coercing toU
as by casting toU
directly.x as T as! U
===x as! U
. Likewise, coercing shouldn't affect the result of any ensuing dynamic casts.x as T as U
===x as U
.
The interaction of these identities with the bridging conversions, as well as
with other type system features like implicit nonoptional-to-Optional
conversion, occasionally requires surprising behavior, for instance the
behavior of nil
Optional values.
These rules also inform the otherwise-inconsistent use of as
to perform
explicit bridging conversions, when as
normally only forces implicit
conversions. By simplifying the scope of dynamic casts, it becomes easier to
preserve these rules without bugs and unfortunate edge cases.
In discussing how to change the behavior of dynamic casts, it's worth enumerating all the things dynamic casts are currently able to do:
-
Check that an object is an instance of a specific class.
class Base {}; class Derived: Base {} func isKindOfDerived(object: Base) -> Bool { return object is Derived } isKindOfDerived(object: Derived()) // true isKindOfDerived(object: Base()) // false
-
Check that an existential contains an instance of a type.
protocol P {} extension Int: P {} extension Double: P {} func isKindOfInt(value: P) -> Bool { return value is Int } isKindOfInt(value: 0) // true isKindOfInt(value: 0.0) // false
-
Check that a generic value is also an instance of a different type.
func is<T, U>(value: T, kindOf: U.Type) -> Bool { return value is U } is(value: Derived(), kindOf: Derived.self) // true is(value: Derived(), kindOf: Base.self) // true is(value: Base(), kindOf: Derived.self) // false is(value: 0, kindOf: Int.self) // true
-
Check whether the type of a value conforms to a protocol, and wrap it in an existential if so:
protocol Fooable { func foo() } func fooIfYouCanFoo<T>(value: T) { if let fooable = value as? Fooable { fooable.foo() } } extension Int: Fooable { func foo() { print("foo!") } } fooIfYouCanFoo(value: 1) // Prints "foo!" fooIfYouCanFoo(value: "bar") // No-op
-
Check whether a value is
_ObjectiveCBridgeable
to a class, or conversely, that an object is_ObjectiveCBridgeable
to a value type, and perform the bridging conversion if so:func getAsString<T>(value: T) -> String? { return value as? String } func getAsNSString<T>(value: T) -> NSString? { return value as? NSString } getAsString(value: "string") // produces "string": String getAsNSString(value: "string") // produces "string": NSString let ns = NSString("nsstring") getAsString(value: ns) // produces "nsstring": String getAsNSString(value: ns) // produces "nsstring": NSString
-
Check whether a value conforms to
ErrorProtocol
, and bridge it toNSError
if so:enum CommandmentError { case Killed, Stole, GravenImage, CovetedOx } func getAsNSError<T>(value: T) -> NSError? { return value as? NSError } getAsNSError(CommandmentError.GravenImage) // produces bridged NSError
This is what enables the use of
catch let x as NSError
pattern matching to catch Swift errors asNSError
objects today. -
Check whether an
NSError
object has a domain and code matching a type conforming to_ObjectiveCBridgeableErrorProtocol
, and extracting the Swift error if so:func getAsNSCocoaError(error: NSError) -> NSCocoaError? { return error as? NSCocoaError } // Returns NSCocoaError.fileNoSuchFileError getAsNSCocoaError(error: NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: []))
-
Drill through
Optional
s. If anOptional
containssome
value, it is extracted, and the cast is attempted on the contained value; the cast fails if the source value isnone
and the result type is not optional:var x: String? = "optional string" getAsNSString(value: x) // produces "optional string": NSString x = nil getAsNSString(value: x) // fails
If the result type is also
Optional
, a successful cast is wrapped assome
value of the result Optional type.nil
source values succeed and becomenil
values of the result Optional type:func getAsOptionalNSString<T>(value: T) -> NSString?? { return value as? NSString? } var x: String? = "optional string" getAsOptionalNSString(value: x) // produces "optional string": NSString? x = nil getAsOptionalNSString(value: x) // produces nil: NSString?
-
Perform covariant container element checks and conversions for
Array
,Dictionary
, andSet
.
There are roughly three categories of functionality intertwined here. (1) through (4) are straightforward dynamic type checks. ([4] is arguably a bit different from [1] through [3] in that protocol conformances are extrinsic to a type, whereas [1] through [3] check only the intrinsic type of the participating value.) (5) through (7) involve Cocoa bridging conversions. (8) and (9) reflect additional implicit conversions supported by the language at compile time into the runtime type system.
Within the scope of this proposal, I'd like to propose removing behaviors (5) through (7) from dynamic casting:
- (5) Dynamic casts will no longer check for
_ObjectiveCBridgeable
conformance in their source or destination types."string" as Any as? NSString
would fail. - (6) Types that conform to
ErrorProtocol
will no longer dynamically cast toNSError
. - (7)
NSError
instances will no longer dynamically cast toErrorProtocol
- conforming types.
With the bridging behavior removed from dynamic casting, we no longer have
the transitivity justification for the special case behavior of as
forcing bridging conversions, as in nsstring as String
. This functionality
should be removed, leaving as
with only its core behavior of acting as a
type annotation to force implicit conversions.
To replace the removed language functionality, we should provide library APIs
that follow the conventions set by the standard library. Bridging conversions
are value-preserving, and the standard library uses unlabeled init(_:)
initializers for value-preserving conversions. For nongeneric unconditionally-
bridgeable types, such as String
and NSString
, this is straightforward
(assuming we gain the ability to define factory initializers in Swift at
some point):
extension String {
init(_ ns: NSString) {
self = ._unconditionallyBridgeFromObjectiveC(ns)
}
}
extension NSString {
// Without a first-class factory init feature, this can be simulated with
// a protocol extension
factory init(_ string: String) {
self = string._bridgeToObjectiveC()
}
}
As an implementation detail, we could add a refinement of
_ObjectiveCBridgeable
for unconditionally-bridgeable types and implement
these initializer pairs as protocol extensions. Similarly, we can provide
NSError
with a factory initializer in the Foundation overlay to handle
bridging from ErrorProtocol
:
extension NSError {
factory init(_ error: ErrorProtocol) {
self = _bridgeErrorProtocolToNSError(error)
}
}
and the inverse bridging of NSError
s to special ErrorProtocol
types can
be handled by modifying the internal _ObjectiveCBridgeableErrorProtocol
to require an unlabeled failable initializer:
public protocol _ObjectiveCBridgeableErrorProtocol : ErrorProtocol {
/// Produce a value of the error type corresponding to the given NSError,
/// or return nil if it cannot be bridged.
init?(_ bridgedNSError: NSError)
}
For bridged generic containers like Array
, Dictionary
, and Set
,
the bridging conversions have to be failable, since not every element type
is bridgeable, and the Objective-C classes come into Swift as untyped
containers. (That may change if we're able to extend the Objective-C generics
importer support from
SE-0057
to apply to Cocoa container classes, though bridging support makes that
challenging.)
extension Array {
init?(_ ns: NSArray) {
var result: Array? = nil
if Array._conditionallyBridgeFromObjectiveC(ns, &result) {
self = result!
return
}
return nil
}
}
extension NSArray {
init?<T>(_ array: Array<T>) {
if !Array<T>.isBridgedToObjectiveC() {
return nil
}
return array._bridgeToObjectiveC()
}
}
Containers also support special force-bridging behavior, where the elements of the bridged value container are lazily type-checked and trap on access if there's a type mismatch. This can still be exposed via a separate labeled initializer:
extension Array {
init(forcedLazyBridging object: NSArray) {
var result: Array? = nil
Array._forceBridgeFromObjectiveC(object, &result)
self = result!
}
}
This seems to me like another improvement over our current behavior, where
object as! Array<T>
does lazy bridging whereas object as? Array<T>
does
eager bridging. This has been a common source of surprise, since in most other
cases x as! T
behaves equivalently to (x as? T)!
. By changing to
an interface defined within the language in terms of initializers,
Array<T>(object)!
performs eager bridging as one would normally expect, and
Array<T>(forcedLazyBridging: object)
explicitly asks for the lazy bridging.
There are common use cases that deserve consideration beyond simple back-and-
forth conversion. In Cocoa code, it's common to work with heterogeneous
containers of objects, which will come into Swift as [AnyObject]
or
[NSObject: AnyObject]
. To extract typed data from these containers, it's
useful to convert from AnyObject
to a bridged value type in one step, like
you can do with object as? String
today. We can provide a
failable initializer for this purpose:
extension _ObjectiveCBridgeable {
init?(bridging object: AnyObject) {
if let bridgeObject = object as? _ObjectiveCType {
var result: Self? = nil
if Self._conditionallyBridgeFromObjectiveC(bridgeObject, &result) {
self = result!
return
}
}
return nil
}
}
Dynamic cast bridging of NSError
is commonly used in the catch _ as NSError
formulation, to catch an arbitrary Swift error and handle it as an NSError
.
This is done frequently because ErrorProtocol
by itself provides no public
API for presenting errors. The need to explicitly bridge to NSError
could be
avoided in most cases by extending ErrorProtocol
to directly support
NSError
's core API in the Foundation overlay:
extension ErrorProtocol {
var domain: String { return NSError(self).domain }
var code: Int { return NSError(self).code }
var userInfo: [NSObject: AnyObject] { return NSError(self).userInfo }
}
Since dynamic behavior is involved, these changes cannot be automatically
migrated with full fidelity. For example, if a value of type Any
or generic
T
being cast to String
or NSString
, it's impossible for the compiler
to know whether the cast is being made with the intent of inducing the
bridging conversion. However, at least some cases can be recognized
by the compiler and migrated. For example, any cast from a value that's
statically of a value type to a class would now always fail, as would
a cast from an object to a value type, so these cases can be warned about
and fixits to use the new initializers can be offered. We can also recognize
code that uses the catch <pattern> as NSError
idiom and migrate away the
as NSError
cast if we make it unnecessary by extending ErrorProtocol
.
As discussed above, in addition to dynamic
type checks and bridging conversions, dynamic casting also has special-case
behavior for (8) drilling through Optional
s and (9) performing covariant
Array
, Dictionary
, and Set
conversions. This dynamic behavior matches
the special implicit conversions supported statically in the language, but
has also been a source of complexity and confusion. We could consider
separating this functionality out of the runtime dynamic casting machinery too,
but we should do so in tandem with discussions of removing or tightening the
corresponding implicit conversion behavior for optionals and covariant
containers as well.
If one wanted to get really reductionist, they could ask whether as?
and
related operations really need special syntax at all; they could in theory
be fully expressed as global functions, or as extension methods on
Any
/AnyObject
if we allowed such things.
On Smarch 13, 20XX, the core team decided to (TBD) this proposal. When the core team makes a decision regarding this proposal, their rationale for the decision will be written here.