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-explicit as coercions don’t otherwise exist
in the language, since as 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!, or is-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. I’d like to
propose the following changes:
Dynamic casts as?, as! and is 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.
The Rules of as[?]
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, if x as! T succeeds. Casting to a type U should
succeed and give the same result for any derived cast result.
x as! T as U === x as! U. If T is coercible to U, then you should get the same
result by casting to Tand coercing to U as by casting to U 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 in https://github.com/apple/swift/pull/1949.
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.
The Abilities of as? Today
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 {
return 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 to NSError 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 as NSErrorobjects 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 Optionals. If an Optional contains some value, it is extracted,
and the cast is attempted on the contained value; the cast fails if the source
value is none 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 as some value
of the result Optional type. nil source values succeed and become nil 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, and Set.
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 the intrinsic type only 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. Optional and covariant container
conversions have also been criticized as occasionally surprising and
inconsistent with the rest of the language. If we curtail these conversions in
the compiler, we would also want to consider removing their special dynamic
cast behavior too. For the purposes of this discussion, I’d like to focus on
removing the bridging behavior, cases (5) through (7).
Replacements for Dynamic Cast Behavior
If we remove bridging behavior from dynamic casts, we still need to provide API
for performing those conversions. I’d recommend introducing unlabeled
initializers for these conversions, matching the conventions for other
value-preserving conversions in the standard library:
extension String {
init(_ ns: NSString)
}
extension NSString {
init(_ value: String)
}
extension Array where Element: _ObjectiveCBridgeable {
init(_ ns: NSArray<Element._ObjectiveCType>)
}
extension NSArray {
init<BridgedElement: _ObjectiveCBridgeable
where BridgedElement._ObjectiveCType == Element>(
_ value: Array<BridgedElement>)
}
/* etc. */
NSError bridging can also be extracted from the runtime, and the same
functionality exposed as a factory initializer on NSError:
extension NSError {
init(_ swiftError: ErrorType)
}
It’s also useful to be able to conditionally bridge to a value type from
AnyObject, especially when working with heterogeneous property lists from
Cocoa. This could be handled using failable initializers:
extension String {
init?(bridging: AnyObject)
}
extension Array {
init?(bridging: AnyObject)
}
extension Dictionary {
init?(bridging: AnyObject)
}
/* etc. */
(This can probably be factored into a protocol extension on
_ObjectiveCBridgeable.) Similarly, one could add a failable initializer to
ErrorProtocol for bridging NSErrors back to Swift error values:
extension ErrorType {
init?(bridging: NSError)
}
If you want to get really reductionist, you can 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. Regardless, I think we want type-checking dynamic casts to
be clearly a different operation from these bridging conversions. This will
lead to a cleaner, easier-to-understand model with less special-case magic
behavior.
-Joe_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution