sam sam - 3 months ago 39
Swift Question

Swift 3 type inference confusion

I'm using macOS.

I have the following code. The only difference between 1, 2, 3, 4 and 5 is what is in the 'metrics' parameter.

let a = 20
let met = ["a": a]

// 1: This compiles.
_ = NSLayoutConstraint.constraints(withVisualFormat: "|[v1(a)]|", metrics: ["a": 20], views: ["v1": v1])

// 2: This fails with "Cannot convert value of type 'Int' to expected dictionary value type 'NSNumber'".
_ = NSLayoutConstraint.constraints(withVisualFormat: "|[v1(a)]|", metrics: ["a": a], views: ["v1": v1])

// 3: This fails with "Cannot convert value of type '[String: Int]' to expected argument type '[String: NSNumber]?'".
_ = NSLayoutConstraint.constraints(withVisualFormat: "|[v1(a)]|", metrics: met, views: ["v1": v1])

// 4: This compiles.
_ = NSLayoutConstraint.constraints(withVisualFormat: "|[v1(a)]|", metrics: met as [String: NSNumber]?, views: ["v1": v1])

// 5: This fails with "Cannot convert value of type 'Int' to expected dictionary value type 'NSNumber'".
_ = NSLayoutConstraint.constraints(withVisualFormat: "|[v1(a)]|", metrics: ["a": a] as [String: NSNumber]?, views: ["v1": v1])


Why does 1 compile, but 2 does not?

Why do 2 and 3 have different error messages?

Why does 4 compile, but 5 does not?

Answer

Updated Answer - For macOS

With Xcode 8 beta 6, Swift no longer implicitly bridges Swift value types to Foundation class types. Which means if a function is expecting an NSNumber and you pass it an Int variable, you will have to explicitly cast it to NSNumber. This is not necessary for an integer literal because Swift will still infer the type correctly.

Why does 1 compile, but 2 does not?

1 compiles because Swift is able to infer the type of 20 to be NSNumber, so ["a": 20] works as a [String: NSNumber].

2 doesn't compile, because the type of a is already established as Int, so you need to explicitly convert that to NSNumber. Xcode's fix-it suggests NSNumber(a), but sadly that doesn't compile. Use NSNumber(value: a) or a as NSNumber.

Why do 2 and 3 have different error messages?

For 2, you are providing a dictionary literal ["a": a] so Swift examines the types of each key and value to see if it matches the types the dictionary it expects. Since a is an Int and the value is a NSNumber, you get the error Cannot convert value of type 'Int' to expected dictionary value type 'NSNumber'. It wants you to provide the conversion.

For 3, you are providing a variable of type [String, Int]. Swift tells you that it can't convert that to [String, NSNumber]. It can, but not without an explicit cast due to the change in Xcode 8 beta 6.

Why does 4 compile, but 5 does not?

4 compiles because you are now providing the explicit cast to [String: NSNumber] that 3 lacked.

5 does not compile because again you are providing a dictionary literal and Swift examines each of the keys and values to make sure they are the right types. It will not convert the Int to an NSNumber without an explicit cast, so the error here is Cannot convert value of type 'Int' to expected dictionary value type 'NSNumber'. The point is that Swift will not cast the individual keys and values of a dictionary literal when you cast it to a dictionary type. You have to provide that cast directly for each one.


Previous Answer - For iOS

With Xcode 8 beta 6, the type of the argument metrics has changed to [String: Any]?. Now, the first 4 examples compile, and the 5th does not. Your first two questions are no longer valid. The only question left is:

Why does 4 compile, but 5 does not?

Statement 4 (met as [String: NSNumber]) compiles because met has type [String: Int] and Swift can cast [String: Int] to [String: NSNumber]. In this case, it is looking at the dictionary as a whole. Swift knows how to convert an Int to an NSNumber, but it won't do so without you asking it to do so explicitly. In this case, since you are presenting a dictionary of type [String: Int] and asking it to convert that to [String: NSNumber], you are asking it to convert the Int to an NSNumber.

In statement 5, you are casting a dictionary literal ["a": a] to a dictionary type as [String: NSNumber]. The error message is:

Cannot convert value of type 'Int' to expected dictionary value type 'NSNumber'

In this case, Swift is looking at the individual types, checking to see that "a" is a String and a is a NSNumber. Casting a dictionary literal to a type does not explicitly cast each key and value to the corresponding type. In that case, you are merely presenting them and saying that they are already that type. Due to a new change in Xcode 8 beta 6, Swift will no longer implicitly convert Swift value types to bridged Foundation types. So Swift wants you to explicitly convert the Int a to an NSNumber.

There are two ways to make Swift happy:

["a": NSNumber(value: a)] as [String: NSNumber]
["a": a as NSNumber] as [String: NSNumber]

Of course, now in both cases the dictionary literal can be inferred to be [String: NSNumber] so the cast in unnecessary.

Also, since metrics is now [String: Any], it makes no sense to convert ["a": a] to [String: NSNumber] when [String: Int] would do.