as, as?, and as!

In Debugging Generics in Swift, Joshua Emmons touches on a tricky area in Swift: the difference between as, as?, and as!.

Consider the following, simplified example from Joshua’s article. The second line traps with a runtime error:

4 as Double // fine
4 as! Double // crashes

You might intuitively think that as! should never fail where as succeeds, but these are in fact two very different operations.

as? and as! are runtime operations

as? and as! perform downcasting at runtime. When the program is running, the expression 4 in the example has already been assigned the type Int. As a result, the cast to Double fails because Double and Int are unrelated types (and Swift generally eschews implicit conversions between number types).

The compiler will correctly spot the futile cast if we use as? instead of as!:

4 as? Double // warning: Conditional downcast from literal to 'Double'
             // always fails; consider using 'as' coercion

as? and as! are the same operation, the only difference being that as! force-unwraps its result. These two lines are equivalent:

x as! Y
(x as? Y)!

(As we’ve seen here, this isn’t quite true as far as compiler warnings are concerned.)

The third member of this family is the is operator. Like as? and as!, is performs a runtime check.

as performs type coercion at compile time

In contrast, as coerces a value to a type at compile time. The coercion succeeds in the example because 4 is an integer literal at this point, not an Int.

Literals have no inherent type; the expression 4 is allowed to become any type that implements the ExpressibleByIntegerLiteral protocol. The … as Double instructs the compiler to use Double’s conformance, just like any other implicit or explicit type annotation would. These two lines are equivalent:

let x = 4 as Double
let x: Double = 4

Ben Cohen:

The best way to think of the as keyword is as a way to provide “type context”.

In patterns, as is a runtime operation

A syntactic nuance: when using as in pattern matching contexts, the check is performed at runtime (it has to be). Example:

let value: Any = 
switch value {
case let i as Int: 
case let s as String: 
}

The as pattern is closer in meaning to the as? and as! operators than to the as operator. Confusing, I know.

This isn’t the whole story

The full semantics of as, as?, as!, and is are so complex that I’m sure I can’t describe them, and apparently I’m not alone:

Xiaodi Wu:

I would wager that the number of people who can enumerate all of the current functions of as, is, as?, and as!—even in this forum—is in the single digits.

Johannes Weiss:

‘as’ and ‘is’ really mean ‘can X be transformed to Y using some random black magic that we put into the Swift compiler for legacy reasons’

It would be nice to have a definitive list of all the functions of these operators, and maybe I’ll try to compile one, but that will have to wait.

I’ll end by mentioning one more use you’ll encounter regularly if you’re an app developer for Apple platforms: bridging. We write nsString as String to bridge between Objective-C and Swift types for Cocoa interop.