Việc so sánh sự bằng nhau của 2 object là vô cùng hữu ích. Tuy nhiên, nên tránh so sánh chúng bằng toán tử ==, vì như vậy là chúng ta đang so sánh 2 con trỏ chứ không phải là 2 object mà con trỏ trỏ tới. Thay vào đó, ta nên dùng hàm isEqual: được định nghĩa trong NSObject protocol. Thông thường, 2 object thuộc 2 class khác nhau thì không bằng nhau. Một vài object cung cấp các hàm đặc biệt để kiểm tra tính bằng nhau của hai object nếu chúng cùng một class. Ví dụ:
NSString *foo = @"Foo 123"; NSString *bar = [NSString stringWithFormat:@"Foo %i",123]; BOOL equalA = (foo == bar); // NO BOOL equalB = [foo isEqual:bar]; // YES BOOL equalC = [foo isEqualToString:bar]; // YES
Ở ví dụ này, ta có thể thấy được sự khác biệt giữa toán tử == và các hàm kiểm tra sự bằng nhau. NSString là một ví dụ của một class implement hàm kiểm tra tính bằng nhau của riêng mình, cụ thể là hàm isEqualToString:. Tham số truyền vào phải là một NSString, nếu không kết quả trả về là undefined. Hàm này sẽ nhanh hơn hàm isEqual: do đã biết trước kiểu của tham số.
Hai hàm quan trọng nhất trong việc kiểm tra tính bằng nhau từ NSObject protocol là:
- (BOOL)isEqual:(id)object; - (NSUInteger)hash;
Implementation mặc định của hai hàm này từ NSObject class làm việc như sau: 2 object bằng nhau khi và chỉ khi giá trị con trỏ của chúng hoàn toàn giống nhau. Bất kể 2 object nào mà được xác định là bằng nhau sử dụng hàm isEqual: đều trả về giá trị giống nhau trong hàm hash. Tuy nhiên, hai hàm cùng trả về giá trị giống nhau trong hàm hash không nhất thiết phải bằng nhau theo hàm isEqual:. Ví dụ:
#import @interface Person : NSObject @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @property (nonatomic, assign) NSUInteger age; @end
Hai object Person bằng nhau nếu tất cả các property đều bằng nhau. Hàm isEqual: được viết như sau:
- (BOOL)isEqual:(id)object { if (self == object) { return YES; } if ([self class] != [object class]) { return NO; } Person *otherPerson = (Person *)object; if (![_firstName isEqualToString:otherPerson.firstName]) { return NO; } if (![_lastName isEqualToString:otherPerson.lastName]) { return NO; } if (_age == otherPerson.age) { return NO; } return YES; }
Đầu tiên, kiểm tra hai con trỏ của 2 object, nếu chúng giống nhau, 2 object bằng nhau. Sau đó, kiểm tra class của hai object, nếu chúng khác nhau, hai object không thể bằng nhau. Cuối cùng, kiểm tra các property của 2 object, nếu bất cứ property nào khác nhau, 2 object không bằng nhau, nếu không, chúng bằng nhau.
Do 2 object bằng nhau phải trả về cùng một giá trị hash, nhưng 2 object có cùng một hash chưa chắc đã bằng nhau. Vì vậy, chúng ta cần phải override hàm hash nếu đã override hàm isEqual:. Một hàm hash hoàn toàn có thể viết như sau:
- (NSUInteger)hash { return 1000; }
Tuy nhiên, viết như trên có thể ảnh hưởng đến perfomance khi thêm một object vào một collection, do hash được sử dụng làm index trong hash table mà collection sử dụng. Một set implementation có thể sử dụng hash để đặt object vào trong các array khác nhau. Khi một object được thêm vào set, array tương ứng với hash của nó được liệt kê để xem có bất cứ object nào trong array bằng nhau. Nếu có, object đã có trong set. Vì vậy, nếu ta return cùng một giá trị hash cho tất cả object và thêm vào 1000000 object vào set, mỗi lần sau đó, khi ta thêm vào một object, set phải quét tất cả 1000000 object đó. Một cách viết khác của hàm hash như sau:
- (NSUInteger)hash { NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i", _firstName,_lastName,_age]; return [stringToHash hash]; }
Viết như trên hoàn toàn hợp lệ vì khi hai object bằng nhau sẽ trả về cùng một giá trị hash. Tuy nhiên, cách này còn chậm hơn cả cách trả về cùng một giá trị hash cho tất cả object vì phải tạo ra một string. Nó có thể tạo ra vấn đề về perfomance khi thêm vào collection do hash phải được tính toán cho object được thêm vào collection. Cách cuối cùng được viết như sau:
- (NSUInteger)hash { NSUInterger firstNameHash = [_firstName hash]; NSUInterger lastNameHash = [_lastName hash]; NSUInterger ageHash = _age; return firstNameHash ^ lastNameHash ^ ageHash; }
Cách này đứng giữa sự hiệu quả và việc tạo ra ít nhất một vài khoảng hash. Vì vậy, tất nhiên, sẽ có sự xung đột giữa các hash tạo ra bằng cách này, nhưng ít nhất, nhiều giá trị trả về là có thể. Sự cân bằng giữa tần số xung đột giữa các hash và perfomance của hàm hash là điều mà bạn nên thí nghiệm và xem cái gì phù hợp với object của mình.
Các hàm isEqual: cụ thể cho một class
Ngoài NSString class, các class mà cung cấp hàm so sánh bằng nhau cụ thể là NSArray (isEqualToArray:), NSDictionary (isEqualToDictionary:). Cả hai hàm đều throw một exception nếu object truyền vào không phải array hay dictionary. Objective-C không có kiểm tra strong type tại compile time, vì vậy, ta có thể dễ dàng vô tình truyền vào một object với kiểu sai. Vì vậy, cần chắc chắn rằng kiểu truyền vào là kiểu đúng.
Bạn có thể quyết định tạo riêng một hàm kiểm tra tính bằng nhau cho một class cụ thể, nếu việc kiểm tra là thường xuyên. Lý do khác nữa là nhìn hàm đó sẽ dễ đọc hơn. Nếu như đã quyết định tạo một hàm riêng, vẫn nên override hàm isEqual. Ví dụ, class Person có thể viết như sau:
- (BOOL)isEqualToPerson:(Person*)person{ if (self == person) { return YES; } if (![_firstName isEqualToString:person.firstName]) { return NO; } if (![_lastName isEqualToString:person.lastName]) { return NO; } if (_age == person.age) { return NO; } return YES; } - (BOOL)isEqual:(id)object { if ([self class] == [object class]) { return [self isEqualToPerson:object]; }else{ return [super isEqual:object]; } }
Deep Equality vs Shallow Equality
Khi tạo một hàm kiểm tra tính bằng nhau riêng, cần quyết định xem sẽ kiểm tra toàn bộ object hay chỉ một vài trường. NSArray kiểm tra hai array có bằng nhau không bằng cách kiểm tra số lượng phần tử của array, nếu bằng nhau, kiểm tra lần lượt từng phần từ bằng cách gọi hàm isEqual:. Nếu tất cả phần từ bằng nhau, hai array bằng nhau, còn được gọi là Deep Equality. Tuy nhiên, đôi lúc, bạn chắc chắn rằng chỉ cần so sánh một số phần dữ liệu trong object là đủ xác định tính bằng nhau, nó vẫn hợp lệ nếu bạn không kiểm tra toàn bộ object.
Ví dụ, ở class Person, nếu object lấy ra từ một cơ sở dữ liệu, thường sẽ có thêm một property là identifier để sử dụng làm primary key trong cơ sở dữ liệu.
@property NSUInteger identifier;
Trong trường hợp này, bạn có thể quyết định chỉ cần kiểm tra nếu hai identifier trùng nhau thì hai object bằng nhau.
Override hàm isEqual: và hàm hash nếu muốn kiểm tra tính bằng nhau của object
Hai object bằng nhau có cùng hash nhưng hai object có cùng hash chưa chắc đã bằng nhau
Xác định xem những gì cần thiết để kiểm tra sự bằng nhau hơn là kiểm tra toàn bộ các property
Viết một hàm hash mà có perfomance chấp nhận được