Reputation: 16558
How can I return [Hashable: Any]
where the concrete type implementing Hashable
is hidden?
Swift has two types of protocols: Those that can be used as types and those that can only be used as type constraints. While this is powerful, it is also unfortunate. It means that certain types of information hiding are not directly possible.
The only solution I could come up with is to use a thunk:
struct Hash: Hashable {
private let value: Any
private let equals: Hash -> Bool
init<H: Hashable>(_ h: H) {
self.value = h
self.hashValue = h.hashValue
self.equals = { ($0.value as! H) == h }
}
let hashValue: Int
}
func ==(lhs: Hash, rhs: Hash) -> Bool {
return lhs.equals(rhs)
}
Perhaps someday Swift will create thunks like this for us. That is after all one of the things compilers are for.
Is this the only way? Am I missing something?
Upvotes: 2
Views: 773
Reputation: 17556
The reason behind this is that swift protocols like Hashable
can be applied to all different types, not just classes.
Let's say we have two different types: a struct which takes up 8 bits of memory and a class which is a pointer to some chunk of memory on the heap.
struct HashyStruct : Equatable, Hashable {
let smallNumber: UInt16
var hashValue: Int {
return Int(smallNumber)
}
}
func ==(lhs: HashyStruct, rhs: HashyStruct) -> Bool {
return lhs.smallNumber == rhs.smallNumber
}
class HashyClass : Equatable, Hashable {
let number: UInt64
init(number: UInt64) {
self.number = number
}
var hashValue: Int {
return Int(number)
}
}
func ==(lhs: HashyClass, rhs: HashyClass) -> Bool {
return lhs.number == rhs.number
}
Both of these types are Hashable
, so why can't we have an dictionary like this:
let anyHashable: [Hashable:Any] = [HashyStruct(smallNumber: 5) : "struct", HashyClass(number: 0x12345678):"class"]
error: using 'Hashable' as a concrete type conforming to protocol 'Hashable' is not supported error: protocol 'Hashable' can only be used as a generic constraint because it has Self or associated type requirements
Dictionary
needs to be able to compare objects to each other to resolve conflicts where two things have the same hash value. How would it compare a HashyStruct
to a HashyClass
? It has no idea which function to call to compare the two objects. It doesn't know whether to call hashValue
on HashyStruct
or HashyClass
.
You can actually implement this if you want to do it dynamically at runtime. The "thunk" you implemented does have that sort of dynamic behavior but it comes with a performance hit that in most cases you don't need to take.
I'm assuming you know how hash tables are implemented as a fixed size array and the hash value is mapped to a limited number of buckets which can have conflicts.
Dictionary
allocates a chunk of memory to store it's objects; lets give it 4 portions of 64 bytes each (the size of a pointer on 64 bit machines).
| 64 bytes | 64 bytes | 64 bytes | 64 bytes |
When you add two items with hash values 1 and 6 = 2 % 4 you'll get the following hash table:
| empty | item 1 | item 2 | empty |
That's all well and good; we can fit any number of pointer objects into our table which will work for classes. But in swift we have structs - HashyStruct
only has 16 bits. If we gave the same amount of memory to a dictionary storing HashyStruct
s then we could store 16 instead of 4 items in our hash set.
| 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes |
As long as the compiler knows the size of the type we can have any type we want in a hash table. But when we have two different types...???
| 4 structs | item 1 | item 2 | empty |
You get a hash table which makes no sense. The compiler doesn't know the size of the items in the array so it can't index them.
A lot of what makes swift great is that you aren't tied to classes and objects allocated on the heap. It by default gives you good performance and you opt into dynamic behavior if you need it.
Upvotes: 1