사용자 정의 Swift 동등성과 해싱이 하나의 불변 값 정의를 사용하도록 만드세요. 이 안내에서는 이름의 대소문자는 무시하되 치수는 정확히 일치해야 하는 로컬 타일 키를 만든 다음 동등성, 집합 멤버십, 딕셔너리 조회를 확인합니다.

두 타일이 같은 경우를 정확히 정의하기

작은 Mac 데스크톱 그래픽 연습을 만든다고 가정해 보세요. 이름, 너비, 높이를 기준으로 가상의 타일 설명을 그룹화해야 합니다. 독자가 구현할 작업은 일관된 그룹화입니다. 같은 치수의 Orbit과 ORBIT은 동등한 키를 만들어야 하고, 어느 한쪽 치수라도 바뀌면 다른 키가 되어야 합니다.

두 프로토콜을 구현하기 전에 규칙을 작성하세요. 이 예에서는 대소문자를 접은 이름, 너비, 높이가 같을 때 두 키가 동등합니다. 치수는 양의 정수 그리기 단위입니다. 앞뒤 공백은 의미가 있습니다. 이는 Swift가 요구하는 규칙이 아니라 예시 애플리케이션의 규칙입니다.

가상 입력 예상 관계 이유
Orbit, 40 × 20 및 ORBIT, 40 × 20 동등 대소문자를 무시함
Orbit, 40 × 20 및 orbit, 41 × 20 비동등 너비가 동등성에 반영됨
Orbit, 40 × 20 및 orbit, 40 × 21 비동등 높이가 동등성에 반영됨
Orbit, 40 × 20 및 orbit , 40 × 20 비동등 뒤쪽 공백이 유지됨

특정 그룹화 규칙이 화면에 표시되는 타일의 전체 값과 다를 때는 전용 키를 사용하세요. 원래의 표시 이름은 이를 둘러싼 프레젠테이션 모델에 유지하세요. 그러면 눈에 보이는 이름이 값의 일부로 다른데도 완전한 타일 설명 두 개를 서로 대체 가능하다고 선언하는 일을 피할 수 있습니다. Swift의 Equatable 문서는 대체 가능성을 기준으로 동등성을 설명하며 반사성, 대칭성, 추이성을 요구합니다. Equatable

동등성과 해싱에 동일한 구성 요소 사용하기

Swift의 Hashable 프로토콜은 Equatable을 상속합니다. 핵심 요구 사항은 동등한 인스턴스가 해셔에 같은 순서로 같은 값을 전달해야 한다는 것입니다. 해시 가능한 값은 집합 요소와 딕셔너리 키로 사용할 수 있습니다. Hashable

흔한 실수는 ==에서 정규화된 이름을 비교하면서 원래 표기를 해싱하는 것입니다. 그러면 두 메서드에서 이름의 의미가 달라집니다. 너비를 동등성에서는 제외하고 해싱에는 포함하는 것도 또 다른 실수입니다. 두 설계 모두 동등하다고 선언된 값들이 서로 다른 해싱 입력을 제공하게 합니다. Apple은 사용자 정의 구현에서 두 메서드에 동일한 유의 속성을 사용하라고 명시적으로 안내합니다. 일반적인 프로토콜 채택하기

이 예에서는 동등성 구성 요소를 중첩된 비공개 구조체에 넣으세요. Swift가 해당 구조체의 동등성과 해싱을 합성하게 한 다음, 바깥쪽 두 연산을 모두 여기에 위임합니다. 이렇게 하면 구성 요소 목록이 한 곳에 선언되어 확인하기 쉽습니다. Swift는 저장 속성이 필요한 프로토콜을 준수할 때 구조체의 준수 구현을 합성할 수 있습니다. 프로토콜 — 합성 구현을 사용해 프로토콜 채택하기

타입이 동등성 구성 요소만 정확히 저장한다면, 일반적으로 Hashable 준수를 합성하는 것만으로 충분합니다. 아래의 명시적인 바깥쪽 메서드는 기존 사용자 정의 구현을 유지하면서 필드 목록 두 개가 따로 관리되지 않도록 하는 방법을 보여 줍니다.

Mac에서 완전한 로컬 예제 실행하기

Foundation이 포함된 Swift 툴체인이 설치되어 있어야 합니다. Swift의 시작하기 문서 권고에 따라 Terminal에서 swift --version으로 확인하세요. Swift를 사용할 수 없다면 계속하기 전에 공식 macOS 설치 안내를 따르세요. 시작하기, Swift 설치 — macOS

다음은 Swift 프로그램을 포함한 완전한 셸 명령 하나입니다. 블록 전체를 Terminal에 붙여 넣으세요. 표준 입력을 통해 프로그램을 전달합니다. 예제는 메모리 내 계산만 수행하고 결과를 출력합니다. Swift를 사용할 수 없어 조사 환경에서 Swift 코드를 실행하지는 못했지만, 코드는 검토했습니다.

swift - <<'SWIFT'
import Foundation

struct TileKey: Hashable, Sendable {
    private struct Components: Hashable, Sendable {
        let foldedName: String
        let width: Int
        let height: Int
    }

    private let components: Components

    init(name: String, width: Int, height: Int) {
        precondition(width > 0 && height > 0,
                     "Tile dimensions must be positive")

        let foldedName = name.folding(
            options: [.caseInsensitive],
            locale: Locale(identifier: "en_US_POSIX")
        )
        components = Components(
            foldedName: foldedName,
            width: width,
            height: height
        )
    }

    static func == (lhs: TileKey, rhs: TileKey) -> Bool {
        lhs.components == rhs.components
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(components)
    }
}

let a = TileKey(name: "Orbit", width: 40, height: 20)
let b = TileKey(name: "ORBIT", width: 40, height: 20)
let c = TileKey(name: "orbit", width: 40, height: 20)
let wider = TileKey(name: "orbit", width: 41, height: 20)
let taller = TileKey(name: "orbit", width: 40, height: 21)
let spaced = TileKey(name: "orbit ", width: 40, height: 20)
let samples = [a, b, c, wider, taller, spaced]

precondition(a == b && b == c && a == c)
precondition(a != wider && a != taller && a != spaced)

// Check the equality laws over this finite sample.
for first in samples {
    precondition(first == first, "Reflexivity failed")
    for second in samples {
        precondition((first == second) == (second == first),
                     "Symmetry failed")
        if first == second {
            precondition(first.hashValue == second.hashValue,
                         "Equal values have different hashes")
        }
        for third in samples where first == second && second == third {
            precondition(first == third, "Transitivity failed")
        }
    }
}

let uniqueKeys = Set(samples)
precondition(uniqueKeys.count == 4)
precondition(uniqueKeys.contains(b))

let patterns = [a: "striped"]
precondition(patterns[b] == "striped")

print("Unique keys:", uniqueKeys.count)
print("Equivalent key found:", uniqueKeys.contains(b))
print("Pattern:", patterns[b] ?? "missing")
print("Sample contract checks passed")
SWIFT

예상 출력:

Unique keys: 4
Equivalent key found: true
Pattern: striped
Sample contract checks passed

여섯 입력은 네 그룹을 이룹니다. 대소문자만 다른 세 입력은 하나의 키를 공유하고, 너비 변경, 높이 변경, 뒤쪽 공백이 있는 입력은 각각 별도의 그룹을 이룹니다. 딕셔너리 검사는 a에 연결된 값을 동등한 키인 b으로 가져올 수 있는지 확인합니다. 이 검사는 특정 숫자 해시에 의존하지 않고 의도한 컬렉션 동작을 점검합니다.

이니셜라이저는 0 이하의 치수에서 의도적으로 중단됩니다. 유효하지 않은 치수가 프로그래밍 실수를 뜻하는 이 고정된 가상 연습에서는 적절한 처리입니다. 데스크톱 인터페이스에서 치수를 입력하는 도중의 미완성 상태를 허용한다면, 키를 만들기 전에 입력을 검증하거나 이니셜라이저가 옵셔널을 반환하거나 오류를 던지도록 바꾸세요. 치수를 임의로 대체하지 마세요. 그러면 동등성 규칙이 달라집니다.

텍스트 정규화를 명시적인 규칙으로 만들기

정규화 단계는 Swift의 문자열 API를 통해 Foundation의 folding(options:locale:)를 호출합니다. API는 지정한 폴딩 옵션을 적용한 문자열을 반환합니다. 여기서는 .caseInsensitive만 요청합니다. StringProtocol.folding(options:locale:)

Foundation 문서는 대소문자, 너비, 발음 구별 기호 폴딩을 각각 별개의 선택 항목으로 다룹니다. 또한 로캘이 폴딩 결과를 바꿀 수 있고 폴딩된 텍스트가 표시에 적합하지 않을 수 있다고 설명합니다. 따라서 이 예에서는 고정된 로캘을 지정하고 폴딩 결과를 키 내부에만 저장합니다. 가상 이름은 의도적으로 단순하게 정했습니다. 이 로캘 선택은 이 연습의 동작을 정의하며, 모든 이름 입력 인터페이스에 통용되는 권장 사항은 아닙니다. NSString.folding(options:locale:)

핵심 설계 결정은 생성 시 한 번 정규화하는 것입니다. 그러면 이후 동등성과 해싱은 저장된 결과를 사용하므로 어느 연산도 로캘이나 변환을 별도로 선택하지 않습니다. 애플리케이션에 나중에 다른 비교 정책이 필요해지면 새 정책에 따라 키를 만들고 해당 메모리 내 컬렉션을 다시 구성하세요.

정규화 범위를 넓히기 전에 원하는 그룹을 구별하는 예제를 추가하세요. 공백은 의미가 있어야 할까요? 악센트가 있는 문자는 서로 구별해야 할까요? 너비가 달라 보이는 문자는 같은 이름으로 취급해야 할까요? 이 질문에 제품 규칙으로 답한 다음, 두 연산이 함께 사용하는 하나의 변환을 구현하세요.

해시 숫자를 믿지 않고 불일치 진단하기

기존 타입에 이 설계를 적용할 때는 다음 순서를 따르세요.

  1. 동등하게 비교되어야 하는 값 쌍 하나와 값을 구별해야 하는 각 필드별 쌍 하나를 작성합니다.
  2. 정규화와 파생 속성을 포함해 ==이 실제로 비교하는 값을 추적합니다.
  3. 순서를 포함해 hasher.combine에 전달되는 값을 추적합니다.
  4. 공유 정의를 하나의 불변 구성 요소 값으로 옮기거나, 두 구현이 동일한 구성 요소를 명시적으로 사용하게 합니다.
  5. 동일한 테스트 데이터로 동등성 법칙, 동등한 값의 해시, 집합 멤버십, 딕셔너리 조회를 확인합니다.

hashValue을 비교해 동등성을 구현하지 마세요. 동등한 값은 해시도 같아야 하지만 해시가 같다는 사실만으로 동등성이 성립하지는 않습니다. 전체 동등성 구성 요소가 ==의 기준입니다. 마찬가지로 서로 다른 모든 쌍의 해시가 달라야 한다고 요구하는 테스트는 해싱 계약이 보장하는 범위를 넘어섭니다. Hashable

이 계약을 확인할 때는 현재 실행 내에서만 해시를 비교하세요. Swift의 Hasher는 무작위 시드를 사용하며, 표준 라이브러리 버전이 바뀌면 알고리즘도 달라질 수 있습니다. 따라서 어제 실행에서 출력된 정수는 오늘 실행의 예상 결과로 적합하지 않습니다. Hasher

유한한 샘플 검사는 유용한 회귀 검사지만, 가능한 모든 문자열과 치수에 대한 증명은 아닙니다. 구조적 근거도 중요합니다. 두 바깥쪽 메서드가 모두 동일한 불변 Components 값에 위임하고, 그 저장 필드가 선택한 계약을 완전히 표현합니다.

동시성 요구 사항을 동등성과 분리하기

이 예에서는 두 구조체의 저장 값이 문자열, 정수, 그리고 또 다른 전송 가능한 구조체이므로 둘 다 Sendable로 선언합니다. Swift 문서에 따르면 전송 가능한 구조체는 전송 가능한 저장 속성만 포함해야 합니다. 전송 가능성은 동시성 영역 사이에서 값을 전달하는 것에 관한 것이며, 어떤 값이 동등한지 정의하지 않습니다. Sendable, Swift 프로그래밍 언어 — 동시성

이 로컬 계산에는 작업이나 액터가 필요하지 않습니다. 나중에 동시성을 사용하는 데스크톱 코드의 일부가 된다면 키의 저장된 구성 요소를 대상으로 동등성과 해싱을 동기식 연산으로 유지하세요. 공유 가변 컬렉션은 별도의 동시성 설계 문제로 다루세요. 언어 가이드에서는 액터와 격리를 가변 상태 접근을 조정하는 메커니즘으로 설명합니다. Swift 프로그래밍 언어 — 동시성

이 패턴을 적용할 때는 Foundation과 맞닿는 부분도 있습니다. NSObject 하위 클래스는 상속된 동등성과 해싱 동작을 사용합니다. Apple이 지원하는 사용자 정의 방식은 isEqual(_:)와 hash을 함께 재정의하는 것입니다. 이 예의 일반 Swift 구조체는 ==와 hash(into:)을 사용합니다. 일반적인 프로토콜 채택하기

로컬 타일 키를 유지 관리하기 쉽게 만들려면 비교 규칙을 작성하고, 해당 규칙을 하나의 불변 표현으로 구현하며, 실제 컬렉션 조회를 점검하면 됩니다. 정의가 바뀌면 동등성과 해싱이 계속 같은 값을 설명하도록 구성 요소 표현과 예제를 함께 업데이트하세요.

출처