programing

재정의에 대한 모범 사례는 동일: 및 해시입니다.

abcjava 2023. 5. 1. 19:47
반응형

재정의에 대한 모범 사례는 동일: 및 해시입니다.

어떻게 적절히 오버라이드합니까?isEqual:목표-C에서?"캐치"는 두 물체가 동일한 경우에 발생하는 것처럼 보입니다(에 의해 결정됨).isEqual:method), 해시 값이 같아야 합니다.

코코아 기본 원리 가이드의 검사 섹션에는 오버라이드 방법에 대한 예가 나와 있습니다.isEqual:이름이 지정된 클래스에 대해 다음과 같이 복사됩니다.MyWidget:

- (BOOL)isEqual:(id)other {
    if (other == self)
        return YES;
    if (!other || ![other isKindOfClass:[self class]])
        return NO;
    return [self isEqualToWidget:other];
}

- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
    if (self == aWidget)
        return YES;
    if (![(id)[self name] isEqual:[aWidget name]])
        return NO;
    if (![[self data] isEqualToData:[aWidget data]])
        return NO;
    return YES;
}

이퀄리티를 한 다음 이퀄리티를 확인하고 으로 포터동일다클동확을마일인다고비니다교합사를 사용하여 객체를 합니다.isEqualToWidget:그것은 오직 그것을 확인합니다.name그리고.data특성.예제에서 보여주지 않는 것은 다음과 같은 방법을 재정의하는 방법입니다.hash.

성질들이 age그래야 하지 않나요?hash메서드를 재정의하여 다음과 같이 합니다.name그리고.data해시에 영향을 미칩니까?만약 그렇다면, 어떻게 하시겠습니까?의 해시를 추가합니다.name그리고.data예:

- (NSUInteger)hash {
    NSUInteger hash = 0;
    hash += [[self name] hash];
    hash += [[self data] hash];
    return hash;
}

그걸로 충분합니까?더 좋은 기술이 있습니까?만약 당신이 원시적인 것들을 가지고 있다면요, 예를 들면.int대로 합니다.NSNumber그들의 해시를 얻기 위해?또는 다음과 같은 구조NSRect?

( 방구: 원래는 "비트와이즈 OR"을 함께 썼습니다.|=추가를 의미합니다.)

시작:

 NSUInteger prime = 31;
 NSUInteger result = 1;

그럼 당신이 하는 모든 원시인들에게

 result = prime * result + var

개체의 경우 0을 0으로 표시하고 그렇지 않으면 해시 코드를 사용합니다.

 result = prime * result + [var hash];

부울의 경우 두 개의 다른 값을 사용합니다.

 result = prime * result + ((var)?1231:1237);

설명 및 귀인

이것은 코트의 작품이 아니며, 더 많은 설명을 요구하는 댓글들이 있었기 때문에, 저는 귀속을 위한 편집이 공정하다고 생각합니다.

이 알고리즘은 "효과적인 자바"라는 책에서 대중화되었으며, 관련 챕터는 현재 온라인에서 찾을있습니다.이 책은 알고리즘을 대중화했으며, 이 알고리즘은 현재 이클립스를 포함한 많은 Java 애플리케이션에서 기본값으로 사용되고 있습니다.그러나 Dan Bernstein 또는 Chris Torrek에 의해 다양하게 귀속되는 훨씬 더 오래된 구현에서 파생되었습니다.그 오래된 알고리즘은 원래 유즈넷에서 떠돌았고, 특정한 속성은 어렵습니다.예를 들어, 이 Apache 코드에는 원래 소스를 참조하는 흥미로운 설명(이름 검색)이 있습니다.

결론은, 이것은 아주 오래되고 간단한 해싱 알고리즘이라는 것입니다.그것은 가장 성능이 좋은 것은 아니며, 심지어 수학적으로 "좋은" 알고리즘이라는 것이 증명되지도 않았습니다.하지만 단순하고 많은 사람들이 좋은 결과로 오랫동안 사용해왔기 때문에 역사적인 뒷받침이 많이 됩니다.

저는 Objective-C를 직접 선택하기 때문에 해당 언어에 대해 구체적으로 말할 수 없지만, 다른 언어에서는 두 인스턴스가 "동일"이면 동일한 해시를 반환해야 합니다. 그렇지 않으면 해시 테이블(또는 사전 유형 컬렉션)에서 키로 사용하려고 할 때 모든 종류의 문제가 발생합니다.

반면에 두 인스턴스가 같지 않으면 해시가 같을 수도 있고 아닐 수도 있습니다. 해시가 동일하지 않은 경우가 가장 좋습니다.이것은 해시 테이블의 O(1) 검색과 O(N) 검색의 차이입니다. 모든 해시가 충돌하는 경우 테이블을 검색하는 것이 목록을 검색하는 것보다 나을 수 없습니다.

모범 사례 측면에서 해시는 입력 값의 랜덤 분포를 반환해야 합니다.즉, 예를 들어 이중 값이 있지만 값의 대부분이 0과 100 사이의 군집을 이루는 경향이 있는 경우 이러한 값에 의해 반환되는 해시가 가능한 해시 값의 전체 범위에 고르게 분포되어 있는지 확인해야 합니다.이렇게 하면 성능이 크게 향상됩니다.

여기에 나열된 몇 가지 해싱 알고리즘을 포함하여 여러 가지 해싱 알고리즘이 있습니다.성능에 큰 영향을 미칠 수 있기 때문에 새로운 해시 알고리즘을 생성하는 것을 피하려고 합니다. 따라서 기존 해시 방법을 사용하고 예제에서와 같이 비트적으로 조합하는 것이 이를 방지하는 좋은 방법입니다.

중요한 속성의 해시 값에 대한 간단한 XOR는 99%의 시간으로 충분합니다.

예:

- (NSUInteger)hash
{
    return [self.name hash] ^ [self.data hash];
}

Matt Thompson이 http://nshipster.com/equality/ 에서 찾은 솔루션(그의 게시물에서 이 질문도 참조했습니다 :~)

저는 이 스레드가 제가 필요로 하는 모든 것을 제공하는 데 매우 도움이 된다는 것을 알았습니다.isEqual:그리고.hash한 번에 실행되는 방법에서 객체 인스턴스 변수를 테스트할 때isEqual:예제 코드는 다음을 사용합니다.

if (![(id)[self name] isEqual:[aWidget name]])
    return NO;

장치 테스트에서 개체가 동일하다는 것을 알고 오류 없이 반복적으로 실패(즉, NO 반환)했습니다.그 이유는, 그 중 하나가NSString인스턴스 변수가 0이므로 위의 문장은 다음과 같습니다.

if (![nil isEqual: nil])
    return NO;

그리고 0은 어떤 방법으로든 반응할 것이기 때문에, 이것은 완벽하게 합법적이지만.

[nil isEqual: nil]

0을 반환합니다. 이는 NO입니다. 따라서 객체와 테스트 대상 객체가 모두 0인 객체를 가질 때 동일하지 않은 것으로 간주됩니다.isEqual:"아니오"를 반환합니다.

이 간단한 수정은 if 문을 다음으로 변경하는 것이었습니다.

if ([self name] != [aWidget name] && ![(id)[self name] isEqual:[aWidget name]])
    return NO;

이러한 방식으로 주소가 동일한 경우 둘 0이거나 둘 다 동일한 개체를 가리킬 때 메서드 호출을 건너뛰지만 둘 중 하나가 0이 아니거나 다른 개체를 가리킬 때는 비교기가 적절하게 호출됩니다.

나는 이것이 누군가가 머리를 긁는 몇 분을 절약하기를 바랍니다.

해시 함수는 다른 개체의 해시 값과 충돌하거나 일치할 가능성이 없는 반고유 값을 만들어야 합니다.

클래스 인스턴스 변수에 적용할 수 있는 전체 해시 함수입니다.64/32비트 응용 프로그램의 호환성을 위해 int가 아닌 NSUInteger를 사용합니다.

서로 다른 개체에 대해 결과가 0이 되면 해시가 충돌할 위험이 있습니다.해시를 충돌하면 해시 함수에 종속된 일부 컬렉션 클래스로 작업할 때 예기치 않은 프로그램 동작이 발생할 수 있습니다.사용하기 전에 해시 함수를 테스트해야 합니다.

-(NSUInteger)hash {
    NSUInteger result = 1;
    NSUInteger prime = 31;
    NSUInteger yesPrime = 1231;
    NSUInteger noPrime = 1237;
    
    // Add any object that already has a hash function (NSString)
    result = prime * result + [self.myObject hash];
    
    // Add primitive variables (int)
    result = prime * result + self.primitiveVariable; 

    // Boolean values (BOOL)
    result = prime * result + (self.isSelected ? yesPrime : noPrime);
    
    return result;
}

것을 돌려주는 것입니다.-hash모든 인스턴스에 대한 값입니다.그렇지 않으면 동일성에 영향을 주는 개체만 기반으로 해시를 구현해야 합니다.만약 당신이 느슨한 비교를 사용한다면 이것은 까다롭습니다.-isEqual:(예: 대/소문자를 구분하지 않는 문자열 비교).int의 경우 NSNumber와 비교하지 않는 한 일반적으로 int 자체를 사용할 수 있습니다.

하지만 |=를 사용하지 마십시오. 포화 상태가 됩니다.대신 ^=를 사용합니다.

무작위 재미있는 사실:[[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]],그렇지만[[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash] 5월 5일 (rdar://4538282, 2006년 5월 5일 이후 오픈)

다음과 같은 경우에만 해시를 제공하면 됩니다.isEqual사실입니다.언제isEqual잘못된 것입니다. 해시가 동일하지 않아도 됩니다.과 같은 이유

해시를 단순하게 유지합니다.가장 고유한 구성원(또는 소수의 구성원) 변수를 선택합니다.

예를 들어, CLPacemark의 경우 이름만 있으면 됩니다.네, 정확히 같은 이름을 가진 2개 또는 3개의 구별되는 CLP Placemark가 있지만 그것들은 희귀합니다.그 해시를 사용합니다.

@interface CLPlacemark (equal)
- (BOOL)isEqual:(CLPlacemark*)other;
@end

@implementation CLPlacemark (equal)

...

-(NSUInteger) hash
{
    return self.name.hash;
}


@end

도시, 국가 등을 지정할 필요가 없습니다.그 이름으로 충분합니다.아마도 이름과 CLL 위치일 것입니다.

해시는 균등하게 분배되어야 합니다.캐럿 ^(x 또는 기호)을 사용하여 여러 멤버 변수를 결합할 수 있습니다.

그래서 약간.

hash = self.member1.hash ^ self.member2.hash ^ self.member3.hash

그렇게 하면 해시가 균등하게 분배됩니다.

Hash must be O(1), and not O(n)

그럼 어떻게 해야 할까요?

다시 말씀드리지만, 간단합니다.어레이의 모든 멤버를 해시할 필요는 없습니다.첫 번째 요소, 마지막 요소, 카운트, 중간 요소를 해시하기에 충분합니다. 그게 전부입니다.

동등 및 해시 계약은 Java 세계에서 잘 지정되고 철저히 연구되지만(@mipardi의 답변 참조), 모든 동일한 고려사항이 목표-C에 적용되어야 합니다.

Eclipse는 Java에서 이러한 메서드를 생성하는 신뢰할 수 있는 작업을 수행하므로 다음은 Objective-C로 손으로 포팅한 Eclipse 예제입니다.

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if ([self class] != [object class])
        return false;
    MyWidget *other = (MyWidget *)object;
    if (_name == nil) {
        if (other->_name != nil)
            return false;
    }
    else if (![_name isEqual:other->_name])
        return false;
    if (_data == nil) {
        if (other->_data != nil)
            return false;
    }
    else if (![_data isEqual:other->_data])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = 1;
    result = prime * result + [_name hash];
    result = prime * result + [_data hash];
    return result;
}

그리고 하위 클래스의 경우YourWidget 속을추니다를 합니다.serialNo:

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if (![super isEqual:object])
        return false;
    if ([self class] != [object class])
        return false;
    YourWidget *other = (YourWidget *)object;
    if (_serialNo == nil) {
        if (other->_serialNo != nil)
            return false;
    }
    else if (![_serialNo isEqual:other->_serialNo])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = [super hash];
    result = prime * result + [_serialNo hash];
    return result;
}

분류 합니다.isEqual:Apple 제품:

  • 시험other isKindOfClass:[self class]는 두 의 서로 다른 의 비대칭입니다.MyWidget평등은 대칭이어야 합니다. a=b인 경우와 b=a인 경우에만 동일해야 합니다.이 문제는 테스트를 다음으로 변경하여 쉽게 해결할 수 있습니다.other isKindOfClass:[MyWidget class] 에는 모두MyWidget하위 클래스는 상호 비교 가능합니다.
  • 사용isKindOfClass:하위 클래스 테스트는 하위 클래스가 재정의되지 않도록 방지합니다.isEqual:정제된 동등성 시험으로.이는 등식이 과도적이어야 하기 때문입니다. a=b와 a=c이면 b=c입니다.만약에MyWidget는 두 의 스instance턴(비) 교개두같습다니와가와 합니다.YourWidget에 인스턴스, 그 다음에 그 다음에 그 다음에.YourWidget는 서로. 에도 마찬가지입니다.serialNo다르다.

두 번째 문제는 객체가 정확히 같은 클래스에 속할 경우 동일하다고 간주함으로써 해결될 수 있습니다.[self class] != [object class]여기서 테스트합니다.일반적인 애플리케이션 클래스의 경우 이 방법이 가장 적합한 것 같습니다.

하지만, 확실히 다음과 같은 경우가 있습니다.isKindOfClass:테스트가 더 좋습니다.이것은 응용프로그램 클래스보다 프레임워크 클래스에 더 일반적입니다.예를 들어, 임의NSString.NSString 기문 시스로퀀, 관이없계과자에 동일한 기본 됩니다.NSString/NSMutableString 별구의 , 그고또한어개클상에관이없스래인떤리▁dist이상없관.NSString클래스 클러스터가 관련되어 있습니다.

이런경우는에,는에,isEqual:잘 정의되고 잘 정의된 행동을 가져야 하며, 하위 클래스가 이를 무시할 수 없음을 분명히 해야 합니다.를 java 서 hashcode 메를덮같 ' 어쓰기 '와 같이 를 지정하여 'override'할 수 .final하지만 목표-C에는 그에 상응하는 것이 없습니다.

잠시만요, 확실히 훨씬 더 쉬운 방법은 먼저 오버라이드하는 것입니다.- (NSString )description및 개체 상태를 나타내는 문자열을 제공합니다(이 문자열에서 개체의 전체 상태를 나타내야 합니다).

그런 다음, 다음과 같은 구현을 제공합니다.hash:

- (NSUInteger)hash {
    return [[self description] hash];
}

이것은 "만약 (isEqualToString: 메서드에 의해 결정된) 두 문자열 객체가 같다면, 그것들은 동일한 해시 값을 가져야 한다"는 원칙에 기초합니다.

원본: NSString 클래스 참조

이것은 당신의 질문에 직접적으로 대답하지는 않지만, 저는 전에 해시를 생성하기 위해 MurmurHash를 사용한 적이 있습니다: MurmurHash.

왜 그런지 설명해야 할 것 같아요. 머머해시는 피비린내 나는 속도야...

는 이 페이지가 equals 및 hash-type 메서드를 재정의하는 데 유용한 지침이 된다는 을 알게 되었습니다.해시 코드를 계산하기 위한 적절한 알고리즘이 포함되어 있습니다.그 페이지는 자바에 맞춰져 있지만, Objective-C/Cocoa에 적응하는 것은 꽤 쉽습니다.

저도 객관적인 신인이지만, 저는 객관적인 C에서 정체성 대 평등에 대한 훌륭한 기사를 발견했습니다.내가 읽은 바로는 기본 해시 함수(고유한 ID를 제공해야 함)를 유지하고 isEqual 메서드를 구현하여 데이터 값을 비교할 수 있을 것 같습니다.

퀸이 중얼거리는 해시에 대한 언급이 여기서 쓸모없다는 것은 잘못된 것입니다.당신이 해싱 뒤에 있는 이론을 이해하고 싶어한다는 퀸의 말이 맞아요.그 중얼거림은 그 이론의 많은 부분을 구현으로 증류합니다.이러한 구현을 이 특정 애플리케이션에 적용하는 방법을 파악하는 것은 검토할 가치가 있습니다.

여기서 핵심 사항 중 일부는 다음과 같습니다.

curdt의 예제 함수는 '31'이 소수이기 때문에 좋은 승수임을 나타냅니다.우리는 프라임이 필요하고 충분한 조건이라는 것을 보여줄 필요가 있습니다.사실 31(및 7)은 31 == -1 % 32이기 때문에 아마도 특별히 좋은 소수는 아닐 것입니다.비트가 절반 정도 설정되어 있고 비트가 절반 이상 클리어된 홀수 승수가 더 나을 수 있습니다.(murmur 해시 곱셈 상수는 해당 속성을 가집니다.)

이러한 유형의 해시 함수는 곱셈 후에 shift와 xor를 통해 결과 값을 조정하면 더 강해질 수 있습니다.곱셈은 레지스터의 높은 쪽 끝에서 많은 비트 상호 작용의 결과를 생성하고 레지스터의 낮은 쪽 끝에서 낮은 상호 작용 결과를 생성하는 경향이 있습니다.시프트 및 xor는 레지스터의 하단에서 상호 작용을 증가시킵니다.

초기 결과를 비트의 약 절반이 0이고 비트의 약 절반이 1인 값으로 설정하는 것도 유용한 경향이 있습니다.

요소가 결합되는 순서에 주의하는 것이 유용할 수 있습니다.값이 강하게 분포되어 있지 않은 부울 및 기타 요소를 먼저 처리해야 합니다.

계산이 끝날 때 몇 개의 추가 비트 스크램블링 단계를 추가하는 것이 유용할 수 있습니다.

이 애플리케이션에 대한 잡음 해시가 실제로 빠른지 여부는 미해결 문제입니다.잡음 해시는 각 입력 단어의 비트를 미리 혼합합니다.여러 입력 단어를 병렬로 처리할 수 있으므로 여러 파이프라인 CPU를 발급하는 데 도움이 됩니다.

속성 이름을 얻기 위한 @tcurdt의 답변과 @oscar-gomez의 답변을 결합하면 is Equal 및 hash 모두에 대한 간편한 드롭인 솔루션을 만들 수 있습니다.

NSArray *PropertyNamesFromObject(id object)
{
    unsigned int propertyCount = 0;
    objc_property_t * properties = class_copyPropertyList([object class], &propertyCount);
    NSMutableArray *propertyNames = [NSMutableArray arrayWithCapacity:propertyCount];

    for (unsigned int i = 0; i < propertyCount; ++i) {
        objc_property_t property = properties[i];
        const char * name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        [propertyNames addObject:propertyName];
    }
    free(properties);
    return propertyNames;
}

BOOL IsEqualObjects(id object1, id object2)
{
    if (object1 == object2)
        return YES;
    if (!object1 || ![object2 isKindOfClass:[object1 class]])
        return NO;

    NSArray *propertyNames = PropertyNamesFromObject(object1);
    for (NSString *propertyName in propertyNames) {
        if (([object1 valueForKey:propertyName] != [object2 valueForKey:propertyName])
            && (![[object1 valueForKey:propertyName] isEqual:[object2 valueForKey:propertyName]])) return NO;
    }

    return YES;
}

NSUInteger MagicHash(id object)
{
    NSUInteger prime = 31;
    NSUInteger result = 1;

    NSArray *propertyNames = PropertyNamesFromObject(object);

    for (NSString *propertyName in propertyNames) {
        id value = [object valueForKey:propertyName];
        result = prime * result + [value hash];
    }

    return result;
}

에서 이제사자정클쉽서수있다구습니현할게에의용래를 구현할 수 .isEqual:그리고.hash:

- (NSUInteger)hash
{
    return MagicHash(self);
}

- (BOOL)isEqual:(id)other
{
    return IsEqualObjects(self, other);
}

생성 후 변환할 수 있는 개체를 만드는 경우 개체가 컬렉션에 삽입된 경우 해시 값이 변경되지 않아야 합니다.실질적으로, 이것은 해시 값이 초기 객체 생성 시점부터 고정되어야 한다는 것을 의미합니다.자세한 내용은 NSObject 프로토콜의 -hash 방법에 대한 Apple 문서를 참조하십시오.

해시 값을 사용하여 컬렉션에서 개체의 위치를 결정하는 컬렉션에 가변 개체가 추가된 경우 개체가 컬렉션에 있는 동안 개체의 해시 메서드에서 반환된 값이 변경되지 않아야 합니다.따라서 해시 방법은 개체의 내부 상태 정보에 의존해서는 안 되며 개체가 컬렉션에 있는 동안 개체의 내부 상태 정보가 변경되지 않도록 해야 합니다.따라서 예를 들어, 가변 사전은 해시 테이블에 넣을 수 있지만, 해당 사전이 있는 동안에는 변경해서는 안 됩니다. (특정 개체가 컬렉션에 있는지 여부를 알기 어려울 수 있습니다.)

이것은 잠재적으로 해시 검색을 훨씬 덜 효율적으로 만들기 때문에 저에게는 완전한 속임수처럼 들리지만, 저는 주의의 측면에서 실수하고 문서가 말하는 것을 따르는 것이 더 낫다고 생각합니다.

여기서 완전한 오류를 범할 위험이 있다면 미안하지만... 아무도 '모범 사례'를 따르려면 대상 개체가 소유한 모든 데이터를 고려하지 않는 동등한 방법을 지정하지 않아야 한다고 언급하지 않았습니다. 예를 들어, 개체와 관련된 데이터가 수집되는 경우와 비교하여,동등한 것을 구현할 때 고려해야 합니다.비교에서 '나이'를 고려하지 않으려면 비교기를 작성하여 isEqual: 대신 비교를 수행해야 합니다.

isEqual: 임의로 동일성 비교를 수행하는 메서드를 정의하면 동일성 해석의 '왜곡'을 잊어버린 경우 이 메서드가 다른 개발자가 잘못 사용할 위험이 발생합니다.

에르고, 이것은 해시에 대한 훌륭한 q&a이지만, 일반적으로 해시 방법을 재정의할 필요는 없으며, 아마도 애드혹 비교기를 정의해야 할 것입니다.

언급URL : https://stackoverflow.com/questions/254281/best-practices-for-overriding-isequal-and-hash

반응형