iOS NSDictionary 内部原理、 深浅复制、kvc与setObject问题

一:字典内部原理

二:key的copy问题

三:kvc与setobject forkey问题 / setValue forkey 和 setObject forkey问题

一:字典内部原理

NSDictionary使用原理

    1.NSDictionary(字典)是使用 hash表来实现key和value之间的映射和存储的, hash函数设计的好坏影响着数据的查找访问效率。

    - (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey;

   2.Objective-C 中的字典 NSDictionary 底层其实是一个哈希表,实际上绝大多数语言中字典都通过哈希表实现,

哈希的原理  (数据结构之哈希表

        散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

哈希概念:    

        哈希表的本质是一个数组,数组中每一个元素称为一个箱子(bin),箱子中存放的是键值对。

哈希表的存储过程:

    1. 根据 key 计算出它的哈希值 h。

    2. 假设箱子的个数为 n,那么这个键值对应该放在第 (h % n) 个箱子中(哈希函数的构造方法:这里采用除留余数法)。

    3. 如果该箱子中已经有了键值对,就使用开放寻址法或者拉链法解决冲突(hash函数冲突处理方法)。

    在使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。

    哈希表还有一个重要的属性: 负载因子(load factor),它用来衡量哈希表的 空/满 程度,一定程度上也可以体现查询的效率,计算公式为:

    负载因子 = 总键值对数 / 箱子个数

    负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,或者 0.75 等)时,哈希表将自动扩容。

重哈希概念:    

    哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key 的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。

    哈希表的扩容并不总是能够有效解决负载因子过大的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。

    总结,细心的读者可能会发现哈希表的两个问题:

   1. 如果哈希表中本来箱子就比较多,扩容时需要重新哈希并移动数据,性能影响较大。

   2. 如果哈希函数设计不合理,哈希表在极端情况下会变成线性表,性能极低。

其实,NSDictionary使用NSMapTable实现。NSMapTable同样是一个key-value的容器,下面是NSMapTable的部分代码:

@interface NSMapTable : NSObject {
   NSMapTableKeyCallBacks   *keyCallBacks;
   NSMapTableValueCallBacks *valueCallBacks;
   NSUInteger             count;
   NSUInteger             nBuckets;
   struct _NSMapNode  **buckets;
}

可以看出来NSMapTable是一个哈希+链表的数据结构,因此在NSMapTable中插入或者删除一对对象时

寻找的时间是O(1)+O(m),m最坏时可能为n。

O(1):为对key进行hash得到bucket的位置
O(m):遍历该bucket后面冲突的value,通过链表连接起来。(这个详细的看下面NSDctionnary的构造)

NSDictionary中的key Value遍历时是无序的,至如按照什么样的顺序,跟hash函数相关。NSMapTable使用NSObject的哈希函数。

-(NSUInteger)hash {
   return (NSUInteger)self>>4;
}

上述是NSObject的哈希值的计算方式,简单通过移位实现。右移4位,左边补0.
因为对象大多存于堆中,地址相差4位应该很正常。

好,下面讲解详细的字典内部取值和赋值的过程

Dictionary的构造 

(这个里用了拉链法解决hash函数冲突)

下面的代码我看看Dictionary在构造时都做了什么:

C#

private void Initialize(int capacity)
        {
            int prime = HashHelpers.GetPrime(capacity);
            this.buckets = new int[prime];
            for (int i = 0; i < this.buckets.Length; i++)
            {
                this.buckets[i] = -1;
            }
            this.entries = new Entry<TKey, TValue>[prime];
            this.freeList = -1;
        }

我们看到,Dictionary在构造的时候做了以下几件事:

  1. 初始化一个this.buckets = new int[prime]
  2. 初始化一个this.entries = new Entry<TKey, TValue>[prime]
  3. Bucket和entries的容量都为大于字典容量的一个最小的质数

其中this.buckets主要用来进行Hash碰撞,this.entries用来存储字典的内容,并且标识下一个元素的位置。

我们以Dictionary<int,string> 为例,来展示一下Dictionary如何添加元素:

首先,我们构造一个:

Dictionary<int, string> test = new Dictionary<int, string>(6);

初始化后:

查看大图

添加元素时,集合内部Bucket和entries的变化

Test.Add(4,”4″)后:

根据Hash算法: 4.GetHashCode()%7= 4,因此碰撞到buckets中下标为4的槽上,此时由于Count为0,因此元素放在Entries中第0个元素上,添加后Count变为1

查看大图

Test.Add(11,”11″)

根据Hash算法 11.GetHashCode()%7=4,因此再次碰撞到Buckets中下标为4的槽上,由于此槽上的值已经不为-1,此时Count=1,因此把这个新加的元素放到entries中下标为1的数组中,并且让Buckets槽指向下标为1的entries中,下标为1的entry之下下标为0的entries。

查看大图

Test.Add(18,”18″)

我们添加18,让HashCode再次碰撞到Buckets中下标为4的槽上,这个时候新元素添加到count+1的位置,并且Bucket槽指向新元素,新元素的Next指向Entries中下标为1的元素。此时你会发现所有hashcode相同的元素都形成了一个链表,如果元素碰撞次数越多,链表越长。所花费的时间也相对较多。

查看大图

Test.Add(19,”19″)

再次添加元素19,此时Hash碰撞到另外一个槽上,但是元素仍然添加到count+1的位置。

查看大图

删除元素时集合内部的变化

Test.Remove(4)

我们删除元素时,通过一次碰撞,并且沿着链表寻找3次,找到key为4的元素所在的位置,删除当前元素。并且把FreeList的位置指向当前删除元素的位置,FreeCount置为1

查看大图

Test.Remove(18)

删除Key为18的元素,仍然通过一次碰撞,并且沿着链表寻找2次,找到当前元素,删除当前元素,并且让FreeList指向当前元素,当前元素的Next指向上一个FreeList元素。

此时你会发现FreeList指向了一个链表,链表里面不包含任何元素,FreeCount表示不包含元素的链表的长度。

查看大图

Test.Add(20,”20″)

再添加一个元素,此时由于FreeList链表不为空,因此字典会优先添加到FreeList链表所指向的位置,添加后FreeCount减1,FreeList链表长度变为1

查看大图

总结:

通过以上试验,我们可以发现Dictionary在添加,删除元素按照如下方法进行:

  1. 通过Hash算法来碰撞到指定的Bucket上,碰撞到同一个Bucket槽上所有数据形成一个单链表
  2. 默认情况Entries槽中的数据按照添加顺序排列
  3. 删除的数据会形成一个FreeList的链表,添加数据的时候,优先向FreeList链表中添加数据,FreeList为空则按照count依次排列
  4. 字典查询及其的效率取决于碰撞的次数,这也解释了为什么Dictionary的查找会很快。

原文:Dictionary的内部实现

二:key的copy问题

看下面的代码

        NSMutableString *mustr = [NSMutableString stringWithString:@"zjy"];
        NSMutableDictionary *mudic = [NSMutableDictionary dictionary];
        mudic[mustr] = @"name";
        [mustr appendString:@"xxx"];
        NSLog(@"%@",mudic[@"mustr"]);

答案是

2018-08-27 11:32:26.091138+0800 lalal[1724:183662] (null)

那是为什么呢?

看打印结果

这里说明是值拷贝,不是浅拷贝,为啥呢,NSDictionary中的key是唯一的,key可以是遵循NSCopying 协议和重载- (NSUInteger)hash;- (BOOL)isEqual:(id)object;方法的任何对象。也就是说在NSDictionary内部,会对 aKey 对象 copy 一份新的。而 anObject 对象在其内部是作为强引用(retain或strong)。

  • hash 方法是用来计算该对象的 hash 值,最终的 hash 值决定了该对象在 hash 表中存储的位置。所以同样,如果想重写该方法,我们尽量设计一个能让数据分布均匀的 hash 函数。
  • isEqual : 方法是为了通过 hash 值来找到 对象 在hash �表中的位置。

在调用setObject: forKey: 后,内部会去调用 � key 对象的 hash 方法确定 object 在hash表内的入口位置,然后会调用 isEqual : 来确定该值是否已经存在于 NSDictionary中。

- (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey;

所以接着向下

可以看到原来的mustr值已经改变了,所以再用mustr取值,是取不到了。所以会报null。

看下面这个问题

        NSMutableString *mustr = [NSMutableString stringWithString:@"zjy"];
        NSMutableDictionary *mudic = [NSMutableDictionary dictionary];
        mudic[@"name"] = mustr;
        [mustr appendString:@"xxx"];
        NSLog(@"%@",mudic[@"name"]);

想想一下,答案是什么?

2018-08-27 17:54:11.560047+0800 lalal[10054:720413] zjyxxx

原因是什么呢?

为啥呢?因为字典没有对value进行nscoding协议啊。再接着看这个对mutabledic用copy得到结果是什么呢?

是__NSFrozenDictionaryM这个,不是mutableDic。


        NSMutableString *mustr = [NSMutableString stringWithString:@"zjy"];
        NSDictionary *mudic = [NSDictionary dictionaryWithObjectsAndKeys:mustr,@"23", nil];

        NSMutableDictionary *mudic3 = [mudic mutableCopy];
        [mudic3 setObject:@"34" forKey:@"23"];
        NSMutableDictionary *mudic4 = [mudic3 copy];
(lldb) p mudic
(__NSDictionaryI *) $0 = 0x0000000100502d50 2 key/value pairs
(lldb) p mudic4
(__NSFrozenDictionaryM *) $1 = 0x0000000100703110
(lldb) 

如果对mudic4进行添加keyvalue值,会报错。

2018-08-28 14:32:09.021331+0800 lalal[6840:399826] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSFrozenDictionaryM setObject:forKey:]: unrecognized selector sent to instance 0x1006121a0'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff2bb6032b __exceptionPreprocess + 171
	1   libobjc.A.dylib                     0x00007fff52cd2c76 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff2bbf8e04 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
	3   CoreFoundation                      0x00007fff2bad6870 ___forwarding___ + 1456
	4   CoreFoundation                      0x00007fff2bad6238 _CF_forwarding_prep_0 + 120
	5   lalal                               0x00000001000018cf main + 511
	6   libdyld.dylib                       0x00007fff538ec015 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

接着又查找了NSMutablestring和NSMutableArrary,发现copy之后,都是不可变对象,是NSString和NSArray。

        NSString *arrr = [NSString stringWithFormat:@"111"];
        NSMutableString *arr = [NSMutableString stringWithFormat:@"244"];
        NSMutableString *arr2 = [arr copy];
      
arr2	NSTaggedPointerString *	@"244"	0x0000000034343235
NSString	NSString	

所以结论如下

不可变对象在进行copy时,系统内部判断也是为了省内存,自动不会产生新的对象,是浅拷贝,

mutable类型在进行copy时,都产生了新对象,但是新对象缺都是不可变类型的,不能添加内容。

不可变和mutable类型在mutableCopy的时候,都会产生新对象,都会深拷贝,都会产生可变对象。但是注意,因为字典的key值遵守了NScoding协议,是会深拷贝的,但是里面的元素value,却都没有进行深拷贝,如果想深拷贝,需要重新写。

这个提示一点,在进行用mutable类型写属性的时候,不要用copy,要用strong。因为有误导性,出了问题,不容易找到。

现在讲NSDictionary的copy和strong问题

先看属性NSDictionary。

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, strong) NSMutableDictionary *mudicstrong;
@property (nonatomic, copy) NSMutableDictionary *mudiccopy;

@property (nonatomic, strong) NSDictionary *dicstrong;
@property (nonatomic, copy) NSDictionary *diccopy;
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {


        NSMutableArray *arr1 = [[NSMutableArray alloc] initWithObjects:@"aa",@"bb",@"cc", nil];
        NSMutableDictionary *dict0 = [[NSMutableDictionary alloc] initWithObjectsAndKeys:arr1,@"arr1", nil];
        NSDictionary *dict00 = [NSDictionary dictionaryWithDictionary:dict0];

        Person *person = [[Person alloc] init];
        person.dicstrong = dict00;
        person.diccopy = dict00;
    }
    return 0;
}
(lldb) p dict00
(__NSFrozenDictionaryM *) $0 = 0x0000000100404520
(lldb) p person.dicstrong
(__NSFrozenDictionaryM *) $1 = 0x0000000100404520
(lldb) p person.diccopy
(__NSFrozenDictionaryM *) $2 = 0x0000000100404520
(lldb) 

可以看出来NSDictionary 不论用copy还是strong,对它赋值不可变类型的字典,都不会发生变化。

好,现在把赋值的NSDictionary换成NSMutableDictionary。

        NSMutableDictionary *dict0 = [[NSMutableDictionary alloc] initWithObjectsAndKeys:arr1,@"arr1", nil];
        Person *person = [[Person alloc] init];
        person.dicstrong = dict0;
        person.diccopy = dict0;
(lldb) p dict0
(__NSDictionaryM *) $6 = 0x0000000100404500 1 key/value pair
(lldb) p person.dicstrong
(__NSDictionaryM *) $7 = 0x0000000100404500 1 key/value pair
(lldb) p person.diccopy
(__NSFrozenDictionaryM *) $8 = 0x0000000100504f60
(lldb) 

可以看到,对NSDictionary 赋值NSMutableDictionary,如果是strong修饰,则不变,依旧是可变字典,容易被篡改。如果是copy修饰,则重新赋值一份,深拷贝。所以,若想要不改变值,那不论管对方赋值的是可变的还是不可变的,均可用copy来修饰,以免被对方修改。

接下来看属性NSMutableDictionary。

int main(int argc, const char * argv[]) {
    @autoreleasepool {


        NSMutableArray *arr1 = [[NSMutableArray alloc] initWithObjects:@"aa",@"bb",@"cc", nil];
        NSMutableDictionary *dict0 = [[NSMutableDictionary alloc] initWithObjectsAndKeys:arr1,@"arr1", nil];
        NSDictionary *dict00 = [NSDictionary dictionaryWithDictionary:dict0];

        Person *person = [[Person alloc] init];
        person.mudicstrong = dict00;
        person.mudiccopy = dict00;
    }
    return 0;
}
(lldb) p dict00
(__NSFrozenDictionaryM *) $0 = 0x0000000100506080
(lldb) p person.mudicstrong
(__NSFrozenDictionaryM *) $1 = 0x0000000100506080
(lldb) p person.mudiccopy
(__NSFrozenDictionaryM *) $2 = 0x0000000100506080
(lldb) 

可以看到对于属性NSMutabledictionary,赋值的是不可变类型,则不论是copy还是strong,都是不可变类型,且指针未发生变化。

接着看赋值可变类型的字典

       NSMutableDictionary *dict0 = [[NSMutableDictionary alloc] initWithObjectsAndKeys:arr1,@"arr1", nil];
        Person *person = [[Person alloc] init];

        person.mudicstrong = dict0;
        person.mudiccopy = dict0;
(lldb) p dict0
(__NSDictionaryM *) $3 = 0x0000000100500300 1 key/value pair
(lldb) p person.mudicstrong
(__NSDictionaryM *) $4 = 0x0000000100500300 1 key/value pair
(lldb) p person.mudiccopy
(__NSFrozenDictionaryM *) $5 = 0x0000000100789110
(lldb) 

可以知道,属性NSMutableDictionary,赋值不可变字典。strong修饰的,结果是可变类型,并且地址未变。但是用copy修饰的,却变成了不可变字典,地址也发生了变化。

结论:

不论属性是可变的还是不可变的, 用不可变的赋值,则产生结果都是不可变的。地址都是一样的(strong和copy和原值)因为对不可变的内部会copy或者retain,这样的是不会产生新值。

属性不论是不可变的还是可变的,用可变的赋值,则产生的结果,strong修饰的跟原值地址一样,copy修饰的会产生新值,为不可变对象,内部对可变的strong的话依旧是retain,对copy的话,会产生不可变的对象。这个也是最上面说的那些。

   NSMutableArray *arr1 = [[NSMutableArray alloc] initWithObjects:@"aa",@"bb",@"cc", nil];
        NSMutableDictionary *dict0 = [[NSMutableDictionary alloc] initWithObjectsAndKeys:arr1,@"arr1", nil];
        NSDictionary *dict00 = [NSDictionary dictionaryWithDictionary:dict0];
        NSDictionary *dict00new = [NSDictionary dictionaryWithDictionary:dict00];


        NSMutableDictionary *mudic4 = [NSMutableDictionary dictionaryWithDictionary:dict00];
        NSMutableDictionary *mudic5 = [NSMutableDictionary dictionaryWithDictionary:mudic4];
(lldb) p dict00
(__NSFrozenDictionaryM *) $0 = 0x0000000100600b40
(lldb) p dict00new
(__NSFrozenDictionaryM *) $1 = 0x0000000100600b40
(lldb) p mudic4
(__NSDictionaryM *) $2 = 0x0000000102054d60 1 key/value pair
(lldb) p mudic5
(__NSDictionaryM *) $3 = 0x0000000102055f60 1 key/value pair
(lldb) 

结论:只有在不可变被赋值不可变时候copy时,才会不发生指针改变,在mutable被赋值可变的不可变的,在不可变的被赋值可变的,才会有新地址。才会copy。

赋值的过程是kvc的赋值过程,这个后续接着讲

下面讲如何对字典进行深拷贝

一、新建Objective-C category文件,我这Category填MutableDeepCopy,Category on填NSDictionary,所以生成的文件是NSDictionary+MutableDeepCopy.h和NSDictionary+MutableDeepCopy.m,生成的文件名很容易理解。
二、两文件源代码:
NSDictionary+MutableDeepCopy.h

#import 

@interface NSDictionary (MutableDeepCopy)
- (NSMutableDictionary *)mutableDeepCopy;
//增加mutableDeepCopy方法
@end


#import "NSDictionary+MutableDeepCopy.h"

@implementation NSDictionary (MutableDeepCopy)
- (NSMutableDictionary *)mutableDeepCopy
{
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:[self count]];
    //新建一个NSMutableDictionary对象,大小为原NSDictionary对象的大小
    NSArray *keys = [self allKeys];
    for(id key in keys)
    {//循环读取复制每一个元素
        id value = [self objectForKey:key];
        id copyValue;
        if ([value respondsToSelector:@selector(mutableDeepCopy)]) {
            //如果key对应的元素可以响应mutableDeepCopy方法(还是NSDictionary),调用mutableDeepCopy方法复制
            copyValue = [value mutableDeepCopy];
        } else if ([value respondsToSelector:@selector(mutableCopy)])
        {
            copyValue = [value mutableCopy];
        }
        if (copyValue == nil)
            copyValue = [value copy];
        [dict setObject:copyValue forKey:key];
        
    }
    return dict;
}
@end

测试:

#import 
#import "NSDictionary+MutableDeepCopy.h"
//导入头文件
int main (int argc, const char * argv[])
{

    @autoreleasepool {
         NSMutableArray *arr1 = [[NSMutableArray alloc] initWithObjects:@"aa",@"bb",@"cc", nil];
        NSDictionary *dict1 = [[NSDictionary alloc] initWithObjectsAndKeys:arr1,@"arr1", nil];
        NSLog(@"%p - %@",dict1,dict1);
        NSMutableDictionary *dict2 = [dict1 mutableCopy];
        //浅复制
        NSMutableDictionary *dict3 = [dict1 mutableDeepCopy];
        //深复制
        [arr1 addObject:@"dd"];
        NSLog(@"%p -%@",dict2,dict2);
        NSLog(@"%p -%@",dict3,dict3);

        NSLog(@"Hello, World!");
        
    }
    return 0;
}
2018-08-28 14:45:43.390614+0800 lalal[7014:430759] 0x102801de0 - {
    arr1 =     (
        aa,
        bb,
        cc
    );
}
2018-08-28 14:45:43.390896+0800 lalal[7014:430759] 0x100730280 -{
    arr1 =     (
        aa,
        bb,
        cc,
        dd
    );
}
2018-08-28 14:45:43.390934+0800 lalal[7014:430759] 0x100733520 -{
    arr1 =     (
        aa,
        bb,
        cc
    );
}

iOS/Objective-C开发 字典NSDictionary的深复制(使用category)

(OC中的字典实际上为一个数组 , 数组中的每个元素同样为一个链表实现的数组 ,也就是数组中套数组-文章)

三:kvc与setobject forkey问题 / setValue forkey 和 setObject forkey问题

看这个代码

@interface NSMutableDictionary<KeyType, ObjectType> : NSDictionary<KeyType, ObjectType>

- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;

@end

再看这个

@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)

/* Send -setObject:forKey: to the receiver, unless the value is nil, in which case send -removeObjectForKey:.
*/
- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;

@end

这两个可能会引起一些混淆,还有使用手法上也有,setvalueForkey是kvc用法,但是不同于直接自NSObject的kvc,这个看注释,也可知道,内部直接调取的是上面setobject这个方法来实现赋值。所以即可知道,为啥setvalue 的key没有遵守NSCoying协议,但是实际key也copy了一份新地址。

@interface NSMutableDictionary<KeyType, ObjectType> : NSDictionary<KeyType, ObjectType>
@end

@interface NSDictionary<__covariant KeyType, __covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
@end


@interface NSObject(NSKeyValueCoding)

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

看上面的继承及分类关系,可以知道,字典的kvc并不是调用NSObject的kvc。好,接着向下看。讲setObject和setValue的区别

setObject:forKey:

将给定的键值对添加到字典中

- (void)setObject:(ObjectType)anObject forKey:(id<NSCopying>)aKey

参数讲解:

anObject:

aKey的值,对该对象的强引用由字典维护

重点:如果anObject为nil的话,会抛出NSInvalidArgumentException异常,如果你想在字典中表示一个nil 值,可以使用 NSNull

比如:
[dict setObject:[NSNull null] forKey:@"null"];

打印:null = "<null>";

aKey:

valuekey,aKey将被复制(使用copyWithZone:方法;key必须遵守NSCopying协议),如果字典中存在了该key,将替换anObject

重点:如果key为nil的话,会抛出NSInvalidArgumentException异常

也就是使用setObject:forKey:方法会对value强引用,会使value的引用计数加一。

    NSMutableDictionary *dict = @{}.mutableCopy;
    NSString *key = @"test";
    NSMutableArray *arr = [[NSMutableArray alloc] init];
    
    printf("after init retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(arr)));
    
    [dict setObject:arr forKey:key];
    
    printf("after setObject:forKey: retain count = %ld\n ",CFGetRetainCount((__bridge CFTypeRef)(arr)));

打印信息:

after init retain count = 1
after setObject:forKey: retain count = 2

字典的KVC-setvalue forkey

使用KVC的方式也可以为一个可变字典添加一个键值对,

- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;

value

/* Send -setObject:forKey: to the receiver, unless the value is nil, in which case send -removeObjectForKey:.

*/

可以为nil,当value为 nil时,会调用字典的removeObjectForKey:方法,不为nil时,调用字典的setObject:forKey:方法来添加键值对

key:

需要注意的是,当使用kvc的时候,key必须是字符串

kvc在引用计数环境下,直接访问实例变量的话,value会被retain,

    NSMutableDictionary *dict = @{}.mutableCopy;
    NSMutableArray *arr = [[NSMutableArray alloc] init];
    
    printf("after init retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(arr)));
    
    [dict setObject:arr forKey:@"test"];
    
    printf("after setObject:forKey: retain count = %ld\n ",CFGetRetainCount((__bridge CFTypeRef)(arr)));
    
    // kvc 方式添加键值对
    [dict setValue:arr forKey:@"test2"];
    
    printf("after kvc retain count = %ld\n ",CFGetRetainCount((__bridge CFTypeRef)(arr)));

打印:

after init retain count = 1
after setObject:forKey: retain count = 2
after kvc retain count = 3

总结:
相同点:

  1. 两个方法的key都不能为nil,否则抛出NSInvalidArgumentException
  2. 都会对value强引用

不同点

  1. setObjec:forKey:key必须遵守NSCopying协议,KVC的key必须为字符串
  2. setObjec:forKey:value不能为空,否则会抛出NSInvalidArgumentException异常; KVC的value会nil时,会调用字典的removeObjectForKey:方法,否则,调用字典的setObject:forKey:方法添加键值对

取值

相应的,我们从字典中取值的时候,可以使用字典的objectForKey :方法,也可以使用valueForKey:方法。

这两种方法都比较简单,一般情况下,字典的valueForKey:方法也是调用objectForKey :来取值的,但这存在了一个前提:key不能以字符"@"开头

key不是以"@"开始时,调用字典的objectForKey:方法。如果以"@"开始的话,则去除掉"@"字符,并用剩余的字符调用[super valueForKey:]方法。当父类也没有找到该key时,会调用valueForUndefinedKey:方法,而valueForUndefinedKey:默认是抛出一个异常的。(只会去除开头的第一个"@",即如果key是以多个"@"字符开始的话,只会去除第一个开始的"@",剩余的"@"字符会被保留)

    NSMutableDictionary *dict = @{}.mutableCopy;
    NSMutableArray *arr = [[NSMutableArray alloc] init];
    NSString *key1 = @"@@@test";
    [dict setObject:arr forKey:key1];
    //取值
    id test2 = [dict valueForKey:key1];

控制台打印:

Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSDictionaryM 0x600000222a80> valueForUndefinedKey:]: this class is not key value coding-compliant for the key @@test.'

可以看到,我们本来是通过"@@@test"字符串作为key去取值的,但是在查找过程中去除了第一个"@"字符,用剩下的字符串"@@test"去作为key去查找的(不是把所有的"@"字符去除)。

另外,与赋值时不同,取值时keynil时并不会抛出异常。

删除

字典可以使用removeObjectForKey :方法删除某个键值对

- (void)removeObjectForKey:(KeyType)aKey;

key为nil的话, 会抛出NSInvalidArgumentException异常
当字典中不存在该key的时候,则什么都没做。

removeAllObjects
清空字典里的数据,其实是向字典中每个key以及对应的value发送release消息


这部分原文字:典的KVC与setObject:forKey:的区别

猜你喜欢

转载自blog.csdn.net/qq_27909209/article/details/82109876