블로그 이미지

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift] Hashable 프로토콜
    Programming Language/Swift 2022. 7. 1. 16:42

      개발하다보면 Hashable 프로토콜을 정말 많이 사용하게 된다. Hashable과 옵셔널 값을 같이 사용하면서 생긴 궁금증도 있고, Hashable을 굉장히 많이 사용하지만 깊이 알고 있지 않은 상태로 사용하고 있는 것 같아 Hashable 이 어떤 프로토콜인지 좀 더 자세히 공부해보려고 한다.

    Hashing

    Hasing(해싱)은 주어진 key나 문자열을 다른 값으로 변환시키는 작업이다. 해싱을 통해 고정 길이의 더 짧은 key, value로 표현되고, 이 key-value를 사용해서 원래의 값을 더 쉽고 빠르게 찾을 수 있다.

     

    해싱은 데이터를 특정 정수 값으로 매핑하는 함수, 알고리즘을 사용한다. 즉 hash function을 사용해서 데이터에서 새로운 값을 만드는 것이다. 이 새로운 값을 hash value, 아니면 단순히 hash라고 한다.

    해싱을 사용한 방법 중 하나로 흔히 hash table을 본 적이 있을 것이다. Hash table은 키-값 쌍을 저장해서 이를 인덱스를 통해 접근할 수 있게 한다.

    위 그림을 보면 개발자가 저장하고 싶은 데이터의 (키, 값) 쌍이 있고, 키를 hash function의 입력값으로 해서 나온 출력값 hash를 hash table의 인덱스로 사용하고 있다.

     

    정리하자면 hashing은 데이터를 새로운 값으로 변환시키는 작업이고, 이 새로운 값을 hash라고 한다.

    Hashable

    Hashable은 프로토콜로, Hasher를 통해 정수 hash 값으로 해싱될 수 있는 타입을 의미한다.

    위에서 그린 그림을 Hashable을 대입해서 다시 그려봤다. Hashable한 타입은 Hasher를 통해 정수 hash 값으로 변환될 수 있다.

    HashableEquatable 프로토콜을 준수한다. Equatable도 프로토콜로, 값이 같은지 여부를 비교할 수 있는 타입이다.

    특징

    • 집합, 딕셔너리 키로 Hashable 프로토콜을 따르는 타입을 사용할 수 있다.
    • 표준 라이브러리의 많은 타입이 Hashable을 준수한다. (문자열, 정수, 실수, Bool, 집합 등)
    • 개발자가 생성한 커스텀 타입도 hashable할 수 있다.
    • 값을 hasing한다 : Hasher 타입의 hash function에 필요한 요소들을 제공해서 해시 값을 생성한다는 의미다.

    Hash 값 제공하기, hash(into:)

    hash(into:) 는 프로토콜을 채택하는 타입에서 꼭 구현해야 하는 required 인스턴스 메서드다. hash(into:)필수적인 요소들의 값을 주어진 hasher에 전달해서 해싱하는 함수다.

     

    파라미터의 hasher는 인스턴스의 요소들을 결합시킬 때 사용하는 hasher다. 이때 해싱에 사용되는 요소들은 == 메서드에서 값을 비교하기 위해 사용했던 요소들과 같은 요소가 되어야 한다.

    Hasher

    hash(into:) 메서드에서 사용하는 hasher의 타입은 Hasher다.

    Hasher는 집합과 딕셔너리에 사용되는 hash 함수로, 구조체다.

     

    Hasher는 임의의 byte sequence를 정수 hash 값으로 mapping할 때 사용된다. Mutating combine 메서드를 사용해서 hasher에 데이터를 추가로 줄 수도 있다.

    var hasher = Hasher()
    hasher.combine(23)
    hasher.combine("Hello")
    // 여기에서는 finalize 메서드를 사용했는데, `hash(into:)` 메서드를 사용할 때는 절대로 hasher에 finalize를 사용하면 안된다고 공식문서에 나와있다.
    let hashValue = hasher.finalize()

    그림으로 다시 정리하면 위와 같다.

    • Hashable 프로토콜을 채택해서 우리가 사용하고자 하는 타입이 Hashable하게 한다.
    • 타입 내부에 hash(into:) 메서드를 구현해서 Hasher 타입 hash function에 추가적인 값을 제공하고, hashing 한다.
    • 결과적으로 우리는 정수 hash 값을 갖게 된다.

    Automatically provided Hashable protocol conformance

    Swift는 여러 경우에 자동으로 Hashable protocol conformance를 제공한다. 이게 무슨 말이냐면 이 문법적 구현(synthesized implementation)을 사용해서 프로토콜 요구사항을 구현하기 위해 반복적인 boilerplate 코드를 작성할 필요가 없게 된다는 뜻이다.

    1. Associated valuer가 없는 Enum

    enum HashableEnum1 {
        case one
        case two
    }
    
    let hashableEnum1 = HashableEnum1.one
    
    if let _ = hashableEnum1 as? AnyHashable {
        print("hashableEnum1 is hashable")
        // hashableEnum1 is hashable 를 출력한다.
    }

    이 경우 enum에 : Hashable라고 따로 명시하지 않아도 자동으로 hashable한 enum이 된다.

    2. 모든 저장 프로퍼티가 Hashable한 Struct

    struct HashableStruct {
        var name: String
    }
    
    let hashableStruct = HashableStruct(name: "Hi")
    
    if let _ = hashableStruct as? AnyHashable {
        print("hashableStruct is hashable")
    } else {
        print("not hashable")
    }
    
    // not hashable이 출력된다.

    헷갈리지 말아야 할게, 조건을 만족할 경우 자동으로 Hashable하게 되는 것이 아니다. 프로토콜 요구사항을 만족시키기 위해 작성해야 하는 코드를 작성하지 않을 수 있다는 것이다.

    위 코드에서는 Hashable을 채택하는 구조체, 클래스를 정의했다. 클래스의 경우에만 "Type 'HashableClass' does not conform to protocol 'Equatable'" 라는 에러가 뜨는 걸 확인할 수 있는데, 이는 Hashable 프로토콜이 요구하는 메서드를 내부에 구현하지 않았기 때문이다.

    컴파일 에러가 나지 않게 하려면 위와 같이 hasher(into:), == 함수를 다 구현해야 한다. 여기에서 말하는 "모든 저장 프로퍼티가 Hashable한 struct인 경우 Hashable의 문법적인 구현을 사용할 수 있다" 라는 말은 모든 저장 프로퍼티가 Hashable한 구조체가 명시적으로 Hashable 프로토콜을 채택한다고 했을 때, hasher(into:), == 함수를 구현하지 않아도 된다는 말이다.

     

    다시 정리하면 Hashable 프로토콜을 채택한 구조체에서 모든 저장 프로퍼티가 Hashable하면 Swift가 자동으로 hash(into:) 메서드를 생성하기 때문에 아래와 같이 코드를 작성할 수 있다.

    struct HashableStruct: Hashable {
        var name: String
    }
    
    let hashableStruct = HashableStruct(name: "Hi")
    
    if let _ = hashableStruct as? AnyHashable {
        print("hashableStruct is hashable")
    } else {
        print("not hashable")
    }
    
    // hashableStruct is hashable 출력

    3. 모든 Associated type이 Hashable한 enum

    2번 케이스와 비슷하다.

    enum HashableEnum2: Hashable {
        case one(name: String)
        case two
    }
    
    if let _ = HashableEnum2.one(name: "Hi") as? AnyHashable {
        print("hashable")
    }
    
    // hashable 출력

    추가로, 위 조건을 만족하지 않는 enum이나 struct은 당연하게도 Hashable의 문법적 구현(synthesized implementation)을 제공받을 수 없기 때문에 아래와 같이 명시적으로 hash(into:)== 메서드를 구현해줘야지만 컴파일 에러가 나지 않는다.

    class Cat {}
    
    enum AnotherEnum: Hashable {
        case one(cat: Cat) // Cat은 Hashable하지 않다.
        case two
    
        func hash(into hasher: inout Hasher) {
            switch self {
            case .one(_):
                hasher.combine("cat")
            case .two:
                hasher.combine(2)
            }
        }
    
        static func == (lhs: AnotherEnum, rhs: AnotherEnum) -> Bool {
            lhs.hashValue == rhs.hashValue
        }
    }

    Optional and Hashable

    Hashable을 더 공부하게 된 계기다. 옵셔널 값을 해싱할 수 있을까?

    Hashable Apple 공식 문서를 보면 하단에 Hashable 프로토콜을 채택하는 수많은 타입들이 나와있는데, Optional도 여기 포함되어 있다. 옵셔널의 WrappedHashable할 때 Hashable하다.

     

    Swift github을 보면, Optional은 아래와 같이 enum으로 구현되어 있다.

    그리고 Swift 4.2에서는 Optional: Hashable을 아래와 같이 구현하고 있다.

    extension Optional: Hashable where Wrapped: Hashable {
      //  ...
      public func hash(into hasher: inout Hasher) {
        switch self {
        case .none:
          hasher.combine(0 as UInt8)
        case .some(let wrapped):
          hasher.combine(1 as UInt8)
          hasher.combine(wrapped)
        }
      }
    }
    • .none : hash 값은 0가 된다.
    • .some(wrapped) : wrapped를 hasher에 섞어서 정수 hash 값을 갖는다.

    그래서 optional은 WrappedHashable할 때 Hashable하다. 따라서 optional 값을 해싱할 때 사용할 수 있다.

    struct HashableStruct: Hashable {
        var hashableOptional1: String? = "Hi"
        var hashableOptional2: String? = nil
        var hashableOptional3: Optional<String> = nil
    }

    위 코드의 경우 구조체의 저장 프로퍼티가 모두 hashable하기 때문에 hash(into:) 메서드를 따로 작성하지 않아도 된다.

    hasher.combine(어떤 값)에서 어떤 값은 hashable해야 한다. 그런데 hashable하지 않은 옵셔널을 집어넣었기 때문에 컴파일러 에러가 뜨는 것이다. 따라서 에러를 안 뜨게 하려면 Cat 클래스를 Hashable하게 만들 수도 있고,
    hasher(into:) 메서드 내에서 임의로 hashing하는 로직을 추가로 구현해도 될 것이다.

    class Cat: Hashable {
        func hash(into hasher: inout Hasher) {
        }
    
        static func == (lhs: Cat, rhs: Cat) -> Bool {
            return true
        }
    }
    
    struct HashableStruct: Hashable {
        var hashableOptional1: String? = "Hi"
        var hashableOptional2: String? = nil
        var hashableOptional3: Optional<String> = nil
        var notHashableOptional: Cat? = Cat()
    
        func hash(into hasher: inout Hasher) {
            hasher.combine(hashableOptional1)
            hasher.combine(hashableOptional2)
            hasher.combine(hashableOptional3)
    
    //        방법 1: 임의로 hashing 로직 추가. 좋은 예시는 아닌 것 같다.
    //        if let notHashableOptional = notHashableOptional {
    //            hasher.combine(1)
    //            hasher.combine("cat")
    //        } else {
    //            hasher.combine(0)
    //        }
    
    //        방법 2: Cat이 Hashable 프로토콜을 준수하게 한다.
            hasher.combine(notHashableOptional)
        }
    
        static func == (lhs: HashableStruct, rhs: HashableStruct) -> Bool {
            lhs.hashValue == rhs.hashValue
        }
    }

     


    Reference

     

    How can I make Optionals Hashable in Xcode 9.4

    In Xcode 10, the Swift compiler is smart enough to: Treat Optionals that wrap Hashable values as being Hashable. Xcode >=9.4 will also treat structs that contain all Hashable properties as also be...

    stackoverflow.com

     

    GitHub - apple/swift: The Swift Programming Language

    The Swift Programming Language. Contribute to apple/swift development by creating an account on GitHub.

    github.com

     

    Protocols — The Swift Programming Language (Swift 5.7)

    Protocols A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of tho

    docs.swift.org

     

    Apple Developer Documentation

     

    developer.apple.com

     

    What is hashing and how does it work?

    Find out how hashing works to transform keys and characters, what it's used for and how it relates to data structure, cybersecurity and cryptography.

    www.techtarget.com

     

    반응형

    댓글

Designed by Tistory.