【iOS】 Foundation 对象复制

NSObject类提供了copy和mutableCopy的方法,通过这两个方法即可复制已有对象的副本。

copy与mutableCopy方法

  • ==copy方法用于复制对象的副本。==通常来说,mutableCopy方法总是返回对象的不可修改副本,即使该对象本身是可修改的。
  • ==multableCopy方法用于复制对象的可变副本。==通常来说,mutableCopy方法总是返回该对象可修改的副本,即使被复制的对象本身是不可修改的,调用mutableCopy方法复制出来的副本也是可以修改的。
  • 无论如何,copy和mutableCopy返回的总是原对象的副本,当程序对复制的副本进行修改时,原对象通常不会受到影响

示例:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        NSMutableString* book = [NSMutableString stringWithString:@"疯狂iOS讲义"];
        //复制book字符串的可变副本
        NSMutableString* bookCopy = [book mutableCopy];
        //修改副本,对原字符串没有任何影响
        [bookCopy replaceCharactersInRange:NSMakeRange(2, 3) withString:@"Android"];
        //此处看到原字符串的值没有发生变化
        NSLog(@"book的值为%@" , book);
        //字符串副本发生了改变
        NSLog(@"bookCopy的值为:%@" , bookCopy);
        NSString* str = @"fkit";
        //复制str(不可变字符串)的可变副本
        NSMutableString* strCopy = [str mutableCopy]; //1⃣️
        [strCopy appendString:@".org"];
        NSLog(@"%@" , strCopy);
        //调用book(可变字符串)的copy方法,程序返回一个不可修改的副本
        NSMutableString* bookCopy2 = [book copy]; //2⃣️
        //由于bookCopy2是不可修改的,因此下面的代码会出错
        //[boookCopy2 appendString:@"aa"];
    }
    return 0;
}

效果:
在这里插入图片描述

从上面的程序可以看出,当程序复制对象的副本后,对副本所做的任何修改,对原始对象都没有影响。
在上面的程序中,1⃣️号的代码复制了不可变字符串的可变副本,虽然被复制的str是不可变字符串,但调用mutableCopy方法得到的依然是可变副本。因此,程序可以对strCopy字符串进行修改。
2⃣️号的代码复制了可变字符串的不可变副本,虽然被复制的book是可变字符串,但调用copy得到的依然是不可变字符串。所以当修改时会出错。

NSCopying与NSMutableCopying协议

通过copy和mutableCopy方法复制对象的副本使用起来确实方便,那么自定义类是否可以调用copy与mutableCopy方法来复制副本呢???

//FKDog.h
#import <Foundation/Foundation.h>

@interface FKDog : NSObject
@property (nonatomic , strong) NSMutableString* name;
@property (nonatomic , assign) int age;
@end

//main.m
#import <Foundation/Foundation.h>
#import "FKDog.h"

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        FKDog* dog1 = [[FKDog alloc]init];
        dog1.name = [NSMutableString stringWithString:@"旺财"];
        dog1.age = 20;
        FKDog* dog2 = [dog1 copy];
    }
    return 0;
}

编译以上程序不会有任何问题,运行该程序将会看到以下错误:

[FKDog copyWithZone:]:unrecognized selector sent to instance 0x103087a10

上面的错误提示:FKDog类找不到copyWithZone:方法。
将代码改成以下代码:

FKDog2 = [dog1 mutableCopy];

编译仍然没有问题,但是会提示以下错误:

[FKDog mutableCopyWithZone:]: unrecognized selector sent to instance 0x103087a10

上面的错误提示:FKDog类找不到mutableCopyWithZone:方法。

从上面的代码看出,虽然NSObject提供了copy和mutableCopy方法,但自定义类并不能直接调用这两个方法来复制自身。

为了保证一个对象可以调用mutableCopy方法来复制自身的不可变副本,通常需要做如下事情:

  • 让该类实现NSCopying协议
  • 让该类实现copyWithZone:方法

与此同时,为了保证一个对象可以调用mutableCopy方法来复制自身的可变副本,通常需要做以下事情:

  • 让该类实现NSMutableCopying协议
  • 让该类实现copyWithZone:方法。

当程序调用对象的copy方法来复制自身时,程序底层需要调用copyWithZone:方法来完成实际的复制工作,copy返回的实际上就是copyWithZone:方法的返回值;当程序调用对象的mutableCopy方法来复制自身时,程序底层需要调用mutableCopyWithZone:方法来完成实际的复制工作,mutableCopy返回的实际上就是mutableCopyWithZone:方法的返回值。

//FKDog.m
#import "FKDog.h"
@implementation FKDog
-(id)copyWithZone:(NSZone*)zone
{
    
    
    NSLog(@"--执行copyWithZone:--");
    //使用zone参数创建FKDog对象
    FKDog* dog = [[[self class] allocWithZone:zone] init];
    dog.name = self.name;
    dog.age = self.age;
    return dog;
}
@end

main函数改成如下:

#import <Foundation/Foundation.h>
#import "FKDog.h"

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        FKDog* dog1 = [[FKDog alloc]init];
        dog1.name = [NSMutableString stringWithString:@"旺财"];
        dog1.age = 20;
        FKDog* dog2 = [dog1 copy];
        dog2.name = [NSMutableString stringWithString:@"snoopy"];
        dog2.age = 12;
        NSLog(@"dog1的名字为:%@" , dog1.name);
        NSLog(@"dog1的年龄为:%d" , dog1.age);
        NSLog(@"dog2的名字为:%@" , dog2.name);
        NSLog(@"dog2的名字为:%d" , dog2.age);
    }
    return 0;
}

效果:
在这里插入图片描述

从上面的程序可以看出,程序复制了dog1的副本,并将复制的副本赋给dog2变量,接下来可以对dog2的name、age属性重新赋值,这些赋值对dog1不会产生任何影响。

为什么此处调用copy时返回的依然是一个可变的FKDog对象呢?因为此处的FKDog没有提供对应的不可变类,自然无法复制不可变的FKDog对象。如果FKDog提供了不可变类,还是应当让FKDog的copyWithZone: 返回不可变的FKDog对象。

如果重写copyWithZone: 方法时,其父类已经实现了NSCopying协议,并且重写过copyWithZone: 方法,那么子类重写copyWithZone: 方法应该先调用父类的copy方法复制从父类继承到的成员变量,然后对子类中定义的成员变量进行赋值。

如果父类已经重写了copyWithZone:方法,那么子类重写copyWitnZone:方法的格式如下:

-(id) copyWithZone:(NSZone*)zone
{
    
    
    id obj = [super copy];
    //对子类定义的成员变量赋值
    ...
    return obj;
}

浅赋值与深复制

为了更好地去理解浅赋值与深复制的概念,先看如下程序:

#import <Foundation/Foundation.h>
#import "FKDog.h"

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        FKDog* dog1 = [[FKDog alloc]init];
        dog1.name = [NSMutableString stringWithString:@"旺财"];
        dog1.age = 20;
        FKDog* dog2 = [dog1 copy];
        [dog2.name replaceCharactersInRange:NSMakeRange(0, 2) withString:@"snoopy"];
        //查看dog2、dog1的name的属性值
        NSLog(@"dog2的name为:%@" , dog2.name);
        NSLog(@"dog1的name为:%@" , dog1.name);
    }
    return 0;
}

结果:
在这里插入图片描述

上面程序调用了dog1的copy方法复制了一个副本,并将该副本赋给dog2变量,接下来修改了dog2对象的name属性值,输出dog1、dog2两个对象的name属性值。

从输出可以看到,虽然只是修改了dog2的name属性值,但是为何dog1的name也发生了改变呢?

通过以下示意图来说明:
在这里插入图片描述

接下来程序复制了一个FKDog对象,查看copyWithZone:的代码,看到如下两行:

dog.name = self.name;
dog.age = self.age;

其中,dog代表复制出来的对象,此时程序将被复制对象的name赋值给dog的name。注意,name只是一个指针变量,该变量中存放的只是字符串的地址,并不是字符串本身。这样赋值的效果是让dog对象的name属性与被复制对象的name属性指向同一个字符串,此时的效果如图:
在这里插入图片描述

对于如图这种复制方式:当对象的属性是指针变量时,如果程序只是复制该指针的地址,而不是真正复制指针所指的对象,这种方式称为“浅复制”。对浅复制而言,在内存中复制了两个对象,这两个对象的指针变量将会指向同一个对象,也就是两个对象依然存在共用部分。

深复制则会采用与此不同的方式,==深复制不仅会复制对象本身,而且会“递归”复制每个指针类型的属性,直到两个对象没有任何共用部分。==如果将上面的FKDog的copyWithZone改为如下形式,即可实现深复制:

-(id) copyWithZone:(NSZone*)zone
{
    
    
    NSLog(@"--执行copyWithZone:--");
    //使用zone参数创建FKDog对象
    FKDog* dog = [[[self class] allocWithZone:zone] init];
    //将原对象的name实例变量复制一份副本后赋值给新对象的name实例变量
    dog.name = [self.name mutableCopy];
    dog.age = self.age;
    return dog;
}

上面程序没有直接将对象赋值,而是先将原对象的name属性值复制了一份可变副本,再将该副本的值赋给新对象的name属性,这样就实现了深复制。
一般来说,深复制的实现难度大很多,尤其是当该对象包含大量的指针类型属性时,如果某些属性所引用的对象再次包含指针类型的属性,那么实现深复制会更加复杂。
一般来说,Foundation框架中的类大部分都只实现了浅复制。

更加详细内容。

setter方法的复制选项

前面介绍合成setter方法和gatter方法时提到可以使用copy指示符,copy的作用是当程序调用setter方法复制时,将传入参数的副本赋给程序的实例变量。

//FKItem.h
#import <Foundation/Foundation.h>

@interface FKItem : NSObject
@property (nonatomic , copy) NSMutableString* name;
@end

//main.m
#import <Foundation/Foundation.h>
#import "FKItem.h"
int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        FKItem* item = [[FKItem alloc]init];
        item.name = [NSMutableString stringWithString:@"疯狂iOS讲义"];
        [item.name appendString:@"fkit"];
    }
    return 0;
}

编译该程序没有错,但运行该程序会提示如下错误:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendString:'

这段错误提示不允许修改item的name属性值,是因为程序定义name属性时使用的是copy指示符。也就是说,setName: 方法的代码如下:

- (void) setName: (NSMutableString*) aname
{
    
    
    name = [aname copy];
}

copy方法默认是复制该对象的不可变副本,虽然程序传入的是NSMutableString,但程序调用该参数的copy方法得到的是不可变副本。因此,程序赋给FKItem对象的name实例变量的值依然是不可变字符串。

注意:定义合成getter、setter方法时并没有提供mutableCopy指示符。因此即使定义实例变量时使用了可变类型,但只要使用copy指示符,实例变量实际得到的值总是不可变对象。

猜你喜欢

转载自blog.csdn.net/weixin_50990189/article/details/117636209