美文网首页闻道丶iOS(尝鲜版)iOS Developer
NSString是怎样比较字符串相等的

NSString是怎样比较字符串相等的

作者: 九昍 | 来源:发表于2017-03-23 17:20 被阅读0次

    版权声明:本文源自简书【九昍】,欢迎转载,转载请务必注明出处: http://www.jianshu.com/p/abb3b69172b9

    在平时开发的时候经常用到[NSString isEqualToString]这个方法,但是却不清楚它具体是怎么实现的,所以决定看下系统源码,了解一下它的内部实现。

    根据苹果官方Guide:【Concepts in Objective-C Programming: Introspection 】
    可以得知如果要重写isEqual: 则需要同时重写hash。这让我想当然的以为isEqual内部会使用hash做一次判断,然后再进行字符串比较,不过事实并非如此。

    通过翻看static Boolean __CFStringEqual(CFTypeRef cf1, CFTypeRef cf2) 的源码,可以得到下面的信息

    static Boolean __CFStringEqual(CFTypeRef cf1, CFTypeRef cf2) {
        CFStringRef str1 = (CFStringRef)cf1;
        CFStringRef str2 = (CFStringRef)cf2;
        const uint8_t *contents1;
        const uint8_t *contents2;
        CFIndex len1;
    
        contents1 = (uint8_t *)__CFStrContents(str1);
        contents2 = (uint8_t *)__CFStrContents(str2);
        len1 = __CFStrLength2(str1, contents1);
    
        //****X 首先判断长度是否相等
        if (len1 != __CFStrLength2(str2, contents2)) return false;
    
        contents1 += __CFStrSkipAnyLengthByte(str1);
        contents2 += __CFStrSkipAnyLengthByte(str2);
    
        //****X 根据两个字符串是否为Unicode编码分别做判断,判断方式是逐个取字符做对比
        if (__CFStrIsEightBit(str1) && __CFStrIsEightBit(str2)) { // 都不是Unicode
            return memcmp((const char *)contents1, (const char *)contents2, len1) ? false : true;
        } else if (__CFStrIsEightBit(str1)) {   /* One string has Unicode contents */
            CFStringInlineBuffer buf;
            CFIndex buf_idx = 0;
    
            CFStringInitInlineBuffer(str1, &buf, CFRangeMake(0, len1));
            for (buf_idx = 0; buf_idx < len1; buf_idx++) {
                if (__CFStringGetCharacterFromInlineBufferQuick(&buf, buf_idx) != ((UniChar *)contents2)[buf_idx]) return false;
            }
        } else if (__CFStrIsEightBit(str2)) {   /* One string has Unicode contents */
            CFStringInlineBuffer buf;
            CFIndex buf_idx = 0;
    
            CFStringInitInlineBuffer(str2, &buf, CFRangeMake(0, len1));
            for (buf_idx = 0; buf_idx < len1; buf_idx++) {
                if (__CFStringGetCharacterFromInlineBufferQuick(&buf, buf_idx) != ((UniChar *)contents1)[buf_idx]) return false;
            }
        } else {                    /* Both strings have Unicode contents */
            CFIndex idx;
            for (idx = 0; idx < len1; idx++) {
                if (((UniChar *)contents1)[idx] != ((UniChar *)contents2)[idx]) return false;
            }
        }
        return true;
    }
        
    

    既然hash方法不是用来提高isEqual:方法的效率的,那它的作用是什么?
    来看一下isEqual:和hash方法的实现:

    [NSObject hash]的实现

    uintptr_t _objc_rootHash(id obj)
    {
        return (uintptr_t)obj;
    }
    
    + (NSUInteger)hash {
        return _objc_rootHash(self);
    }
    

    [NSObject isEqual:]的实现

    - (BOOL)isEqual:(id)obj {
        return obj == self;
    }
    

    可以看出,对于NSObject无论是hash还是isEqual方法,都是用对象地址作为依据,所以对于NSObject,如果hash值相同isEqual:的返回值就是YES。

    如果我们需要实现自定义的isEqual该怎么做,首先看下apple官方demo的实现

    - (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;
    }
    

    官方的推荐是实现一个isEqualToType:方法,然后在isEqual内部调用isEqualToType方法,并且在isEqual内部检查对象类型及合法性。

    当修改完isEqual以后,如果不修改hash方法,那么此时若有两个不同的MyWidget对象,并且他们的name、data相同,此时他们的hash仍然是取的自身地址,此时hash不相等,不满足对象相等hash一定相等的原则。

    @implementation Model
    
    - (id)copyWithZone:(nullable NSZone *)zone {
        
        Model *result = [[[self class] allocWithZone:zone] init];
        
        result.firstName = self.firstName;
        result.lastName  = self.lastName;
        
        return result;
    }
    
    - (NSUInteger)hash {
        NSLog(@"%@ call %s", self, __func__);
        return NSUINTROTATE([_firstName hash], NSUINT_BIT / 2) ^ [_lastName hash];
    }
    
    - (BOOL)isEqual:(id)other {
        NSLog(@"%@ call %s", self, __func__);
        if (other == self)
            return YES;
        if (!other || ![other isKindOfClass:[self class]])
            return NO;
        return [self isEqualToModel:other];
    }
    
    - (BOOL)isEqualToModel:(Model *)other {
        if (self == other)
            return YES;
        if (![(id)[self firstName] isEqual:[other firstName]])
            return NO;
        if (![[self lastName] isEqual:[other lastName]])
            return NO;
        return YES;
    }
    @end
    
    -----------------------------------------------------------------
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        Model *model1 = [[Model alloc] init];
        model1.firstName = @"A";
        model1.lastName = @"B";
        Model *model2 = [[Model alloc] init];
        model2.firstName = @"A";
        model2.lastName = @"B";
        
        NSMutableDictionary *mutDict = [NSMutableDictionary dictionary];
        
        [mutDict setObject:@"" forKey:model1];
        
        NSLog(@"**********************************");
        [mutDict setObject:@"" forKey:model2];
    
    程序运行后打印的log如下:
    
    2016-12-19 16:03:27.236 test[28108:2027961] <Model: 0x618000024bc0> call -[Model hash]
    2016-12-19 16:03:27.236 test[28108:2027961] <Model: 0x610000024860> call -[Model hash]
    2016-12-19 16:03:27.236 test[28108:2027961] **********************************
    2016-12-19 16:03:27.237 test[28108:2027961] <Model: 0x618000024c60> call -[Model hash]
    2016-12-19 16:03:27.237 test[28108:2027961] <Model: 0x610000024860> call -[Model isEqual:]
    
    首先解释下为什么第一次setObject:forKey:调用了两次hash方法第一次调用hash方法是为了获取hash值从而做进一步判断,这是因为我们没有指定字典的Capacity,这种情况下Capacity的值为0,这时候我们向字典里添加元素,字典空间不够用,会重新申请空间,此时需要在新申请的控件通过hash确定对象的地址,所以第二次调用hash这个方法。
    
    从上面的log可以看到在以hash表为基础的对象(NSDictionary、NSSet等)中存储数据时,若hash相等则会调用对象的isEqual方法进一步判断对象是否相等,从而确定对象是否已经存在,如果我们只修改isEqual方法,则可能会出现两个isEqual的对象由于hash值不相等导致被误判为两个不相等对象的情况,这就是为什么必须要同时修改isEqual:和hash方法最重要的原因。
    

    相关文章

      网友评论

        本文标题:NSString是怎样比较字符串相等的

        本文链接:https://www.haomeiwen.com/subject/ggrtottx.html