4

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?

sam
  • 3,399
  • 4
  • 36
  • 51

2 Answers2

4

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.

Community
  • 1
  • 1
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • I'm using Xcode8b6. The type of the metrics parameter is '[String: NSNumber]?'. These error messages are from Xcode 8b6 so all of the questions are valid. – sam Aug 20 '16 at 17:18
  • Are you developing on macOS perhaps? It seems that iOS and macOS are different. – vacawama Aug 20 '16 at 17:30
  • Yes, I should have mentioned that. I am on macOS. I had no idea iOS and macOS were different. – sam Aug 20 '16 at 18:05
  • I guess I'm still confused. Why is there a difference in behavior between a dictionary literal and a dictionary variable when both have the same type? Why does the choice of literal vs variable matter? In 2/3, both have the type '[String: Int]'. And in 4/5, both have the type '[String: Int]' and the same explicit cast to '[String: NSNumber]?'. – sam Aug 20 '16 at 18:39
  • With a dictionary literal, Swift hasn't yet established the type of the literal. Consider `["a": 0]`. Is that `[String: Int]` or `[String: Double]`. If you assign it to a variable, Swift will have to pick one, and it defaults to `[String: Int]`, but if you pass that literal to a function that takes `[String, Double]` it will decide to treat the `0` as a `Double`. It doesn't convert an `Int` to a `Double`, it decides that the `0` is a valid `Double`. – vacawama Aug 20 '16 at 18:45
  • When you cast a dictionary literal, you provide more information to Swift about what that literal contains. So `["a": 0] as [String: Double]` tells Swift that all of the keys should be `String` and all of the values `Double` and it will interpret them that way if necessary, but it will not convert them. – vacawama Aug 20 '16 at 18:46
  • Once you assign a dictionary literal to a variable, the type gets **locked in** because Swift is strongly typed. So, a variable has a specific type `[String: Int]` and the ability to interpret the `0` as a `Double` is now lost. – vacawama Aug 20 '16 at 19:01
1

Swift 3 removed implicit casting between Swift and Foundation types (Int -> NSNumber in this case). let a = 20 gives a the Int type, which you manually need to cast to NSNumber using a as NSNumber. On the other hand, your first line compiles because 20 infers the expected NSNumber type.

Alternatively, you can give a the NSNumber type to begin with using

let a: NSNumber = 20

after which you can use it where NSNumber is expected.

I'm not sure why your fifth line doesn't compile - that might be a bug.

Tim Vermeulen
  • 12,352
  • 9
  • 44
  • 63