Make a Swift dictionary where the key is "Type"?
Asked Answered
G

4

29

I'm trying to do this sort of thing ..

static var recycle: [Type: [CellThing]] = []

but - I can't :)

enter image description here

Undeclared type 'Type'

In the example, CellThing is my base class, so A:CellThing, B:CellThing, C:CellThing and so on. The idea is I would store various A A A, B B, C C C C in the dictionary arrays.

How to make a "Type" (ideally I guess, constrained to CellThing) be the key in a Swift dictionary?

I appreciate I could (perhaps?) use String(describing: T.self), but that would make me lose sleep.


Here's a use case, envisaged code would look something like this ...

@discardableResult class func make(...)->Self {
  return makeHelper(...)
  }

private class func makeHelper<T: CellThing>(...)->T {
  let c = instantiateViewController(...) as! T
  return c
  }

So then something like ...

static var recycle: [Type: [CellThing]] = []

private class func makeHelper<T: CellThing>(...)->T {
  let c = instantiateViewController(...) as! T

  let t = type whatever of c (so, maybe "A" or "B")
  recycle[t].append( c )

  let k = recycle[t].count
  print wow, you have k of those already!

  return c
  }
Grisby answered 25/2, 2017 at 17:48 Comment(12)
Did you try CellThing.selfIngressive
Are types hashable?Doane
@Doane - I appreciate I might have to make CellThing hashable. Is that actually the issue (and the error is just oddball) ??Grisby
Yes. It is looking for a class named Type, but there isn't one. I am able to use a class as the key for a non-static variable: var recycle: [UIView.self: [String]] = [] // no compiler errorsIngressive
What is the type of "a type" in Swift ??? if I have let tt = type(of: s) ... what the heck type is "tt" ????Grisby
@JoeBlow Depends on what the static type of s is. For example, if s is a string, tt will be String.Type. That's the type of a type.Armament
@Hamish. That is deep. So would it be "CellThing.Type" .... the dictionary could be [ CellThing.Type : [CellThing] ] ...........???Grisby
@JoeBlow In theory, yes. But Swift metatype types (CellThing.Type) aren't Hashable, so you cannot use them directly as the key type of a dictionary. As I demonstrate below, you can use a wrapper type to achieve the same effect though :)Armament
That's quite deep, even if we made "CellThing" hashable .. easy enough .. would the metatype CellThing.Type in fact be hashable?? :OGrisby
@JoeBlow No, it wouldn't. An instance of CellThing being Hashable wouldn't allow the type to become Hashable (as it currently stands, metatype types cannot adopt protocols anyway).Armament
Have you tried AnyHashable?Asbestosis
I also don't see how AnyHashable would help; it can only wrap Hashable things, but metatype types aren't Hashable.Armament
A
41

Unfortunately, it's currently not possible for metatype types to conform to protocols (see this related question on the matter) – so CellThing.Type does not, and cannot, currently conform to Hashable. This therefore means that it cannot be used directly as the Key of a Dictionary.

However, you can create a wrapper for a metatype, using ObjectIdentifier in order to provide the Hashable implementation. For example:

/// Hashable wrapper for a metatype value.
struct HashableType<T> : Hashable {

  static func == (lhs: HashableType, rhs: HashableType) -> Bool {
    return lhs.base == rhs.base
  }

  let base: T.Type

  init(_ base: T.Type) {
    self.base = base
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(ObjectIdentifier(base))
  }
  // Pre Swift 4.2:
  // var hashValue: Int { return ObjectIdentifier(base).hashValue }
}

You can then also provide a convenience subscript on Dictionary that takes a metatype and wraps it in a HashableType for you:

extension Dictionary {
  subscript<T>(key: T.Type) -> Value? where Key == HashableType<T> {
    get { return self[HashableType(key)] }
    set { self[HashableType(key)] = newValue }
  }
}

which could then use like so:

class CellThing {}
class A : CellThing {}
class B : CellThing {}

var recycle: [HashableType<CellThing>: [CellThing]] = [:]

recycle[A.self] = [A(), A(), A()]
recycle[B.self] = [B(), B()]

print(recycle[A.self]!) // [A, A, A]
print(recycle[B.self]!) // [B, B]

This should also work fine for generics, you would simply subscript your dictionary with T.self instead.


Unfortunately one disadvantage of using a subscript with a get and set here is that you'll incur a performance hit when working with dictionary values that are copy-on-write types such as Array (such as in your example). I talk about this issue more in this Q&A.

A simple operation like:

recycle[A.self]?.append(A())

will trigger an O(N) copy of the array stored within the dictionary.

This is a problem that is aimed to be solved with generalised accessors, which have been implemented as an unofficial language feature in Swift 5. If you are comfortable using an unofficial language feature that could break in a future version (not really recommended for production code), then you could implement the subscript as:

extension Dictionary {
  subscript<T>(key: T.Type) -> Value? where Key == HashableType<T> {
    get { return self[HashableType(key)] }
    _modify {
      yield &self[HashableType(key)]
    }
  }
}

which solves the performance problem, allowing an array value to be mutated in-place within the dictionary.

Otherwise, a simple alternative is to not define a custom subscript, and instead just add a convenience computed property on your type to let you use it as a key:

class CellThing {
  // Convenience static computed property to get the wrapped metatype value.
  static var hashable: HashableType<CellThing> { return HashableType(self) }
}

class A : CellThing {}
class B : CellThing {}

var recycle: [HashableType<CellThing>: [CellThing]] = [:]

recycle[A.hashable] = [A(), A(), A()]
recycle[B.hashable] = [B(), B()]

print(recycle[A.hashable]!) // [A, A, A]
print(recycle[B.hashable]!) // [B, B]
Armament answered 25/2, 2017 at 18:1 Comment(13)
So to recap, looking at the question title (1) the metatype there would in fact be: CellThing.Type (2) however, actually in Swift you cannot make a metatype hashable: so that's that. Who knew?Grisby
@JoeBlow CellThing.Type is the metatype type, which is the type of a type (and CellThing.self is a metatype value, an instance of that). You cannot currently directly conform a metatype type to a protocol. See this related question on the matter.Armament
Is ObjectIdentifier a sort of injection of NSObject-like behavior here? i.e., is its job to make up for the fact that we didn't inherit from NSObject?Doane
@Doane Metatype values are just pointers to the underlying metatype information for a given type – ObjectIdentifier simply uses those pointer values (you can see in the source that it just does a cast to Builtin.RawPointer) to give you a hashValue implementation (and equality through a simple identity comparison).Armament
Right, and I'm saying: If this were Objective-C we wouldn't have this problem because Class inherits from NSObject, so ObjectIdentifier is sort of giving us a way to make a Swift metatype act just enough like that...?Doane
@Doane Yeah exactly – since Swift metatype types aren't fully fledged types (cannot conform to protocols), we have to wrap them up in order to get that functionality, allowing them to hashed just like an Objective-C Class. Although obviously they don't benefit from the the various Obj-C runtime features that Class does.Armament
Trying this approach to use meta type of a class as Dictionary keys I get following compile error: Error:(41, 49) '>' is not a postfix unary operator pointing to this line var recycle: [Metatype<CellThing>: [CellThing]] = [:]Constant
@Constant Hmm, I seem to recall that was a bug in an earlier version of the language (what version are you using?), but compiles fine for me in Swift 3.1 and Swift 4. Regardless, one workaround for that bug was to expand the type, i.e var recycle: Dictionary<Metatype<CellThing>, [CellThing]> = [:], or define a typealias for Metatype<CellThing> and then use that as the dictionary Key type.Armament
Nevermind! My syntax was messed up: var viewDelegates = [Metatype<MyViewDelegate>: MyViewDelegate] = [:] %] (Using Swift 3.1 here, too).Constant
I'm having a problem related to this: #44454351Constant
{And notice the new comment by sevensevens below .. :O }Grisby
I apologize for bumping such an old thread, but I was just wondering, is there a way to make your answer work if CellThing was not a base class, but instead a protocol? When I try it Swift tells me cannot subscript a value of type '[HashableType<CellThing> : [CellThing]]' with an argument of type 'A.Type' print(recycle[A.self]!)Seise
@Seise thanks for your question. I'm having the same problem. Did you find the solution?Coadjutor
F
2

Since quite some time has passed since Hamish's answer - it's now also possible to use type as a key to a dictionary without constraining your type in any way or using a HashableType wrapper. It's possible because you can create an ObjectIdentifier for any metatype and because ObjectIdentifier is Hashable. If you define an extension for Dictionary:

extension Dictionary where Key == ObjectIdentifier {
    subscript<T>(key: T.Type) -> Value? {
        get { return self[ObjectIdentifier(T.self)] }
        set { self[ObjectIdentifier(T.self)] = newValue }
    }
}

Then you define your Dictionary and can use it like this:

var dictionary: [ObjectIdentifier: Any] = [:]
dictionary[A.self] = [A(), A(), A()]
dictionary[B.self] = [B(), B(), B()]

This method works for everything that has a metatype: protocol, struct, class, enum. Here is full example, including how to define a generic protocol conformance constraint on a custom implementation.

extension Dictionary where Key == ObjectIdentifier {
    subscript<T>(key: T.Type) -> Value? {
        get { return self[ObjectIdentifier(T.self)] }
        set { self[ObjectIdentifier(T.self)] = newValue }
    }
}

protocol MyProtocol {
    
}

struct MyStruct {
    let data = "struct"
}

class MyClass: MyProtocol {
    let data = "class"
}

enum MyEnum: String {
    case myCase
}

func example() {
    var dictionary: [ObjectIdentifier: Any] = [:]
    
    dictionary[MyProtocol.self] = "MyProtocol"
    dictionary[MyProtocol.Protocol.self] = "MyProtocol Protocol"
    dictionary[MyStruct.self] = MyStruct()
    dictionary[MyClass.self] = MyClass()
    dictionary[MyEnum.self] = MyEnum.myCase
    
    print(String(describing: dictionary[MyProtocol.self]!))          // "MyProtocol"
    print(String(describing: dictionary[MyProtocol.Protocol.self]!)) // "MyProtocol Protocol"
    print(String(describing: dictionary[MyStruct.self]!))            // MyProject.MyStruct(data: "struct")
    print(String(describing: dictionary[MyClass.self]!))             // MyProject.MyClass
    print(String(describing: dictionary[MyEnum.self]!))              // myCase
}

// It might be useful to constrain what can be put into a dictionary
// based on protocol conformance or others:

protocol Resource {
    
}

class MyResource: Resource {
    let message = "Swift is cool"
}

class ResourceStorageByType {
    var dictionary: [ObjectIdentifier: Any] = [:]
    
    func add<T: Resource>(_ item: T) {
        dictionary[T.self] = item
    }
    
    func get<T: Resource>(_ type: T.Type) -> T? {
        return dictionary[T.self] as? T
    }
}

func example2() {
    let storage = ResourceStorageByType()
    storage.add(MyResource())
    if let retrievedResource = storage.get(MyResource.self) {
        print(retrievedResource.message) // Swift is cool
    }
}
Friede answered 26/2 at 17:36 Comment(0)
R
1

If you extend the Dictionary type you can use the already defined generic Key directly.

extension Dictionary {
    // Key and Value are already defined by type dictionary, so it's available here
    func getSomething(key: Key) -> Value {
        return self[key]
    }
}

This works because Dictionary already has generics Key and Value defined for it's own use.

Radom answered 24/7, 2019 at 14:0 Comment(3)
seven, thanks - I'm .. not actually sure if this solves the problem, as, I no longer understand the question I wrote :) Will have to think about it. And ask Hamish :)Grisby
@Grisby - I read the question as how can I write something that takes a generic and operates on a dictionary. The easiest way (for me at least) is just to extend dictionary.Radom
@Radom Not quite – the question is how to use a metatype as a dictionary key, for example how you would write something like [String.self: "hello"] or dict[String.self] = "hello". That being said, the idea of using a dictionary extension gave me an idea of how you could define a convenience subscript to deal with boxing the metatype in a HashableType for you :)Armament
A
-5

Hope AnyHashable helps. But It appeared in Xcode 8.0

You can do something like:

var info: [AnyHashable : Any]? = nil
Asbestosis answered 8/12, 2017 at 12:22 Comment(1)
still, Type is not HashableExtenuate

© 2022 - 2024 — McMap. All rights reserved.