文章

采用通用协议

确保自定类型遵从 Swift 协议,使它们更简单易用。

概览

在程序中使用自定类型进行数据建模时,你可能经常需要检查两个值是相同还是不同,或者某个值是否包含在值列表中。这项功能以及在集合中储存值或在字典中将值用作键的功能由两个相关的标准资源库协议 Equatable (英文)Hashable (英文) 进行管理。

  • 你可以使用等于 (==) 和不等于 (!=) 运算符来比较 Equatable 类型的不同实例。

  • Hashable 类型的实例可以将自己的值以数学方式缩减为单个整数,供集合和字典在内部使用以实现持续快速查询。

许多标准资源库类型都同时属于 Equatable 和 Hashable,包括字符串、整数、浮点值、布尔值,以及 Equatable 和 Hashable 类型的集合。以下示例中的 == 比较和 contains(_:) 方法调用取决于字符串和整数都属于 Equatable:

if username == "Arturo" {
    print("Hi, Arturo!")
}

let favoriteNumbers = [4, 7, 8, 9]
if favoriteNumbers.contains(todaysDate.day) {
    print("It's a good day today!")
}

遵从 EquatableHashable 协议非常简单明了,并让你可以更加轻松地在 Swift 中使用自定类型。因此建议你让所有的自定模型类别都遵从这两种协议。

自动遵从 Equatable 和 Hashable

只需在类型原始声明的同一文件中声明遵从这些协议,便可让许多自定类型成为 Equatable 和 Hashable 类型。在声明类型时将 EquatableHashable 添加到所采用协议的列表中后,编译器便会自动填充这两种协议的相关要求:

/// A position in an x-y coordinate system.
struct Position: Equatable, Hashable {
    var x: Int
    var y: Int
    
    init(_ x: Int, _ y: Int) {
        self.x = x
        self.y = y
    }
}

遵从 Equatable 后,你就能使用等于运算符 (==) 或不等于运算符 (!=) 来比较任意两个 Position 类型实例。

let availablePositions = [Position(0, 0), Position(0, 1), Position(1, 0)]
let gemPosition = Position(1, 0)

for position in availablePositions {
    if gemPosition == position {
        print("Gem found at (\(position.x), \(position.y))!")
    } else {
        print("No gem at (\(position.x), \(position.y))")
    }
}
// No gem at (0, 0)
// No gem at (0, 1)
// Gem found at (1, 0)!

遵从 Hashable 意味着你可以将多个位置储存在一个集合中,并快速检查你以前是否到访过某个位置,如下例中所示:

var visitedPositions: Set = [Position(0, 0), Position(1, 0)]
let currentPosition = Position(1, 3)

if visitedPositions.contains(currentPosition) {
    print("Already visited (\(currentPosition.x), \(currentPosition.y))")
} else {
    print("First time at (\(currentPosition.x), \(currentPosition.y))")
    visitedPositions.insert(currentPosition)
}
// First time at (1, 3)

除了简化你的代码外,这种自动遵从还能减少错误,因为在进行哈希和相等性测试时会自动包含你添加到自定类型中的所有新属性。只要类型是满足以下标准的结构或枚举,便符合自动遵从 EquatableHashable 的条件:

  • 如果是结构,其储存的“所有”属性都必须遵从 EquatableHashable

  • 如果是枚举,其关联的“所有”值都必须遵从 EquatableHashable。(如果是无关联值的枚举,那么即使没有声明采用这些协议,也会遵从 EquatableHashable。)

手动遵从 Equatable 和 Hashable

在以下情形中,你需要为类型手动实现 EquatableHashable 遵从性:

  • 类型不满足之前一部分中列出的标准。

  • 你想要自定类型的遵从性。

  • 你想要扩展在另一个文件或模块中声明的类型来实现遵从性。

class Player {
    var name: String
    var position: Position
    
    init(name: String, position: Position) {
        self.name = name
        self.position = position
    }
}

Player 类型是一个类,因此不符合自动合成 EquatableHashable 要求的条件。要让这个类遵从 Equatable 协议,请在扩展中声明遵从性并实现静态 == 运算符方法。在 == 方法的实现中比较各个重要属性的相等性:

extension Player: Equatable {
    static func ==(lhs: Player, rhs: Player) -> Bool {
        return lhs.name == rhs.name && lhs.position == rhs.position
    }
}

要让 Player 遵从 Hashable 协议,请在另一个扩展中声明遵从性,并实现 hash(into:) 方法。在 hash(into:) 方法中,对提供的哈希计算器使用每个重要属性调用 combine(_:) 方法:

extension Player: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(position)
    }
}

使用对 Equatable 和 Hashable 重要的所有属性

在实现 == 方法和 hash(into:) 方法时,应使用影响自定类型的两个实例是否被视为相等的所有属性。在上面的实现中,Player 类型在两个方法中都使用了 nameposition

如果你的类型包含不影响两个实例是否被视为相等的属性,请从 == 方法的比较中和 hash(into:) 的哈希处理中排除这些属性。例如,某个类型可能缓存一个高开销的计算值,以便这个值只需计算一次。如果你要比较该类型的两个实例,那么该计算值缓存与否不应影响两个实例的相等性,因此应从比较和哈希处理中排除缓存的值。

自定 NSObject 子类行为

NSObject 子类会继承对 EquatableHashable 协议的遵从性,其相等性基于实例恒等性。如果需要自定此行为,应覆盖 isEqual(_:) (英文) 方法和 hash (英文) 属性,而不是 == 运算符方法和 hashValue 属性。

extension MyNSObjectSubclass {
    override func isEqual(_ object: Any?) -> Bool {
        guard let other = object as? MyNSObjectSubclass
            else { return false }
        return self.firstProperty == other.firstProperty 
            && self.secondProperty == other.secondProperty
    }

    override var hash: Int {
        var hasher = Hasher()
        hasher.combine(firstProperty)
        hasher.combine(secondProperty)
        return hasher.finalize()
    }
}

如前一部分中所述,被视为相等的两个实例必须具有相同的哈希值。如果你覆盖其中一个声明,那么也必须覆盖另一声明,从而维持该保证。

另请参阅

数据建模

在结构和类之间做出选择

决定数据的储存方式和行为的建模方式。