Warning
This package requires Swift 5.9.2, which ships with Xcode 15.1. This package will fail to compile with Swift 5.9.0 (Xcode 15.0 and 15.0.1).
@Hashable
is a Swift macro for adding Hashable
conformance. It is particularly useful when synthesised conformance is not possible, such as with classes or a struct with 1 or more non-hashable properties.
The @Hashable
macro is applied to the type that will conform to Hashable
and the Hashed
macro is applied to each of the properties that should contribute to the Hashable
conformance.
import HashableMacro
/// A struct that uses the ``stringProperty`` and ``intProperty`` for `Hashable` conformance.
@Hashable
struct MyStruct {
// Any property that is hashable is supported.
@Hashed
let stringProperty: String
// Works on private properties, too.
@Hashed
private let intProperty: Int
// Non-decorated properties are ignored
let notHashableType: NotHashableType
}
All decorated properties are included in both the ==
and hash(into:)
implementations, ensuring the contract of Hashable
is upheld:
Two instances that are equal must feed the same values to
Hasher
inhash(into:)
, in the same order.
DocC documentation for HashableMacro is hosted by Swift Package Index. Most of the implementation can be seen in the Macros.swift file.
The @NotHashed
macro can be applied to properties that should not be included in the Hashable
conformance. If this macro is used to decorate a property the @Hashed
macro should not be used to decorate a property in the same type.
This can be useful for types that have a smaller number of non-hashable properties than hashable properties.
/// A struct that uses the ``stringProperty`` and ``intProperty`` for `Hashable` conformance.
@Hashable
struct MyStruct {
// Implicitly used for `Hashable` conformance
let stringProperty: String
// Implicitly used for `Hashable` conformance
private let intProperty: Int
// Explicitly excluded from `Hashable` conformance
@NotHashed
let notHashableType: NotHashableType
}
If the @Hashable
macro is added but no properties are decorated with @Hashed
or @NotHashed
then all stored properties will be used.
/// A struct that uses the ``stringProperty`` and ``intProperty`` for `Hashable` conformance.
@Hashable
struct MyStruct {
// Implicitly used for `Hashable` conformance
let stringProperty: String
// Implicitly used for `Hashable` conformance
private let intProperty: Int
// Implicitly excluded from `Hashable` conformance
var computedProperty: Bool {
intProperty > 0
}
}
One (fairly minor) advantage of this over adding Hashable
conformance without the macro is that you can see the code being produce via Right Click → Expand Macro.
When a type implements NSObjectProtocol
(e.g. it inherits from NSObject
) it should override hash
and isEqual(_:)
, not hash(into:)
and ==
. @Hashable
detects when it is attached to a type conforming to NSObjectProtocol
and will provide the hash
property and isEqual(_:)
function instead.
@Hashable
will also provide an isEqual(to:)
function that takes a parameter that matches Self
, which will also have an appropriately named Objective-C function.
import HashableMacro
@Hashable
final class Person: NSObject {
@Hashed
var name: String = ""
}
extension Person {
override var hash: Int {
var hasher = Hasher()
hasher.combine(self.name)
return hasher.finalize()
}
}
extension Person {
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Person else {
return false
}
guard type(of: self) == type(of: object) else {
return false
}
return self.isEqual(to: object)
}
@objc(isEqualToPerson:)
func isEqual(to object: Person) -> Bool {
return self.name == object.name
}
}
When the @Hashable
macro is added to a class the generated hash(into:)
function is marked final
. This is because subclasses should not overload ==
. There are many reasons why this can be a bad idea, but specifically in Swift this does not work because:
!=
is not part of theEquatable
protocol, but rather an extension onEquatable
, causing it to always use the==
implementation from the class that addsEquatable
conformance- It is possible to overload
!=
but this is still not a good idea because...
- It is possible to overload
- Anything that uses generics to compare the values, for example
XCTAssertEqual
, will use the==
implementation from the class that addsEquatable
conformance- It is possible to work around this by using a separate function, in a similar way to
NSObject
, which is then called from==
- It is possible to work around this by using a separate function, in a similar way to
If this is an issue for your usage you can pass finalHashInto: false
to the macro, but it will not attempt to call super
or use properties from the superclass.
This is not something the macro aims to solve.