Reimplementation of Implicitly Unwrapped Optionals

A new implementation of implicitly unwrapped optionals (IUOs) landed in the Swift compiler earlier this year and is available to try in recent Swift snapshots. This completes the implementation of SE-0054 - Abolish ImplicitlyUnwrappedOptional Type. This is an important change to the language that eliminated some inconsistencies in type checking and clarified the rule of how these values are to be treated so that it is consistent and easy to reason about. For more information, see the motivation section of that proposal.

The main change you’ll see is that diagnostics will now print T? rather than T! when referring to a value that was declared as an implicitly unwrapped optional with underlying type T. You may also encounter a source compatibility issue that requires you to modify your code before it will compile successfully.

Implicit Unwrapping is Part of a Declaration

Implicitly unwrapped optionals are optionals that are automatically unwrapped if needed for an expression to compile. To declare an optional that’s implicitly unwrapped, place a ! after the type name rather than a ?.

A mental model many people have for implicitly unwrapped optionals is that they are a type, distinct from regular optionals. In Swift 3, that was exactly how they worked: declarations like var a: Int? would result in a having type Optional<Int>, and declarations like var b: String! would result in b having type ImplicitlyUnwrappedOptional<String>.

The new mental model for IUOs is one where you consider ! to be a synonym for ? with the addition that it adds a flag on the declaration letting the compiler know that the declared value can be implicitly unwrapped.

In other words, you can read String! as “this value has the type Optional<String> and also carries information saying that it can be implicitly unwrapped if needed”.

This mental model matches the new implementation. Everywhere you have T!, the compiler now treats it as having type T? , and adds a flag in its internal representation of the declaration to let the type checker know it can implicitly unwrap the value where necessary.

The most visible result of this change is that you’ll now see diagnostics talking about T? rather than T! for values declared with T!. Seeing T? in the diagnostic rather than T! takes a little getting used to, but embracing this new mental model should help you along.

Source Compatibility

Most projects should build without running into compatibility issues. However, it’s possible that these implementation changes will result in changes in behavior that are consistent with SE-0054 but inconsistent with previous releases of the compiler.

Coercions to T!

Coercions of the form as T! were disallowed by SE-0054.

In Swift 4.1, there’s a deprecation warning for these coercions. In many cases, replacing as T! with as T?, or simply removing the coercion, results in successful compilation.

There are enough cases where existing code failed to compile using one of those two changes that there is special-case handling for this in the new implementation. Specifically, if you write x as T!, the compiler will first attempt to type check this as x as T?. Only if that fails, the compiler will attempt to type check it as (x as T?)!, forcing the optional.

This form of coercion is still considered deprecated, though, and this special handling may be removed in a future version of Swift.

Using ! on Types Rather Than Declarations

Coercions to T! are a special case of a more general issue: using ! as part of a type.

There are three places where using ! as part of a type is permitted:

  1. Property declarations
  2. Parameters in function declarations
  3. Return values in function declarations

In other locations, ! should be flagged as an error, and releases prior to Swift 4.1 attempted to do so, but missed some cases:

let fn: (Int!) -> Int! = ...   // error: not a function declaration!

Swift 4.1 emits deprecation warnings in these scenarios but continues to honor the implicit-unwrapping behavior. The new implementation in recent snapshots treats the ! as if it were ? and emits a diagnostic telling you what’s happening and that using ! in these locations is deprecated.

Calling map on a Value Declared as an Implicitly Unwrapped Optional

Previously code like this:

class C {}
let values: [Any]! = [C()]
let transformed = values.map { $0 as! C }

would have resulted in force-unwrapping values and then calling map(_:) on the array. This was true even if you had defined a member map(_:) in an extension of ImplicitlyUnwrappedOptional, because member-lookup into ImplicitlyUnwrappedOptional did not work as expected.

In the new implementation, because ! is a synonym for ?, the compiler attempts to call map(_:) on Optional<T> here:

let transformed = values.map { $0 as! C } // calls Optional.map; $0 has type [Any]

and produces: warning: cast from '[Any]' to unrelated type 'C' always fails

Because this technically passes type checking, we won’t attempt to force-unwrap values.

You can work around this by using optional chaining to produce an optional array:

let transformed = values?.map { $0 as! C } // transformed has type Optional<[C]>

or by force-unwrapping values to produce an array:

let transformed = values!.map { $0 as! C } // transformed has type [C]

Note that in many cases you won’t see a change in behavior:

let values: [Int]! = [1]
let transformed = values.map { $0 + 1 }

This continues to work as it did before because there is no way to type check the expression successfully if you call the map(_:) on Optional. Instead, we end up force-unwrapping values and calling map(_:) on the resulting array.

You Can’t Infer a Type that isn’t a Type

Because implicitly unwrapped optionals are no longer a type distinct from optionals, they can’t be inferred as a type or as any part of a type.

In the examples below, although the right-hand side of the assignment contains a value that was declared as implicitly unwrapped, the inferred type for the left-hand side only indicates that the value (or return value) is an optional.

var x: Int!
let y = x   // y has type Int?

func forcedResult() -> Int! { ... }
let getValue = forcedResult    // getValue has type () -> Int?

func id<T>(_ value: T) -> T { return value }
let z = id(x)   // z has type Int?

func apply<T>(_ fn: () -> T) -> T { return fn() }
let w: Int = apply(forcedResult)    // fails, because apply() returns unforced Int?

Some specific instances where you might also notice this change in behavior are in AnyObject lookup, try?, and switch.

AnyObject Lookup

Note that the result of AnyObject lookup is treated as an optional that is implicitly unwrapped. If you lookup a property that itself is also declared as implicitly unwrapped, the expression now has two levels of implicit unwrapping (property is declared as a UILabel!):

func getLabel(object: AnyObject) -> UILabel {
  return object.property // forces both optionals, resulting in a UILabel
}

if let and guard let only unwrap a single level of optionality.

For the following example, previous versions of Swift inferred the type of label to be UILabel! after unwrapping one level of optional for the if let. In the snapshot builds Swift will infer it to be UILabel?:

// label is inferred to be UILabel?
if let label = object.property {
   // Error due to passing a UILabel? where a UILabel is expected
  functionTakingLabel(label)
}

This can be fixed by using an explicit type:

// Implicitly unwrap object.property due to explicit type.
if let label: UILabel = object.property {
  functionTakingLabel(label) // okay
}

try?

Similarly, try? adds a level of optionality, so when combining try? with a function that returns an implicitly unwrapped value, you might find that you now need to modify code to explicitly unwrap a second level of optionality.

func test() throws -> Int! { ... }

if let x = try? test() {
  let y: Int = x    // error: x is an Int?
}

if let x: Int = try? test() { // explicitly typed as Int
  let y: Int = x    // okay, x is an Int
}

if let x = try? test(), let y = x { // okay, x is Int?, y is Int
 ...
}

switch

Swift 4.1 accepted the following code because it treated output as implicitly unwrapped:

func switchExample(input: String!) -> String {
  switch input {
  case "okay":
    return "fine"
  case let output:
    return output  // implicitly unwrap the optional, producing a String
  }
}

Note that had this been written in this way, it would not have compiled successfully:

func switchExample(input: String!) -> String {
  let output = input  // output is inferred to be String?
  switch input {
  case "okay":
    return "fine"
  default:
    return output  // error: value of optional type 'String?' not unwrapped;
                   // did you mean to use '!' or '?'?
  }
}

The new implementation infers the type of output in the first example to be a String? which is not implicitly unwrapped.

One way to get this compiling again is to force-unwrap the value:

  case let output:
    return output!

Another fix for this is to pattern match explicitly for non-nil and nil:

func switchExample(input: String!) -> String {
  switch input {
  case "okay":
    return "fine"
  case let output?: // non-nil case
    return output   // okay; output is a String
  case nil:
    return "<empty>"
  }
}

Overloading In-Out Parameters with Optional Versus Implicitly Unwrapped Optional

Swift 4.1 introduced a deprecation warning for cases where code attempts to overload a function where the difference is that an in-out parameter is a plain optional versus an implicitly unwrapped optional.

  func someKindOfOptional(_: inout Int?) { ... }

  // Warning in Swift 4.1.  Error in new implementation.
  func someKindOfOptional(_: inout Int!) { ... }

Swift 4.1 also added the ability to pass a value declared as implicitly unwrapped as an in-out parameter to a function expecting a plain optional and vice-versa. This made it possible to delete the second overload above (assuming the implementations are identical):

  func someKindOfOptional(_: inout Int?) { ... }

  var i: Int! = 1
  someKindOfOptional(&i)   // okay! i has type Optional<Int>

With the new implementation of implicitly unwrapped optionals, overloading by optionality no longer makes sense given that the type of Int! is a synonym for Int?. As a result, overloads like those above will now result in an error, and second overload (declared with Int!) must be be removed.

Extensions of ImplicitlyUnwrappedOptional

ImplicitlyUnwrappedOptional<T> is now an unavailable type alias for Optional<T>, and code that attempts to create extensions on the type won’t compile:

// 1:11: error: 'ImplicitlyUnwrappedOptional' has been renamed to 'Optional'
extension ImplicitlyUnwrappedOptional {

Bridging Nil

Rather than hitting a runtime failure when bridging nil values, nil will be bridged to NSNull.

import Foundation

class C: NSObject {}

let iuoElement: C! = nil
let array: [Any] = [iuoElement as Any]
let ns = array as NSArray
let element = ns[0] // Swift 4.1: Fatal error: Attempt to bridge
                    // an implicitly unwrapped optional containing nil

if let value = element as? NSNull, value == NSNull() {
  print("pass")     // We reach this statement with the new implementation
} else {
  print("fail")
}

Conclusion

Implicitly unwrapped optionals have been reimplemented such that they are no longer a distinct type from Optional<T>. As a result, type checking is more consistent and there are fewer special cases in the compiler. Removing these special cases should lead to fewer bugs in handling of these declarations.

You’ll probably be exposed to implicit unwrapping as a result of interacting with imported Objective-C APIs. You might occasionally find it convenient to use implicit unwrapping when declaring @IBOutlet properties, or in other places where you know you won’t access a value until it has been fully initialized. However, you are usually better off avoiding implicit unwrapping and should use explicit unwrapping through if let and guard let. When you’re certain it’s safe, use explicit force-unwrapping via !.

Questions? Comments?

If you have questions or comments about this post, please feel free to follow up on this related thread in the Swift forum.