Hashable?

  • 정수 해시 값을 제공하고 Dictionary의 키가 될 수 있는 타입입니다.
    (Dictionary의 key value는 Hashable 프로토콜을 만족하는 타입이여야 합니다.)
  • 다르게 말하면 value가 hashable해야할 때, Hashable 프로토콜을 채택해서 구현해주면 hashable이 보장된 정수 해시 값을 제공합니다.
  • 구조체(Strucrue)의 저장프로퍼티는 모두 Hashable을 준수해야합니다.
  • 열거형(Enumeration)의 모든 연관값(associated values)은 모두 Hashable을 준수해야합니다.

Hashable이 필요한 경우?

Dictionary의 key에 들어갈 타입을 기본 데이터로 정해주면 Hashable을 만족하지 않는다는 에러는 만나지 않습니다. 왜냐하면 String,Int,Bool 등 기본 데이터 타입들은 모두 Hashable protocol을 만족하고 있기 때문이죠! 그런데 커스텀 타입을 key의 타입으로 정하면 에러를 만날 수 있습니다. 아래에 예제 코드로 설명해볼께요.

에러가 나는 예제 코드

// 에러가 나는 코드

class Participant{
 private var name : String
 private var age : Int
 
 init(_ name: String, _ age: Int){
 self.name = name
 self.age = age
 }
}

class Student: Participant{
  private var admissionYear : Int 
}

enum SpecializedField {
  case Math, ComputerScience
}

class Professor: Participant{
  private var specializedField: SpecializedField
}

class Lecture{
 private var participantList = [Participant:Bool]() // 딕셔너리
///... 이하 생략
}

만약 이렇게만 한다면


이런 에러를 만날 수 있습니다. 😢

에러 메세지를 보면 Dictionary에서 key의 타입으로 설정해준 Participant가 Hashable하지 않다고 나옵니다. 그렇다면 이제 Participant를 Hashable하도록 만들어주면 됩니다!

정상적으로 돌아가는 예제 코드


class Participant{
 private var name : String
 private var age : Int
 
 init(_ name: String, _ age: Int){
 self.name = name
 self.age = age
 }
}

extension Participant : Hashable{
    static func == (lhs: Participant, rhs: Participant) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age 
    }
    
    //* 여기
    func hash(into hasher: inout Hasher) {
       hasher.combine(name)
       hasher.combine(age)
    }
}

enum SpecializedField {
  case Math, ComputerScience
}

class Professor: Participant{
  private var specializedField: SpecializedField
}
extension Professor : Hashable{
    static func == (lhs: Professor, rhs: Professor) -> Bool {
        return lhs.specializedField == rhs.specializedField 
    }
    
    //* 여기
    func hash(into hasher: inout Hasher) {
       hasher.combine(specializedField)
    }
}

class Student : Participant{
  private var admissionYear : Int 
}

extension Student : Hashable{
    static func == (lhs: Student, rhs: Student) -> Bool {
        return lhs.admissionYear == rhs.admissionYear 
    }
    
    //* 여기
    func hash(into hasher: inout Hasher) {
       hasher.combine(admissionYear)
    }
}

class Lecture{
 private var participantList = [Participant:Bool]() // 딕셔너리
///... 이하 생략
}
  

방법은, 관련된 모든 클래스에서 Hashable을 구현해주면 됩니다. 관련 클래스의 모든 저장 프로퍼티를 Hashable하도록 만들어주면 돼요!

그럼 방법을 좀 더 살펴보죠.

Apple Developer Document에 있는 Dictionary에 들어가보니까 해쉬값을 제공하기 위한 인스턴스 메소드로

func hash(into hasher: inout Hasher)

가 있네요. Hashable을 준수하려면 hash(into:)가 필수라고 되어있습니다.

파라미터로 Hasher타입의 hasher를 받고있죠? 문서에는 필수 구성요소들에게 주어진 hasher를 공급함으로써 value의 필수 구성요소들을 해쉬하라고 하네요! (Hashes the essential components of this value by feeding them into the given hasher.)

이 일은 위에 정상적으로 돌아가는 예제 코드에서 extension으로 Hashable을 채택한 부분에 있는 hash(into:) 메소드('* 여기'표시한 곳 )가 해주는 일 같아요!

그럼 여기에 나오는 hasher는 뭘까요?

Hasher?

hasher는 구조체로, 해당 인스턴스의 구성요소를 결합할 때 사용한다고 해요.(The hasher to use when combining the components of this instance.)

그럼 여기에 나오는 combine은 뭘까요?

combine?

제네릭 인스턴스 메소드로 Hasher 구조체에서 value를 추가하는 메소드인데요.

mutating func combine<H>(_ value: H) where H : Hashable

해셔에 주어진 값을 추가하여 그 필수적인 부분을 해셔 상태로 혼합한다고 해요.(Adds the given value to this hasher, mixing its essential parts into the hasher state.)

이 일은 위에 정상적으로 돌아가는 예제 코드에서 '* 여기'표시한 곳 안에 haser.combine() 이 부분에서 해주는 일 같네요.

제 생각에는 단순히 저장 프로퍼티만 hash 값으로 만들어 주면 하나의 객체를 이루는 요소임을 알 수 없으니까 프로퍼티들을 묶어주는 것 같아요. (혹시 아시는 분 있다면 댓글 남겨주세요!)

이렇게 participantList에 key로 들어갈 수 있는 커스텀 타입의 모든 저장 프로퍼티를 Hashable 하도록 만들어주면 Participant 타입을 딕셔너리의 Key 타입으로 쓸 수 있게 됩니다!

참고한 자료

  1. How to conform to the Hashable protocol - Swift version: 5.1
  2. Dictionary - Apple Developer Document
  3. Hashable - Apple Developer Document
  4. hash(into:) - Apple Developer Document
  5. hasher - Apple Developer Document
  6. combine(_:) - Apple Developer Document

궁금한 점, 틀린 내용, 오타 지적, 오역 지적 등 피드백 환영합니다! 댓글로 남겨주세요!
😊 🙏

profile
개발 공부하고 있어요!

7개의 댓글

comment-user-thumbnail
2020년 3월 14일

자바의 hashCode의 동작이랑 유사하네요~
자바의 hashCode가 더 보기엔 좋은 느낌입니다 ㅋㅋㅋ

자바의 hashCode는 공통 상위 클래스인 Object 클래스에 정의되어 있어서 모든 클래스는 hashCode를 이미 내장하고 있어요!
그래서 hashCode는 이미 있고, 이를 재정의 하는 경우는 어떤 클래스의 객체가 같은 경우를 개발자가 직접 정의해주고 싶을 경우에 재정의 한답니다!

1개의 답글
comment-user-thumbnail
2020년 3월 14일

오 정리 잘해주셔서 해셔블에 대해 더 이해하게 된거같아요! 잘읽고 갑니다 !! 🌷

1개의 답글
comment-user-thumbnail
2021년 8월 25일

hasher.combine
제 생각에는 단순히 저장 프로퍼티만 hash 값으로 만들어 주면 하나의 객체를 이루는 요소임을 알 수 없으니까 프로퍼티들을 묶어주는 것 같아요. (혹시 아시는 분 있다면 댓글 남겨주세요!)

-- 저는 Swift 4.1버전이하에서 hashValue() 에서 XOR 연산으로 숫자(ex) 16777619), 해쉬값 합치던것을 Swift 4.1이후에는 combine(난수에다가 해쉬값 합치기) 으로 바꾼 것이라고 생각합니다. 너무 당연한가요..?
단순히 저장 프로퍼티만 hash 값으로 만들어 주면 하나의 객체를 이루는 요소임 <- 사실 이게 무슨말인지 정확히 이해하지 못했습니다.. ㅠㅠ

일단 제가 그렇게 생각한 이유는 옛날코드와 바뀐코드에서의 차이점이 그것뿐 이어서 그렇습니다.


// Swift 4.1 ver 이하 Hashable 작업
struct GridPoint {
    var x: Int
    var y: Int
}

extension GridPoint: Hashable {
    var hashValue: Int {
        return x.hashValue ^ y.hashValue &* 16777619
    }

    static func == (lhs: GridPoint, rhs: GridPoint) -> Bool {
        return lhs.x == rhs.x && lhs.y == rhs.y
    }

// Swift 4.1 ver 이후 Hashable 작업
extension Student : Hashable{
    static func == (lhs: Student, rhs: Student) -> Bool {
        return lhs.admissionYear == rhs.admissionYear 
    }
    
    //* 여기
    func hash(into hasher: inout Hasher) {
       hasher.combine(admissionYear)
    }
}

근데 그렇다고 그냥 넘어가기엔 찝찝해서 내부도 까봤습니다.

-Hasher의 combine 내부-

참고 : https://github.com/apple/swift/blob/main/stdlib/public/core/Hasher.swift

  public mutating func combine<H: Hashable>(_ value: H) {
    value.hash(into: &self)
  }

Hasher.comebine 코드를 까봤는데 이렇게 되있었습니다. value가 String이라고 예를 들어보겠습니다.

-String의 hash-

참고 : https://github.com/apple/swift/blob/main/stdlib/public/core/StringHashable.swift

  public func hash(into hasher: inout Hasher) {
    if _fastPath(self._guts.isNFCFastUTF8) {
      self._guts.withFastUTF8 {
        hasher.combine(bytes: UnsafeRawBufferPointer($0))
      }
      hasher.combine(0xFF as UInt8) // terminator
    } else {
      _gutsSlice._normalizedHash(into: &hasher)
    }
  }

String의 hash함수 입니다.
다시 hasher.combine(bytes: UnsafeRawBufferPointer($0))를 사용하니
hasher로 다시 돌아가봤습니다.

-hasher의 combine-

참고 : https://github.com/apple/swift/blob/main/stdlib/public/core/Hasher.swift

  public mutating func combine(bytes: UnsafeRawBufferPointer) {
    _core.combine(bytes: bytes)
  }

_core의 combine을 사용하네요.. _core 가 뭔지 들어가봤습니다.

  internal struct _Core {

    @inline(__always)
    internal init(seed: Int) {
      self.init(state: _State(seed: seed))
    }
}

뭔진 모르겠지만 seed가 있는 걸로보아 4.1 버전 이하에서 쓰이던 16777619 숫자같은 난수로 만든 무언가
에다가 combine(XOR같은 연산)을 해준것이 아닐까요. combine 내부보니 XOR처럼 단순하진 않고 뭐 복잡하고있었습니다만..

이것도 제 추측이어서 틀릴 수 있습니다. 예전에 쓰신 글이라서 혹시 지금은 어떻게 생각하고 계신지 궁금합니다.
제 의견에 코멘트 남겨주시면 감사하겠습니다!

1개의 답글