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
  }
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Did you try CellThing.self – Mozahler Feb 25 '17 at 17:55
  • Are types hashable? – matt Feb 25 '17 at 17:56
  • @matt - I appreciate I might have to make CellThing hashable. Is that actually the issue (and the error is just oddball) ?? – Fattie Feb 25 '17 at 17:59
  • 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 errors – Mozahler Feb 25 '17 at 18:04
  • What is the type of "a type" in Swift ??? if I have **let tt = type(of: s)** ... what the heck type is "tt" ???? – Fattie Feb 25 '17 at 18:05
  • @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. – Hamish Feb 25 '17 at 18:06
  • @Hamish. That is deep. So would it be "CellThing.Type" .... the dictionary could be `[ CellThing.Type : [CellThing] ]` ...........??? – Fattie Feb 25 '17 at 18:07
  • @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 :) – Hamish Feb 25 '17 at 18:09
  • That's quite deep, even if we made "CellThing" hashable .. easy enough .. would the metatype `CellThing.Type` in fact be hashable?? :O – Fattie Feb 25 '17 at 18:10
  • @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). – Hamish Feb 25 '17 at 18:12
  • Have you tried AnyHashable? – Nuzhdin Vladimir Dec 08 '17 at 13:22
  • hi Zevs ! I often use AnyHashable, but, I do not immediately know if it solves the problem at hand here! W'd have to ask @Hamish :) – Fattie Dec 08 '17 at 16:46
  • 1
    I also don't see how `AnyHashable` would help; it can only wrap `Hashable` things, but metatype types aren't `Hashable`. – Hamish Dec 08 '17 at 18:43

3 Answers3

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]
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 1
    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? – Fattie Feb 25 '17 at 18:16
  • 1
    @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](http://stackoverflow.com/q/42286462/2976878) on the matter. – Hamish Feb 25 '17 at 18:19
  • 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? – matt Feb 25 '17 at 20:54
  • 2
    @matt 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](https://github.com/apple/swift/blob/master/stdlib/public/core/ObjectIdentifier.swift#L68) that it just does a cast to `Builtin.RawPointer`) to give you a `hashValue` implementation (and equality through a simple identity comparison). – Hamish Feb 25 '17 at 21:06
  • 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...? – matt Feb 25 '17 at 21:15
  • @matt 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. – Hamish Feb 25 '17 at 21:27
  • 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]] = [:]` – BadmintonCat Jun 09 '17 at 08:02
  • @BadmintonCat 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, [CellThing]> = [:]`, or define a `typealias` for `Metatype` and then use that as the dictionary `Key` type. – Hamish Jun 09 '17 at 08:19
  • Nevermind! My syntax was messed up: `var viewDelegates = [Metatype: MyViewDelegate] = [:]` %] (Using Swift 3.1 here, too). – BadmintonCat Jun 09 '17 at 08:22
  • I'm having a problem related to this: https://stackoverflow.com/questions/44453851/instantiating-classes-stored-in-metatype-dictionary – BadmintonCat Jun 09 '17 at 09:17
  • {And notice the new comment by sevensevens below .. :O } – Fattie Jul 24 '19 at 17:06
  • 2
    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]]' with an argument of type 'A.Type' print(recycle[A.self]!)` – Evert Mar 23 '20 at 23:38
  • 2
    @Evert thanks for your question. I'm having the same problem. Did you find the solution? – Adnan Zahid May 05 '20 at 23:36
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.

sevensevens
  • 1,703
  • 16
  • 27
  • 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 :) – Fattie Jul 24 '19 at 17:06
  • @Fattie - 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. – sevensevens Jul 24 '19 at 20:56
  • 2
    @sevensevens 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 :) – Hamish Jul 24 '19 at 22:55
-5

Hope AnyHashable helps. But It appeared in Xcode 8.0

You can do something like:

var info: [AnyHashable : Any]? = nil
Fattie
  • 27,874
  • 70
  • 431
  • 719
Nuzhdin Vladimir
  • 1,714
  • 18
  • 36