2

I'm trying to put two different generic types into a collection. In this example there are two arrays, one containing Ints and the other Strings.

let intArray = Array<Int>()
let stringArray = Array<String>()
let dict = [1:intArray, "one": stringArray]

The error reads Type of expression is ambiguous without more context.

So I tried specifying the Dictionary's type

let dict: [Hashable: Any] = [1:intArray, "one": stringArray]

This leads to two errors.

  • Using 'Hashable' as a concrete type conforming to protocol 'Hashable' is not supported.
  • Protocol 'Hashable' can only be used as a generic constraint because it has Self or associated type requirements

Adding import Foundation and using NSDictionary as the type works fine.

let dict: NSDictionary = [1:intArray, "one": stringArray]

But this should be possible in pure Swift too without using Foundation. What is the type the Dictionary has to have?

edit: This apparently has more to do with the type of the keys. They have to be of the same type, not just conform to Hashable.

let dict: [Int:Any] = [1:intArray, 2: stringArray]

This works. But is it possible to make the type of the value more precise? [Int:Array<Any>] does not work.

orkoden
  • 18,946
  • 4
  • 59
  • 50
  • 1
    The root cause of this is that Hashable conforms to Equatable, and Equatable has the `==(lhs: Self, rhs: Self)` requirement which is what is behind "Protocol 'Hashable' can only be used as a generic constraint because it has Self or associated type requirements". – BallpointBen Mar 07 '16 at 10:37
  • Also, you need `==` to resolve hash collisions, so this seems to be a valid complaint by the compiler. – BallpointBen Mar 07 '16 at 10:48
  • 1
    Possible duplicate of [Generic dictionary value type in Swift](http://stackoverflow.com/questions/24736612/generic-dictionary-value-type-in-swift) – 0x416e746f6e Mar 07 '16 at 12:18
  • @AntonBronnikov The relevant (but probably duplicate) linked thread covers generic _values_ in a dictionary with same-type keys, whereas this covers both values and keys, where the latter (generic keys) is the more complex part. I realize that this threas is possibly, however, a duplicate of [the one I linked to in the end of my answer](http://stackoverflow.com/questions/24119624/). – dfrib Mar 07 '16 at 13:20
  • @dfri, sorry, you are right. The OP's question is more broad. – 0x416e746f6e Mar 07 '16 at 13:51
  • The linked duplicate is a partial answer. I didn't realize it, but my question was about having multiple types as key and values for the dictionary. – orkoden Mar 07 '16 at 14:28

3 Answers3

1

What is the type the Dictionary has to have?

You may try:

let dict: [NSObject: Any] = [1: intArray, "one": stringArray]

The statement let dict: [Hashable: Any] = ... does not compile, because the type of the key of a Dictionary must be a concrete type conforming to Hashable, e.g. Int, String, etc. Hashable is not a concrete type.

The above suggestion works, because 1. NSObject is a concrete type (where you can instantiate objects from), 2. NSObject is a Hashable, and 3. because instances of subclasses of NSObjects will work here as well, and 4. the compiler can initialise NSObject subclasses from string and number literals.

If you don't like NSObject as the type of the key, you may create your own class or struct.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
1

Note that your first attempt let dict = [1:intArray, "one": stringArray] works if you include Foundation; yielding an NSDictionary (so no need to explicitly state this type).

The reason why we can have these kinds of, apparently, generic dictionaries when using Foundation is the implicit type conversion performed (behind the hood) by the compiler when bridging Swift native types to Foundation.

let intArray : [Int] = [10, 20, 30]
let stringArray : [String] = ["foo", "baz", "bar"]
let dict = [1:intArray, "onx": stringArray]

print(dict.dynamicType)
for e in dict {
    print(e.dynamicType, e.key.dynamicType, e.value.dynamicType)
}

/* __NSDictionaryI
   (AnyObject, AnyObject) __NSCFString _SwiftDeferredNSArray
   (AnyObject, AnyObject) __NSCFNumber _SwiftDeferredNSArray */

The keys as well as values in dict above are wrapped in type AnyObject; which can hold only reference (class) type objects; the compiler implicitly performs conversion of value types Int/String to Foundation reference types __NSCFNumber/__NSCFString. This is still an NSDictionary though; e.g. AnyObject itself does not conform to Hashable, so it can't be used as a key in a native Swift dictionary.


If you wish to create Swift-native "generic-key" dictionary, I'd suggest you create a wrapper (say a structure) that conforms to Hashable and that wraps over the underlying (various) key type(s). See e.g. (the somewhat outdated) thread

Community
  • 1
  • 1
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • Using Foundation and NSDictionary is already mentioned in my question. – orkoden Mar 07 '16 at 14:28
  • @orkoden I mainly explained why the `NSDictionary` seemingly works "generically" on Swift native types, when it in fact performs implicit type conversions to reference type behind the hood; the last part of my answer (referring [to this answer](http://stackoverflow.com/questions/24119624/)) points to an existing thread for solving this with Swift:s native `Dictionary` type. AntonBronnikov:s solution solves this neatly though, glad he could help you out! – dfrib Mar 07 '16 at 14:34
1

Elaborating on the answer from @RobNapier, here is a similar approach that uses enum for both, keys and values of the dictionary:

enum Key: Equatable, Hashable {
    case IntKey(Int)
    case StringKey(String)

    var hashValue: Int {
        switch self {
        case .IntKey(let value)     : return 0.hashValue ^ value.hashValue
        case .StringKey(let value)  : return 1.hashValue ^ value.hashValue
        }
    }

    init(_ value: Int)    { self = .IntKey(value) }
    init(_ value: String) { self = .StringKey(value) }
}

func == (lhs: Key, rhs: Key) -> Bool {
    switch (lhs, rhs) {
    case (.IntKey(let lhsValue),    .IntKey(let rhsValue))    : return lhsValue == rhsValue
    case (.StringKey(let lhsValue), .StringKey(let rhsValue)) : return lhsValue == rhsValue
    default: return false
    }
}

enum Value {
    case IntValue(Int)
    case StringValue(String)

    init(_ value: Int)    { self = .IntValue(value) }
    init(_ value: String) { self = .StringValue(value) }
}

var dict = [Key: Value]()

dict[Key(1)] = Value("One")
dict[Key(2)] = Value(2)
dict[Key("Three")] = Value("Three")
dict[Key("Four")] = Value(4)
Community
  • 1
  • 1
0x416e746f6e
  • 9,872
  • 5
  • 40
  • 68