iOS开发中的对象等同性

前言

Objective-C作为一门面向对象编程的语言,我们在使用它开发的时候,接触的最多的就是类与对象了。
我们在使用对象的时候,往往需要判断两个对象是否相等,这种相等包含两种含义:1.在程序上是否是同一块内存地址。2.在语义上,是否能指代同一对象。
这就是对象的等同性
关于如何判断两个对象是否相同,我们由浅至深通过代码逐渐分析:

目录

正文

首先,为了能够对对象进行方便操作,我们定义一个 Person 的类,其中含有 name 和 gender 两个属性,当两个 person 的 name 和 gender 同时一致时,我们认为这两个对象指代同一个人。

一. == 操作

第一步,既然要进行相等判断,就可以使用 == 来进行

    Person *person1 = [[Person alloc] init];
    Person *person2 = nil;

    person2 = person1;
    NSLog(@"person1 == person2: %d", person1 == person2);

此时的输出结果为:

person1 == person2: 1

这样就能表示两个 Person 是相等的吗? 并不是,看下面代码:

    Person *person1 = [[Person alloc] init];
    person1.name = @"张三";
    person1.gender = PersonGenderMan;

    Person *person2 = [[Person alloc] init];
    person2.name = @"张三";
    person2.gender = PersonGenderMan;

    NSLog(@"person1 == person2: %d", person1 == person2);

此时的输出结果为:

person1 == person2: 0

很显然,这并不能满足我们对于 Person 类相等的设定。同时也可以得出结论: == 操作只有分辨两个对象是否是同一内存地址的功能。

二. - isEqual: 与 hash

在Objective-C中,基本所有的对象都是继承自 NSObject 基类的,在 NSObject 基类的文档中,我们可以看到, NSObject 类实现了 NSObject 协议,而 NSObject 这个协议中,有 - (BOOL)isEqual:(id)object; 一个方法与 @property (readonly) NSUInteger hash; 这样一个属性需要类去实现,我们看一下官方文档对这两个东西的描述:
- isEqual:

Returns a Boolean value that indicates whether the receiver and a given object are equal.
Required.

hash

Returns an integer that can be used as a table address in a hash table structure.
Required.

大意就是说 - (BOOL)isEqual:(id)object; 这个方法是用来判断两个对象是否相等的。而 hash 是用在哈希表表结构的表地址。
由文档我们可知, - isEqual: 方法返回 YES ,那么 hash 必然相等; 如果 hash 相等,那么 - isEqual: 返回的不一定是 YES 。

三. - isEqual: 的系统实现

系统是如何实现 - isEqual: 的呢,看下面代码:

    Person *person1 = [[Person alloc] init];
    person1.name = @"张三";
    person1.gender = PersonGenderMan;

    Person *person2 = person1;

    Person *person3 = [[Person alloc] init];
    person1.name = @"张三";
    person1.gender = PersonGenderMan;

    NSLog(@"person1 == person2: %d", [person1 isEqual:person2]);
    NSLog(@"person1 == person3: %d", [person1 isEqual:person3]);

输出结果为:

person1 == person2: 1
person1 == person3: 0

由此可见,系统默认 NSObject 中 - isEqual: 的实现仅仅是对两个对象地址的判断。

四. - isEqual: 与 hash 的自定义实现

要自定义 - isEqual: 方法,我们可以这样考虑:
如果此时两个对象的内存地址一致,那么这两个对象的各个属性包括自身都是相等的,此时 - isEqual: 方法应该是这样的:

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }
    return NO;
}

然后,如果两个对象表示的不是一种实物,那么它们就没必要相同了,此时 - isEqual: 方法应该是这样的:

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    } else if ([self class] != [object class]) {
        return NO;
    }
    return NO;
}

接下来,有一种情况,如果 Person 有一个子类 Man ,如果是一个 Person 对象和一个 Man 对象进行对比,那么上段代码无疑是不相等的,所以在这个地方,我们要根据实际逻辑进行调整。现在假如 Person 与 Man 类无多余差别,那么修改之后 - isEqual: 应该是:

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

最后,我们根据 Person 类的只要 name 和 gender 相同,那么两个对象就相等的设定继续改进 - isEqual: 方法,修改之后应该是:

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    } else if (![object isKindOfClass:[self class]]) {
        return NO;
    }
    Person *person = (Person *)object;
    if ([self.name isEqualToString:person.name] &&
        self.gender == person.gender) {
        return YES;
    }
    return NO;
}

这样我们就完成了 - isEqual: 方法的自定义。接下来我们进行 hash 的自定义。
首先,既然 - isEqual: 方法返回YES时, hash 值相等, 而 hash 相等时, - isEqual: 不一定返回YES,那么 hash 可以这样实现:

- (NSUInteger)hash {
    return 9999;
}

为了验证这种实现方式的可靠性,我们对 - isEqual: 和 hash 插入一些打印:

- (BOOL)isEqual:(id)object {
    NSLog(@"%@->%@ call isEqual, param:%@", self.name, self, object);
    if (self == object) {
        return YES;
    } else if (![object isKindOfClass:[self class]]) {
        return NO;
    }
    Person *person = (Person *)object;
    if ([self.name isEqualToString:person.name] &&
        self.gender == person.gender) {
        return YES;
    }
    return NO;
}

- (NSUInteger)hash {
    NSLog(@"%@->%@ call hash", self.name, self);
    return 9999;
}

然后执行下面的代码:

    NSMutableSet *set = [NSMutableSet setWithCapacity:100];
    for (int i = 0; i < 5; i++) {
        Person *person = [[Person alloc] init];
        person.name = [NSString stringWithFormat:@"张三%d", i];
        person.gender = PersonGenderMan;
        [set addObject:person];
        NSLog(@"-----------------------------");
    }

控制台输出如下:

张三0-><Person: 0x60000023bf40> call hash
 -----------------------------
张三1-><Person: 0x60000023c160> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x60000023c160>
 -----------------------------
张三2-><Person: 0x60000023bf80> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x60000023bf80>
张三1-><Person: 0x60000023c160> call isEqual, param:<Person: 0x60000023bf80>
 -----------------------------
张三3-><Person: 0x604000036b60> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x604000036b60>
张三1-><Person: 0x60000023c160> call isEqual, param:<Person: 0x604000036b60>
张三2-><Person: 0x60000023bf80> call isEqual, param:<Person: 0x604000036b60>
 -----------------------------
张三4-><Person: 0x6040000379c0> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x6040000379c0>
张三1-><Person: 0x60000023c160> call isEqual, param:<Person: 0x6040000379c0>
张三2-><Person: 0x60000023bf80> call isEqual, param:<Person: 0x6040000379c0>
张三3-><Person: 0x604000036b60> call isEqual, param:<Person: 0x6040000379c0>
 -----------------------------

Set的实现原理为:将相同 hash 值的对象放在同一个数组,然后依次对比数组中每个对象,如果 - isEqual:返回 YES ,说明对象已经存在;如果 - isEqual: 全部返回 NO ,则证明对象不存在。
根据Set的实现原理,我们可知,如果 hash 值返回同一个值,那么向存在多个相同类对象的Set中添加新的对象,要进行多次的 - isEqual: 的调用,这样做是不值得的。

hash 值的目的是尽最大可能返回一个标识,但并不是一定要是唯一的。根据 Person 的设定,能标识 Person 的是 name 和 gender 属性,所以我们可以根据这两个属性来进行 hash 值的生成。

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

五. 保证 hash 的不可变

在我们将对象放入容器中后,就要保证对象 hash 的不可变,如果此时将对象的 hash 进行改变,那么容器就可能会有错误数据的出现,例如:

    Person *person1 = [[Person alloc] init];
    person1.name = @"张三";
    person1.gender = PersonGenderMan;

    Person *person2 = [[Person alloc] init];
    person2.name = @"李四";
    person2.gender = PersonGenderMan;

    NSMutableSet *set = [NSMutableSet setWithObjects:person1, person2, nil];
    NSLog(@"before set: %@", set);

此时输出为:

before set: {(
    <Person: 0x60400023d280>,
    <Person: 0x604000035800>
)}

接下来我们将其中一个对象改为与另一个对象一致:

    person2.name = @"张三";
    NSLog(@"after set: %@", set);

    NSLog(@"person1 hash: %zd", [person1 hash]);
    NSLog(@"person2 hash: %zd", [person2 hash]);
    NSLog(@"person1 isEqual person2: %d", [person1 isEqual:person2]);

此时的输出为:

after set: {(
    <Person: 0x60400023d280>,
    <Person: 0x604000035800>
)}
person1 hash: 32052695
person2 hash: 32052695
person1 isEqual person2: 1

此时根据两个对象的 hash 值以及 - isEqual: 方法的判断,两个对象是相等的,那么此时 Set 中的数据便存在错误。
我们对此时的 Set 进行一份拷贝:

NSSet *newSet = [set copy];
    NSLog(@"new set:%@", newSet);

结果为:

new set:{(
    <Person: 0x60400023d280>
)}

新 Set 中,相等的对象以及被剔除了。

所以我们在容器中使用对象时,要尽量避免对象 hash 值的不可变,或者说在将对象放入容器后不再改变对象的内容,这样才能避免容器中出现错误的数据。

参考

Effective+Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法

猜你喜欢

转载自blog.csdn.net/tugele/article/details/80079163
今日推荐