본문 바로가기
프로그래밍 놀이터/iOS

[Effective Objective-C] #8 객체의 동등 비교를 이해하라

by 돼지왕 왕돼지 2017. 8. 9.
반응형

 [Effective Objective-C] #8 객체의 동등 비교를 이해하라


출처 : Effective Objective-C

==, CLASS, deep equality, effective objective-c, hash, isEqual, isEqual:, isequalarray, isequaldictionary, Set, swallow equality, [Effective Objective-C] #8 객체의 동등 비교를 이해하라, ^, 가변구역 해시, 동등 비교, 동등 비교 메서드, 배열, 사전, 성능, 재정의, 전용 메서드, 집합, 컨테이너 내부의 가별 클래스들의 동등성, 컬렉션 포함된 객체 수정, 클래스 비교, 포인터 값 비교, 포인터 비교, 프로퍼티 비교, 해시 충돌, 해시값


-

== 연산자를 사용하면 포인터 값을 비교한다.

두 객체가 같은지 비교하려면 NSObject 프로토콜에 정의되어 있는 isEqual: 메서드를 사용해야 한다.



-

몇몇 객체는 이미 같은 클래스인지 확인된 두 객체를 비교하는 특별한 동등 확인(equality-checking) 메서드들을 제공한다.



-

다음 두 메서드는 NSObject 프로토콜의 핵심 동등 비교 메서드이다.

- (BOOL)isEqual:(id)object;

- (NSUInteger)hash;


위 두 메서드는 NSObject 클래스에 기본적인 구현이 되어 있다.

구현 내용은 두 객체가 같은 객체일 뿐 아니라 포인터도 정확히 같아야 똑같다고 판단하는 것이다.



-

isEqual: 메서드가 두 객체가 같다고 판단하기 위해서는 두 객체의 hash 메서드 결과도 같아야 한다.

그러나 hash 메서드의 반환값이 같은 두 객체가 isEqual: 에서도 같을 필요는 없다.



-

isEqual: 의 예는 아래와 같다.

- (BOOL)isEqual:(id)object {

     if ( self == object ) return YES;

     if ( [self class] != [object class] ) return NO;


     EOCPerson *otherPerson = (EOCPerson*)object;

     if ( ![_firstName isEqualToString:otherPerson.firstName] ){

          return NO;

     }

     if ( … ){

          return NO;

     }

     ...

     return YES;

}


제일 먼저 객체의 포인터가 자신과 같은지 비교한다.

포인터가 같다면 같은 객체이다.


다음으로 두 객체의 클래스를 비교한다.

클래스가 같지 않다면 두 객체는 같을 수 없다.

하위 클래스 인스턴스가 동일하다고 판단되기를 원할 수도 있다. 이 때는 다른 구현을 추가해야 한다.


마지막으로 각 프로퍼티가 같은지 검사해야 한다.



-

서로 같은 객체는 같은 해시값을 반환해야 한다는 메서드 명세가 있다.

그러나 해시값이 같은 객체들이 꼭 같을 필요는 없다.

그러므로 isEqual: 을 재정의하면 반드시 hash 도 재정의해야 한다.



-

집합(set) 구현체는 객체를 서로 다른 배열에 넣기 위해 hash 를 사용한다.



-

hash 를 구현하는 간단한 방법은 아래와 같다.

- (NSUInteger) hash{

     NSString *stringToHash = [NSString stringWithFormat:@“%@:%@:%i”, _firstName, _lastName, _age];

     return [stringToHash hash];

}


이 방법의 단점은 느리다는 단점이 있다.

문자열을 새로 생성하는 부하가 있기 때문이다.

이 부하는 객체를 컬렉션에 추가할 때 또 다시 성능 문제를 야기한다.

객체가 컬렉션에 추가될 때 hash 를 계산하기 때문이다.






-

다음과 같이 hash 생성할 수도 있다.

- (NSUinteger)hash {

     NSUInteger firstNameHash = [_firstName hash];

     NSUInteger lastNameHash = [_lastName hash];

     NSUInteger ageHash = _age;

     return firstNameHash ^ lastNameHash ^ ageHash;

}


효율성을 확보하는 것과 적절한 크기의 해시값을 생성하는 것 사이에서 적절한 균형을 이룬다.

물론 이 알고리즘으로 생성된 해시는 충돌할 가능성이 있다.

그러나 적어도 다수의 값을 반환할 수 있다.

충돌이 발생하는 빈도와 해시값 계산에 드는 계산량의 균형점은 여러 방법을 직접 테스트해 보고 자신의 객체에 가장 적합한 방법을 찾아야 한다.




클래스 전용 동등 비교 메서드


-

NSString 이외에 특별 동등 비교 메서드를 제공하는 클래스는 NSArray(isEqualArray:), NSDictionary(isEqualDictionary:)가 있다.

두 메서드 다 비교하려는 객체가 각각 배열 또는 사전이 아니면 예외를 던질 것이다.

오브젝티브-c 는 컴파일 시간에 타입을 엄격하게 검사하지 않는다.

그래서 잘못된 타입의 객체를 전달하기 쉽다.

그러므로 전달하는 객체가 정확한 타입인지 꼭 확인해야 한다.



-

동등성 비교를 매우 자주 해야 한다면 전용 동등 메서드를 만들어야 한다는 생각이 들 수 있다.

그렇게 하면 객체를 비교할 때 타입을 체크하지 않아 속도향상의 이득을 많이 얻을 수 있다.

전용 메서드를 만드는 또 다른 이유는 코드를 좀 더 예쁘게 보이고 읽기 편하게 만들기 위해서다.



-

전용 메서드를 만들면 isEqual: 메서드도 재정의해야 한다.

isEqual: 메서드가 인자로 받은, 비교할 객체의 클래스가 리시버 객체 자신의 클래스와 동일하면 전용 메서드로 넘긴다.

같지 않다면 상위 클래스의 구현으로 전달하는게 일반적인 구현 패턴이다.




깊은 동등성 대 얕은 동등성


-

동등 비교 메서드를 새로 만들 때 객체 전체를 비교할지, 아니면 몇몇 필드만 비교할지 정해야 한다.

NSArray 는 모든 객체가 같다면 두 배열을 같은 것으로 간주한다.

이를 깊은 동등성(deep equality) 비교라고 한다.



-

반대로 필요한 부분만 체크하여 동등하다고 보는 것을

얕은 동등성(swallow equality) 비교라고 한다.




컨테이너 내부의 가별 클래스들의 동등성


-

객체를 컬렉션에 추가하고 나면 그것의 해시값을 변경할 수 없다.

분류된 객체의 해시값이 바뀌면 객체는 이제 잘못된 곳에 담겨 있는 상황이 된다.

객체의 가변 구역에서 해시는 제외하는 것을 보장하거나, 간단히 컬렉션 내부에 있는 객체는 변경하지 않게 해야 한다.



-

컬렉션에 포함된 객체를 수정했을 때 일어나는 일을 알고 있어야 한다는 것!

이런 일(즉, 컬렉션에 포함된 객체를 수정하는 일)을 절대로 하지 말라는 것이 아니라,

그렇게 했을 때 발생하는 잠재적인 문제점과 그에 따른 코드를 잘 알고 있어야 한다는 것.




기억할 점


객체의 동등성을 비교하려면 그 객체의 isEqual: 과 hash 메서드를 구현하라.


같은 객체는 항상 해시값이 같아야 한다.

그러나 같은 해시값을 가진 객체가 꼭 동일할 필요는 없다.


객체가 동등한지 비교할 때 모든 프로퍼티를 비교하지 말고 꼭 필요한 프로퍼티만 비교하라.


hash 메서드를 구현할 때 최대한 빠른 결과를 나올 수 있게 구현해야 하지만 해시값 충돌도 최소화해야 한다.




반응형

댓글