4

We have just updated Xcode to 10.2 (hence iOS 12.2 SDK) and started seeing odd behavior with respect to the behavior of Swift Generics and Optionals. We have kept the Swift version at 4.2, so no Swift 5 update. The only change was updating to Xcode 10.2 from Xcode 10.1.

Here is a sample code that illustrates the oddities. The comments show what has changed between versions. Ideally, there shouldn't be any changes.

class Phone<T> {}

extension Phone {
    class func create(initial: T? = nil) -> Phone<T> {
        if let _ = initial { print("Regular: Unwrapping worked.") }
        return Phone()
    }
}

extension Phone where T == Void {
    class func create(initial: T? = nil) -> Phone<T> {
        if let _ = initial { print("T == Void: Unwrapping worked.") }
        return Phone()
    }
}

var phone: Phone<Int?> = Phone()
var phone2: Phone<Int?> = Phone()
var phone3: Phone<Int?> = Phone()

// unwrapping works iOS 12.1, doesn't work in 12.2
phone = Phone.create(initial: Optional(nil))

// unwrapping works iOS 12.1, doesn't work in 12.2
phone2 = Phone.create(initial: Optional<Int?>(nil))

// doesn't compile in iOS 12.1, unwrapping works in iOS 12.2
phone3 = Phone.create(initial: Optional<Int>(nil))

// doesn't compile in iOS 12.1, unwrapping doesn't work in 12.2 (uses the T == Void function)
let phone4 = Phone.create(initial: Optional(nil))

We have gone through the release notes of Xcode 10.2 but haven't spotted any changes around Optionals or Generics. It is really hard to understand what is causing this change in behavior between versions.

It is especially very interesting how phone2 and phone3 behave differently. There are few odd things happening in the code sample above, so the question is if anyone knows what might have caused the behavioral changes in this release?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Guven
  • 2,280
  • 2
  • 20
  • 34
  • 1
    I remembered that gereric optinals chaged in 4.2: https://developer.apple.com/documentation/xcode_release_notes/xcode_10_release_notes/swift_4_2_release_notes_for_xcode_10 Don't know if this is relatet but i thought i mention it… – errnesto Mar 28 '19 at 17:08

1 Answers1

3

This is due to SE-0213: Literal initialization via coercion which means that Optional(nil) is now treated as nil as Optional by the compiler. Previously with Optional(nil), you'd get a wrapped nil value e.g Int??.some(nil), however now you get just nil.

So for the following:

let phone: Phone<Int?> = Phone.create(initial: Optional(nil))

the compiler is treating it as:

let phone: Phone<Int?> = Phone.create(initial: nil as Optional)

which is equivalent to:

let phone: Phone<Int?> = Phone.create(initial: nil)

Because you've specified the generic parameter T to be Int?, the initial: parameter takes an Int??. Therefore by passing nil you're passing Int??.none, and therefore the unwrapping fails.

One way to restore the old behaviour is to specify .init explicitly in order to force the compiler to call the initialiser:

let phone: Phone<Int?> = Phone.create(initial: Optional.init(nil))

Now you're passing an Int??.some(nil) to the parameter and the unwrapping succeeds.

However I would question why you're dealing with doubly wrapped optionals in the first place – I would strongly encourage avoiding them unless absolutely necessary.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • This is great @Hamish, thanks so much. I see that SE-0213 is implemented in Swift 5, why do you think I am seeing this in a full Swift 4.2 codebase? Time for an Apple radar or am I missing something? (and I totally agree that the optional handling in the code is pretty bad, this is not the way it should be designed). – Guven Mar 29 '19 at 06:27
  • 1
    @Guven It's worth noting that SE-0213 was implemented in the Swift 5 compiler, which [you're using even if you have the "Swift Language Version" set to 4.2](https://stackoverflow.com/a/51283128/2976878). It appears that the main implementation of SE-0213 doesn't consider the compatibility mode the compiler is running in, so is always in effect even in Swift 4.2 or 4 mode. – Hamish Mar 29 '19 at 11:28
  • 1
    Considering it can subtly change the behaviour of code, IMO that this was the wrong decision, and it should have been guarded by a version check. Unfortunately now that Swift 5 has been released, it's a bit too late to change. – Hamish Mar 29 '19 at 11:29