글을 쓰게 된 계기

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을 채택한 자료형은 모두 SetDictionaryKey로 사용할 수 있다.
  • Swift의 표준 라이브러리의 많은 자료형은(String, Int, Float, Bool…) 기본적으로 Hash가 가능하다.
  • 또한, SetHashable한 것만 담을 수 있다.
  • 나같은 초보자들의 눈높이에 맞춰 쉽게 말하자면 Hash 값은 한 마디로 하나의 유일값이자 구분값이라고 이해하면 쉽다.
  • Set을 예로 들면, Set은 중복된 값은 들어갈 수 없다. 그렇기 때문에 이 안에 담기는 자료형은 Hashable을 채택하여 Hasher로 하여금 고정된 길이의 Hash값을 가지고 있어서 이를 통해 서로 같은지 다른지 판단할 수 있어야 한다는 것이다.
  • 마찬가지로 DictionaryKeyHashable 이어야 하는데, 여기서 Key는 중복되면 안되기 때문에 유일값이자 구분값을 통해 중복된지 아닌지 확인하는 것이다.
  • 비교적 최근 언어들은 모두 기본적으로 Hashable이 구현되어 있어서 따로 구현할 일은 없지만 사용자 정의 자료형에서는 Hashable을 구현해야만 한다.

Hashable의 구현

  • HashableEquatable을 상속받기 때문에 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가 성립하지는 않는다는 것이다.