오랜만에 Swift UI 공식 튜토리얼을 보기 시작하다 Landmark라는 구조체를 작성하면서 Hashable 프로토콜에 대해 문서를 읽고 정리해봤다.
Hashable은 Swift에서 해시 함수를 통해 해시 값으로 변환할 수 있게 해주는 프로토콜이다. 따라서 Hashable을 채택한 타입은 식별자 역할을 할 수 있고 Set, Dictionary에서 하나의 키가 될 수 있음을 의미한다.
public protocol Hashable : Equatable {
var hashValue: Int { get }
func hash(into hasher: inout Hasher)
}
추가로 Hashable 프로토콜은 Equatable 프로토콜을 채택하고 있는 것을 볼 수 있다.
Equatable을 채택하는 유형은 == 연산자를 사용해 동일성을 비교하거나 != 연산자를 사용해 부등성을 비교할 수 있다. Swift 표준 라이브러리 대부분의 기본 유형은 Equatable을 준수한다.
기본 유형이라고 하면 Int, String, Double.. 등 비교가 가능한 모든 것들이라고 생각할 수 있다. 아무런 추가 작성 없이 간단하게 == 연산을 할 수 있는 이유가 바로 Equatable을 채택하고 있기 때문이다.
Hashable이 Equatable을 채택 하고있는, 어쩌면 할 수 밖에 없는 이유는 두 해시 값을 비교할 수 있어야하기 때문이 아닐까?
추가로 Flutter을 사용해서 개발경험이 있는 사람이라면 Equatable이라는 패키지를 자연스럽게 사용해봤을 수 있는데, Swift의 Hashable + Equatable과 비슷한 역할을 하고있다고 볼 수 있겠다.
위에서 기본 유형들은 Hashable을 기본적으로 채택하고 있다고 설명했다. 그러면 커스텀 유형은 어떻게 Hashable을 채택해야할까?
구조체의 경우 값타입이다. 즉, 그 자체로 값이기 때문에 name과 age가 같으면 같은 값으로 취급한다. 따라서 따로 hash함수나 ==연산자를 구현하지 않아도 바로 hash연산이 가능하기 때문에 따로 구현할 필요가 없다.
struct Person: Hashable {
var name: String
var age: Int
}
클래스는 참조타입이기 때문에 인스턴스는 값이 아닌 메모리 주소를 가르키고 있다. 따라서 두 클래스를 비교하기 위한 == 연산자와 hash연산을 어떻게 할건지 구현을 해줘야한다. 각 인스턴스는 메모리 주소로 고유하게 식별이 가능하지만 클래스의 내용 따라 고유한 해시값을 제공하기 위함이다.
class Person: Hashable {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.name = age
}
// Equatable
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
// Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(age)
}
}
열거형은 연관값이 있는 경우와 없는 경우로 나뉜다. 연관값이 없는 경우에는 채탱하지 않아도 자동으로 구현되지만 연관값이 있는 경우에 Hashable을 채택하고 연관값의 타입까지 Hashable을 채택하고 있어야 한다.
// 연관값이 없음
enum Framework {
case react,
}
// 연관값이 있음, String은 Hashable을 채택하고 있음
enum Framework: Hashable {
case react(madeBy: String)
}