Swift, How to Implement Hashable Protocol Based on Object Reference

Swift, how to implement Hashable protocol based on object reference?

If you are working with classes and not structs, you can use the ObjectIdentifier struct. Note that you also have to define == for your class in order to conform to Equatable (Hashable requires it). It would look something like this:

class MyClass: Hashable { }

func ==(lhs: MyClass, rhs: MyClass) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}

class MyClass: Hashable {
var hashValue: Int {
return ObjectIdentifier(self).hashValue
}
}

Swift: Using ObjectID of class for hashable protocol results in random behaviour in set.contains method. What is wrong with the code?

Simply put, Set relies on func hash(into hasher: inout Hasher) and ==. It is invalid to have an unmatched pair of these. In your case, your equality is value-based (dependant upon self.number), whereas your hash is identity based. This isn't legal.

Your mySet.contains(secondNumber1) line is failing because secondNumber2 might have a hash collision with number1. Whether a collision occurs or not is undefined, because Swift uses a random seed to defend against hash-flood DDoS attacks. If a hash collision does occur, then your equality operator (==) falsely identifies as number1 as a match for secondNumber1

Instead, what you could do is implement a wrapper struct that implements equality and hashing based on an object's identity. The object itself could have its own value-based equality and hash, for other purposes.

struct IdentityWrapper<T: AnyObject> {
let object: T

init(_ object: T) { self.object = object }
}

extension IdentityWrapper: Equatable {
static func == (lhs: IdentityWrapper, rhs: IdentityWrapper) -> Bool {
return lhs.object === rhs.object
}
}

extension IdentityWrapper: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self.object))
}
}

Using the IdentityWrapper in a set requires you to manually wrap objects before interacting with the set. It's performant (since struct don't need any array allocation), and most likely the struct is entirely inlined anyway, but it can be a little annoying. Optionally, you could implement a struct IdentitySet<T> which just wraps a Set<IdentityWrapper<T>>, which tucks away the wrapping code.

class MyClass: Hashable {
var number: Int

init(_ number: Int) {
self.number = number
}

// Value-based equality
static func == (lhs: MyClass, rhs: MyClass) -> Bool {
return lhs.number == rhs.number
}

// Value-based hashing
func hash(into hasher: inout Hasher) {
hasher.combine(self.number)
}
}

var mySet: Set<IdentityWrapper<MyClass>> = []

let number1 = MyClass(1)
let secondNumber1 = MyClass(1)

number1 == secondNumber1 // true: integer values are equal, so are the wrapping classes
number1 === secondNumber1 // false: two different instances

mySet.insert(IdentityWrapper(number1))

print(mySet.contains(IdentityWrapper(number1))) // true
print(mySet.contains(IdentityWrapper(secondNumber1))) // false

Swift 5.+ - Making a class hashable?

Here is an example for using class-based model in the described use-case. Tested with Xcode 11.4

class Stuff: Hashable, Equatable {
static func == (lhs: Stuff, rhs: Stuff) -> Bool {
lhs.title == rhs.title
}

func hash(into hasher: inout Hasher) {
hasher.combine(title)
}

var title: String = ""
}

struct StaffView: View {
let listOfStaff: [Stuff]

var body: some View {
ScrollView {
ForEach(listOfStaff, id: \.self) { stuff in
Text(stuff.title)
}
}
}
}

Make a swift protocol conform to Hashable

Deriving the protocol from Hashable and using a type eraser might help here:

protocol SomeLocation: Hashable {
var name: String { get }
var coordinates: Coordinate { get }
}

struct AnyLocation: SomeLocation {
let name: String
let coordinates: Coordinate

init<L: SomeLocation>(_ location: L) {
name = location.name
coordinates = location.coordinates
}
}

You then can simply declare the protocol conformance on the structs, and if Coordinate is already Hashable, then you don't need to write any extra hashing code code, since the compiler can automatically synthesize for you (and so will do for new types as long as all their properties are Hashable:

struct ShopLocation: SomeLocation, Decodable {
var name: String
var coordinates: Coordinate
}

struct CarLocation: SomeLocation, Decodable {
var name: String
var coordinates: Coordinate
}

If Coordinate is also Codable, then you also can omit writing any code for the encoding/decoding operations, the compile will synthesize the required methods (provided all other properties are already Codable).

You can then use the eraser within the annotation class by forwardingn the initializer constraints:

final class LocationAnnotation: NSObject, MKAnnotation {   
let location: AnyLocation

init<L: SomeLocation>(location: L) {
self.location = AnyLocation(location)
super.init()
}

override var hash: Int {
location.hashValue
}

override func isEqual(_ object: Any?) -> Bool {
(object as? LocationAnnotation)?.location == location
}
}

What is the use of hashable protocol in swift4?

To make an object conform to Hashable we need to provide a hashValue property that will return a unique, consistent number for each instance.
The Hashable protocol inherits from Equatable, so you may also need to implement an == function.

Note: If two objects compare as equal using == they should also generate the same hash value, but the reverse isn’t true – hash collisions can happen.

Before Swift 4.1, conforming to Hashable was complex because you needed to calculate a hashValue property by hand.
In Swift 4.1 this improved so that hashValue could be synthesized on your behalf if all the properties conform to Hashable .
Swift 4.2 introduces a new Hasher struct that provides a randomly seeded, universal hash function to make all our lives easier. Refer for more

Conforming to Hashable protocol?

You're missing the declaration:

struct DateStruct: Hashable {

And your == function is wrong. You should compare the three properties.

static func == (lhs: DateStruct, rhs: DateStruct) -> Bool {
return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day
}

It's possible for two different values to have the same hash value.

Creating a protocol that represents hashable objects that can be on or off

I don't know if I'd necessarily make the OnOffRepresentable protocol inherit from Hashable. It doesn't seem like something that you'd want to be represented as on or off must also be hashable. So in my implementation below, I add the Hashable conformance to the type erasing wrapper only. That way, you can reference OnOffRepresentable items directly whenever possible (without the "can only be used in a generic constraint" warning), and only wrap them inside the HashableOnOffRepresentable type eraser when you need to place them in sets or use them as dictionary keys.

protocol OnOffRepresentable {
func isInOnState() -> Bool
func isInOffState() -> Bool
}

extension UISwitch: OnOffRepresentable {
func isInOnState() -> Bool { return on }
func isInOffState() -> Bool { return !on }
}

extension UIButton: OnOffRepresentable {
func isInOnState() -> Bool { return selected }
func isInOffState() -> Bool { return !selected }
}

struct HashableOnOffRepresentable : OnOffRepresentable, Hashable {

private let wrapped:OnOffRepresentable
private let hashClosure:()->Int
private let equalClosure:Any->Bool

var hashValue: Int {
return hashClosure()
}

func isInOnState() -> Bool {
return wrapped.isInOnState()
}

func isInOffState() -> Bool {
return wrapped.isInOffState()
}

init<T where T:OnOffRepresentable, T:Hashable>(with:T) {
wrapped = with
hashClosure = { return with.hashValue }
equalClosure = { if let other = $0 as? T { return with == other } else { return false } }
}
}

func == (left:HashableOnOffRepresentable, right:HashableOnOffRepresentable) -> Bool {
return left.equalClosure(right.wrapped)
}

func == (left:HashableOnOffRepresentable, right:OnOffRepresentable) -> Bool {
return left.equalClosure(right)
}

var toggleToLabelMapper: [HashableOnOffRepresentable : UILabel] = [:]

let anySwitch = HashableOnOffRepresentable(with:UISwitch())
let anyButton = HashableOnOffRepresentable(with:UIButton())

var switchLabel:UILabel!
var buttonLabel:UILabel!

toggleToLabelMapper[anySwitch] = switchLabel
toggleToLabelMapper[anyButton] = buttonLabel

How to implement the Hashable Protocol in Swift for an Int array (a custom string struct)


Update

Martin R writes:

As of Swift 4.1, the compiler can synthesize Equatable and Hashable
for types conformance automatically, if all members conform to
Equatable/Hashable (SE0185). And as of Swift 4.2, a high-quality hash
combiner is built-in into the Swift standard library (SE-0206).

Therefore there is no need anymore to define your own hashing
function, it suffices to declare the conformance:

struct ScalarString: Hashable, ... {

private var scalarArray: [UInt32] = []

// ... }

Thus, the answer below needs to be rewritten (yet again). Until that happens refer to Martin R's answer from the link above.


Old Answer:

This answer has been completely rewritten after submitting my original answer to code review.

How to implement to Hashable protocol

The Hashable protocol allows you to use your custom class or struct as a dictionary key. In order to implement this protocol you need to

  1. Implement the Equatable protocol (Hashable inherits from Equatable)
  2. Return a computed hashValue

These points follow from the axiom given in the documentation:

x == y implies x.hashValue == y.hashValue

where x and y are values of some Type.

Implement the Equatable protocol

In order to implement the Equatable protocol, you define how your type uses the == (equivalence) operator. In your example, equivalence can be determined like this:

func ==(left: ScalarString, right: ScalarString) -> Bool {
return left.scalarArray == right.scalarArray
}

The == function is global so it goes outside of your class or struct.

Return a computed hashValue

Your custom class or struct must also have a computed hashValue variable. A good hash algorithm will provide a wide range of hash values. However, it should be noted that you do not need to guarantee that the hash values are all unique. When two different values have identical hash values, this is called a hash collision. It requires some extra work when there is a collision (which is why a good distribution is desirable), but some collisions are to be expected. As I understand it, the == function does that extra work. (Update: It looks like == may do all the work.)

There are a number of ways to calculate the hash value. For example, you could do something as simple as returning the number of elements in the array.

var hashValue: Int {
return self.scalarArray.count
}

This would give a hash collision every time two arrays had the same number of elements but different values. NSArray apparently uses this approach.

DJB Hash Function

A common hash function that works with strings is the DJB hash function. This is the one I will be using, but check out some others here.

A Swift implementation provided by @MartinR follows:

var hashValue: Int {
return self.scalarArray.reduce(5381) {
($0 << 5) &+ $0 &+ Int($1)
}
}

This is an improved version of my original implementation, but let me also include the older expanded form, which may be more readable for people not familiar with reduce. This is equivalent, I believe:

var hashValue: Int {

// DJB Hash Function
var hash = 5381

for(var i = 0; i < self.scalarArray.count; i++)
{
hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i])
}

return hash
}

The &+ operator allows Int to overflow and start over again for long strings.

Big Picture

We have looked at the pieces, but let me now show the whole example code as it relates to the Hashable protocol. ScalarString is the custom type from the question. This will be different for different people, of course.

// Include the Hashable keyword after the class/struct name
struct ScalarString: Hashable {

private var scalarArray: [UInt32] = []

// required var for the Hashable protocol
var hashValue: Int {
// DJB hash function
return self.scalarArray.reduce(5381) {
($0 << 5) &+ $0 &+ Int($1)
}
}
}

// required function for the Equatable protocol, which Hashable inheirits from
func ==(left: ScalarString, right: ScalarString) -> Bool {
return left.scalarArray == right.scalarArray
}

Other helpful reading

  • Which hashing algorithm is best for uniqueness and speed?
  • Overflow Operators
  • Why are 5381 and 33 so important in the djb2 algorithm?
  • How are hash collisions handled?

Credits

A big thanks to Martin R over in Code Review. My rewrite is largely based on his answer. If you found this helpful, then please give him an upvote.

Update

Swift is open source now so it is possible to see how hashValue is implemented for String from the source code. It appears to be more complex than the answer I have given here, and I have not taken the time to analyze it fully. Feel free to do so yourself.



Related Topics



Leave a reply



Submit