글을 쓰게 된 계기
지난 Realm과 Swift의 Set의 만남에서 Hashable에 대해 언급했었다.
그럼 바로 Hashable에 대해서 알아보자.
Hashable이란?
Apple Developer Documentation의 Summary에 따르면
A type that can be hashed into Hasher to produce an interger hash value.
라고 한다. 대충 직역해보자면 Hasher에게 Hash되어 정수 Hash 값을 만들 수 있는 타입이다. 그러니까 도대체 이 Hash가 뭐냔말이다…이건 마치 치질의 한자어가 치질 치와 질병 병이 합쳐진 것과 같은 뫼비우스의 띠 같은 난해한 말이다.
Hash 함수는 임의의 길이를 갖는 임의의 데이터에 대해 고정된 길이의 데이터로 매핑하는 함수를 뜻한다고 한다. 이렇게 Hash 함수를 통해 나온 고정된 길이의 값을 Hash 값이라고 한다. 이러한 Hash 함수는 보통 보안, 암호 관련 분야에 쉽게 찾아볼 수 있다.
일단 이 글은 Swift의 Hashable에 대한 내용이므로 이 쪽으로 더 파고들진 않겠다.
일단 정리해보자면 Hash 함수에 의해 고정된 길이의 정수형으로 Hash될 수 있는 타입이라 할 수 있다.
Swift의 Hashable
Hashable을 채택한 자료형은 모두 Set와 Dictionary의 Key로 사용할 수 있다.
Swift의 표준 라이브러리의 많은 자료형은(String, Int, Float, Bool…) 기본적으로 Hash가 가능하다.
또한, Set은 Hashable한 것만 담을 수 있다.
나같은 초보자들의 눈높이에 맞춰 쉽게 말하자면 Hash 값은 한 마디로 하나의 유일값이자 구분값이라고 이해하면 쉽다.
Set을 예로 들면, Set은 중복된 값은 들어갈 수 없다. 그렇기 때문에 이 안에 담기는 자료형은 Hashable을 채택하여 Hasher로 하여금 고정된 길이의 Hash값을 가지고 있어서 이를 통해 서로 같은지 다른지 판단할 수 있어야 한다는 것이다.
마찬가지로 Dictionary의 Key도 Hashable 이어야 하는데, 여기서 Key는 중복되면 안되기 때문에 유일값이자 구분값을 통해 중복된지 아닌지 확인하는 것이다.
비교적 최근 언어들은 모두 기본적으로 Hashable이 구현되어 있어서 따로 구현할 일은 없지만 사용자 정의 자료형에서는 Hashable을 구현해야만 한다.
Hashable의 구현
Hashable은 Equatable을 상속받기 때문에 Equatable의 요구사항도 만족해야 한다.
또한, 구조체의 경우 모든 저장 프로퍼티는 반드시 Hashable을 채택해야 한다.
그리고 열거형의 경우 모든 연관값이 반드시 Hashable을 채택해야 한다.
위 요건에 충족하지 않는 경우에는 hash(into:) 함수를 통해 직접 구현해야 한다.
이제 코드를 통해서 알아보자.
struct SomeStruct: Hashable {
var name: String
var age: Int
}
let a = SomeStruct(name: "A", age: 0)
let b = SomeStruct(name: "B", age: 0)
if a == b {
print("같음")
} else {
print("다름")
}
// 다름
위의 코드를 보면 SomeStruct라는 사용자 정의 자료형을 구현했는데 name과 age는 각각 String, Int 형으로 이미 Hashable을 채택했기 때문에 hash(into:)를 구현하지 않아도 된다.
그러나 아래와 같은 경우에서는 hash(into:)를 직접 구현해야 한다.
struct SomeValue {
var value: Int
var anotherValue: String
}
struct SomeStruct: Hashable {
var name: String
var value: SomeValue
func hash(into hasher: inout Hasher) {
hasher.combine(value.value)
}
static func == (lhs: SomeStruct, rhs: SomeStruct) -> Bool {
return lhs.value.value == rhs.value.value
}
}
위 코드를 보면 SomeStruct라는 사용자 정의 자료형은 SomeValue라는 저장 프로퍼티를 가지고 있는데, 이는 Hashable한 저장 프로퍼티가 아니다.
때문에 SomeValue를 Hashable 하게 만들거나 SomeStruct에서 hash(into:)를 직접 구현하면 된다.
이 경우는 두 번째 case다.
이 경우 유일값으로 SomeValue 자료형의 value를 이용한다.
아래 결과를 보자.
let a = SomeStruct(name: "A", value: SomeValue(value: 0, anotherValue: "aa"))
let b = SomeStruct(name: "B", value: SomeValue(value: 0, anotherValue: "bb"))
if a == b {
print("같음")
} else {
print("다름")
}
/// 같음
SomeValue의 value가 유일값이자 구분값이 되었기 때문에 a와 b가 같다.
여기서 중요한 것은 a == b라고 함은 a.hashValue == a.hashValue와 같지만 반대로 a.hashValue == b.hashValue라고 하여 a == b가 성립하지는 않는다는 것이다.