iOS面試之@property

acBool發表於2019-03-02

原文連結

@property介紹

相信做過iOS開發的同學都使用過@property,@property翻譯過來是屬性。在定義一個類時,常常會有多個@property,有了@property,我們可以用來儲存類的一些資訊或者狀態。比如定義一個Student類:

@interface Student : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy) NSString *sex;

@end
複製程式碼

Student類中有兩個屬性,分別是name和sex。

在程式中使用時,可以使用

self.name = @"xxx";
self.sex = @"xxx";
複製程式碼

那麼,為什麼可以這樣用呢?self.name是self的name變數嘛?還是其他的什麼?屬性中的copy代表什麼?nonatomic呢?下面來看一下這些問題的答案。

@property 本質

@property到底是什麼呢?實際上@property = 例項變數 + get方法 + set方法。也就是說屬性

@property (nonatomic, copy) NSString *name;
複製程式碼

代表的有例項變數,get方法和set方法。如果大家用過Java,相信對set方法和get方法應該很熟悉,這裡的set、get方法和Java裡面的作用是一樣的,get方法用來獲取變數的值,set方法用來設定變數的值。使用@property生成的例項變數、get方法、set方法的命名有嚴格的規範,例項變數的名稱、get方法名、set方法名稍後再介紹。

這裡需要注意的是,包括例項變數、get方法和set方法,不會真的出現在我們的編輯器裡面,使用屬性生成的例項變數、get方法、set方法是在編譯過程中生成的。下面介紹一下set方法、get方法以及自動生成的例項變數。

setter方法

set方法也可以稱為setter方法,之後看到setter方法直接理解成set方法即可。同理,get方法也被稱為getter方法。

還是以上面的屬性:

@property (nonatomic, copy) NSString *name;
複製程式碼

為例,屬性name生成的setter方法是

- (void)setName:(NSString *)name;
複製程式碼

該命名方法是固定的,是約定成束的。如果屬性名是firstName,那麼setter方法是:

- (void)setFirstName:(NSString *)firstName;
複製程式碼

專案中,很多時候會有重寫setter方法的需求,只要重寫對應的方法即可。比如說重寫name屬性的setter方法:

- (void)setName:(NSString *)name
{
    NSLog(@"rewrite setter");
    _name = name;
}
複製程式碼

關於_name是什麼,後續會介紹。

getter方法

以屬性

@property (nonatomic, copy) NSString *name;
複製程式碼

為例,編譯器自動生成的getter方法是

- (NSString *)name;
複製程式碼

getter方法的命名也是固定的。如果屬性名是firstName,那麼getter方法是:

- (NSString *)firstName;
複製程式碼

重寫getter方法:

- (NSString *)name
{
    NSLog(@"rewrite getter");
    return _name;
}
複製程式碼

如果我們定義了name屬性,並且按照上面所述,重寫了getter方法和setter方法,Xcode會提示如下的錯誤:

Use of undeclared identifier '_name'; did you mean 'name'?
複製程式碼

稍後我們再解釋為何會有該錯誤,以及如何解決。先來看一下_name到底是什麼。

例項變數

既然@property = 例項變數 + getter + setter,那麼屬性所生成的例項變數名是什麼呢?根據上面的例子,也很容易猜到,專案中也經常使用,例項變數的名稱就是_name。例項變數的命名也是有固定格式的,下劃線+屬性名。如果屬性是@property firstName,那麼生成的例項變數就是_firstName。這也是為何我們在setter方法和getter方法,以及其他的方法中可以使用_name的原因。

這裡再提一下,無論是例項變數,還是setter、getter方法,命名都是有嚴格規範的。正是因為有了這種規範,編譯器才能夠自動生成方法,這也要求我們在專案中,對變數的命名,方法的命名遵循一定的規範。

自動合成

定義一個@property,在編譯期間,編譯器會生成例項變數、getter方法、setter方法,這些方法、變數是通過自動合成(autosynthesize)的方式生成並新增到類中。實際上,一個類經過編譯後,會生成變數列表ivar_list,方法列表method_list,每新增一個屬性,在變數列表ivar_list會新增對應的變數,如_name,方法列表method_list中會新增對應的setter方法和getter方法。

動態合成

既然有自動合成,那麼相對應的就要有非自動合成,非自動合成又稱為動態合成。定義一個屬性,預設是自動合成的,預設會生成getter方法和setter方法,這也是為何我們可以直接使用self.屬性名的原因。實際上,自動合成對應的程式碼是:

@synthesize name = _name;
複製程式碼

這行程式碼是編譯器自動生成的,無需我們來寫。相應的,如果我們想要動態合成,需要自己寫如下程式碼:

@dynamic sex;
複製程式碼

這樣程式碼就告訴編譯器,sex屬性的變數名、getter方法、setter方法由開發者自己來新增,編譯器無需處理。

那麼這樣寫和自動合成有什麼區別呢?來看下面的程式碼:

Student *stu = [[Student alloc] init];
stu.sex = @"male";
複製程式碼

編譯,不會有任何問題。執行,也沒問題。但是當程式碼執行到這一行的時候,程式崩潰了,崩潰資訊是:

[Student setSex:]: unrecognized selector sent to instance 0x60000217f1a0
複製程式碼

即:Student沒有setSex方法,沒有屬性sex的setter方法。這就是動態合成和自動合成的區別。動態合成,需要開發者自己來寫屬性的setter方法和getter方法。新增上setter方法:

- (void)setSex:(NSString *)sex
{
    _sex = sex;
}
複製程式碼

由於使用@dynamic,編譯器不會自動生成變數,因此除此之外,還需要手動定義_sex變數,如下:

@interface Student : NSObject
{
    NSString *_sex;
}

@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy) NSString *sex;

@end
複製程式碼

現在再編譯,執行,執行沒有錯誤和崩潰。

重寫setter、getter方法的注意事項

上面的例子中,重寫了屬性name的getter方法和setter方法,如下:

- (void)setName:(NSString *)name
{
    NSLog(@"rewrite setter");
    _name = name;
}

- (NSString *)name
{
    NSLog(@"rewrite getter");
    return _name;
}
複製程式碼

但是編譯器會提示錯誤,錯誤資訊如下:

Use of undeclared identifier '_name'; did you mean 'name'?
複製程式碼

提示沒有_name變數。為什麼呢?我們沒有宣告@dynamic,那預設就是@autosynthesize,為何沒有_name變數呢?奇怪的是,倘若我們把getter方法,或者setter方法註釋掉,gettter、setter方法只留下一個,不會有錯誤,為什麼呢?

還是編譯器做了些處理。對於一個可讀寫的屬性來說,當我們重寫了其setter、getter方法時,編譯器會認為開發者想手動管理@property,此時會將@property作為@dynamic來處理,因此也就不會自動生成變數。解決方法,顯示的將屬性和一個變數繫結:

@synthesize name = _name;
複製程式碼

這樣就沒問題了。如果一個屬性是隻讀的,重寫了其getter方法時,編譯器也會認為該屬性是@dynamic,關於可讀寫、只讀,下面會介紹。這裡提醒一下,當專案中重寫了屬性的getter方法和setter方法時,注意下是否有編譯的問題。

修改例項變數的名稱

使用自動合成時,針對

@property (nonatomic, copy) NSString *name;
複製程式碼

屬性,生成的變數名是_name。倘若,不習慣使用下劃線開頭的變數名,能否指定屬性對應的變數名呢?答案是可以的,使用的是上面介紹過的@synthesize關鍵字。如下:

@synthesize name = stuName;
複製程式碼

這樣,name屬性生成的變數名就是stuName,後續使用時需要寫stuName,而不是_name。如getter、setter方法:

- (void)setName:(NSString *)name
{
    NSLog(@"rewrite setter");
    stuName = name;
}

- (NSString *)name
{
    NSLog(@"rewrite getter");
    return stuName;
}
複製程式碼

注意:雖然可以使用@synthesize關鍵字修改變數名,但是如無特殊需求,不建議這樣做。因為預設情況下編譯器已經為我們生成了變數名,大多數的專案、開發者也都會遵循這樣的規範,既然蘋果已經定義了一個好的規範,為什麼不遵守呢?

getter方法中為何不能用self.

有經驗的開發者應該都知道這一點,在getter方法中是不能使用self.的,比如:

- (NSString *)name
{
    NSLog(@"rewrite getter");
    return self.name;  // 錯誤的寫法,會造成死迴圈
}
複製程式碼

原因程式碼註釋中已經寫了,這樣會造成死迴圈。這裡需要注意的是:self.name實際上就是執行了屬性name的getter方法,getter方法中又呼叫了self.name, 會一直遞迴呼叫,直到程式崩潰。通常程式中使用:

self.name = @"aaa";
複製程式碼

這樣的方式,setter方法會被呼叫。

@property修飾符

當我們定義一個字串屬性時,通常我們會這樣寫:

@property (nonatomic, copy) NSString *name;
複製程式碼

當我們定義一個NSMutableArray型別的屬性時,通常我們會這樣寫:

@property (nonatomic, strong) NSMutableArray *books;
複製程式碼

而當我們定一個基本資料型別時,會這樣寫:

@property (nonatomic, assign) int age;
複製程式碼

定義一個屬性時,nonatomic、copy、strong、assign等被稱作是關鍵字,或者是修飾符。

修飾符種類

修飾符有四種:

  1. 原子性。原子性有nonatomic、atomic兩個值,如果不寫nonatomic,那麼預設是atomic的。如果屬性是atomic的,那麼在訪問其getter和setter方法之前,會有一些判斷,大概是判斷是否可以訪問等,這裡系統使用的是自旋鎖。由於使用atomic並不能絕對保證執行緒安全,且會耗費一些效能,因此通常情況下都使用nonatomic。
  2. 讀寫許可權。讀寫許可權有兩個取值,readwrite和readonly。宣告屬性時,如果不指定讀寫許可權,那麼預設是readwrite的。如果某個屬性不想讓其他人來寫,那麼可以設定成readonly。
  3. 記憶體管理。記憶體管理的取值有assign、strong、weak、copy、unsafe_unretained。
  4. set、get方法名。如果不想使用自動合成所生成的setter、getter方法,宣告屬性時甚至可以指定方法名。比如指定getter方法名:
@property (nonatomic, assign, getter=isPass) BOOL pass;
複製程式碼

屬性pass的getter方法就是

- (BOOL)isPass;
複製程式碼

預設修飾符

宣告屬性時,如果不顯示指定修飾符,那麼預設的修飾符是哪些呢?或者說未指定的修飾符,預設取值是什麼呢?如果是基本資料型別,預設取值是:

atomic,readwrite,assign
複製程式碼

如果是Objective-C物件,預設取值是:

atomic,readwrite,strong
複製程式碼

atomic是否是執行緒安全的

上面提到了,宣告屬性時,通常使用nonatomic修飾符,原因就是因為atomic並不能保證絕對的執行緒安全。舉例來說,假設有一個執行緒A在不斷的讀取屬性name的值,同時有一個執行緒B修改了屬性name的值,那麼即使屬性name是atomic,執行緒A讀到的仍舊是修改後的值,可見不是執行緒安全的。如果想要實現執行緒安全,需要手動的實現鎖。下面是一段示例程式碼:

宣告name屬性,使用atomic修飾符

@property (atomic, copy) NSString *name;
複製程式碼

對屬性name賦值。同時,一個執行緒在不斷的讀取name的值,另一個執行緒在不斷的設定name的值:

stu.name = @"aaa";
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for(int i = 0 ; i < 1000; ++i){
        NSLog(@"stu.name = %@",stu.name);
    }
});
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    stu.name = @"bbb";
});
複製程式碼

看一下輸出:

2018-12-06 15:42:26.837215+0800 TestClock[15405:175815] stu.name = aaa
2018-12-06 15:42:26.837837+0800 TestClock[15405:175815] stu.name = bbb
複製程式碼

證實了即使使用了atomic,也不能保證執行緒安全。

weak和assign區別

經常會有面試題問weak和assign的區別,這裡介紹一下。

weak和strong是對應的,一個是強引用,一個是弱引用。weak和assign的區別主要是體現在兩者修飾OC物件時的差異。上面也介紹過,assign通常用來修飾基本資料型別,如int、float、BOOL等,weak用來修飾OC物件,如UIButton、UIView等。

基本資料型別用weak來修飾

假設宣告一個int型別的屬性,但是用weak來修飾,會發生什麼呢?

@property (nonatomic, weak) int age;
複製程式碼

Xcode會直接提示錯誤,錯誤資訊如下:

Property with 'weak' attribute must be of object type
複製程式碼

也就是說,weak只能用來修飾物件,不能用來修飾基本資料型別,否則會發生編譯錯誤。

物件使用assign來修飾

假設宣告一個UIButton型別的屬性,但是用assign來修飾,會發生什麼呢?

@property (nonatomic, assign) UIButton *assignBtn;
複製程式碼

編譯,沒有問題,執行也沒有問題。我們再宣告一個UIButton,使用weak來修飾,對比一下:

@interface ViewController ()

@property (nonatomic, assign) UIButton *assignBtn;

@property (nonatomic, weak) UIButton *weakButton;

@end
複製程式碼

正常初始化兩個button:

UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100,100,100,100)];
[btn setTitle:@"Test" forState:UIControlStateNormal];
btn.backgroundColor = [UIColor lightGrayColor];
self.assignBtn = btn;
self.weakButton = btn;
複製程式碼

此時列印兩個button,沒有區別。釋放button:

btn = nil;
複製程式碼

釋放之後列印self.weakBtn和self.assignBtn

NSLog(@"self.weakBtn = %@",self.weakButton);
NSLog(@"self.assignBtn = %@",self.assignBtn);
複製程式碼

執行,執行到self.assignBtn的時候崩潰了,崩潰資訊是

 EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
複製程式碼

weak和assign修飾物件時的差別體現出來了。

weak修飾的物件,當物件釋放之後,即引用計數為0時,物件會置為nil

2018-12-06 16:17:05.774298+0800 TestClock[15863:192570] self.weakBtn = (null)
複製程式碼

而向nil傳送訊息是沒有問題的,不會崩潰。

assign修飾的物件,當物件釋放之後,即引用計數為0時,物件會變為野指標,不知道指向哪,再向該物件發訊息,非常容易崩潰。

因此,當屬性型別是物件時,不要使用assign,會帶來一些風險。

堆和棧

上面說到,屬性用assign修飾,當被釋放後,容易變為野指標,容易帶來崩潰問題,那麼,為何基本資料型別可以用assign來修飾呢?這就涉及到堆和棧的問題。

相對來說,堆的空間大,通常是不連續的結構,使用連結串列結構。使用堆中的空間,需要開發者自己去釋放。OC中的物件,如 UIButton 、UILabel ,[[UIButton alloc] init] 出來的,都是分配在堆空間上。

棧的空間小,約1M左右,是一段連續的結構。棧中的空間,開發者不需要管,系統會幫忙處理。iOS開發 中 int、float等變數分配記憶體時是在棧上。如果棧空間使用完,會發生棧溢位的錯誤。

由於堆、棧結構的差異,棧和堆分配空間時的定址方式也是不一樣的。因為棧是連續的控制元件,所以棧在分配空間時,會直接在未使用的空間中分配一段出來,供程式使用;如果剩下的空間不夠大,直接棧溢位;堆是不連續的,堆尋找合適空間時,是順著連結串列結點來尋找,找到第一塊足夠大的空間時,分配空間,返回。根據兩者的資料結構,可以推斷,堆空間上是存在碎片的。

回到問題,為何assign修飾基本資料型別沒有野指標的問題?因為這些基本資料型別是分配在棧上,棧上空間的分配和回收都是系統來處理的,因此開發者無需關注,也就不會產生野指標的問題。

棧是執行緒安全的嘛

擴充套件一下,棧是執行緒安全的嘛?回答問題之前,先看一下程式和執行緒的關係。

程式和執行緒的關係

執行緒是程式的一個實體,是CPU排程和分派的基本單位。一個程式可以擁有多個執行緒。執行緒本身是不配擁有系統資源的,只擁有很少的,執行中必不可少的資源(如程式計數器、暫存器、棧)。但是執行緒可以與同屬於一個程式的其他執行緒,共享程式所擁有的資源。一個程式中所有的執行緒共享該程式的地址空間,但是每個執行緒有自己獨立的棧,iOS系統中,每個執行緒棧的大小是1M。而堆則不同。堆是程式所獨有的,通常一個程式有一個堆,這個堆為本程式中的所有執行緒所共享。

棧的執行緒安全

其實通過上面的介紹,該問題答案已經很明顯了:棧是執行緒安全的。

堆是多個執行緒所共有的空間,作業系統在對程式進行初始化的時候,會對堆進行分配; 棧是每個執行緒所獨有的,儲存執行緒的執行狀態和區域性變數。棧線上程開始的時化,每個執行緒的棧是互相獨立的,因此棧是執行緒安全的。

copy、strong、mutableCopy

屬性修飾符中,還有一個經常被問到的面試題是copy和strong。什麼時候用copy,為什麼?什麼時候用strong,為什麼?以及mutableCopy又是什麼?這一節介紹一下這些內容。

copy和strong

首先看一下copy和strong,copy和strong的區別也是面試中出現頻率最高的。之前舉得例子中其實已經出現了copy和strong:

@property (nonatomic, copy) NSString *sex;

@property (nonatomic, strong) NSMutableArray *books;
複製程式碼

通常情況下,不可變物件屬性修飾符使用copy,可變物件屬性修飾符使用strong

可變物件和不可變物件

Objective-C中存在可變物件和不可變物件的概念。像NSArray、NSDictionary、NSString這些都是不可變物件,像NSMutableArray、NSMutableDictionary、NSMutableString這些是可變物件。可變物件和不可變物件的區別是,不可變物件的值一旦確定就不能再修改。下面看個例子來說明。

- (void)testNotChange
{
    NSString *str = @"123";
    NSLog(@"str = %p",str);
    str = @"234";
    NSLog(@"after str = %p",str);
}
複製程式碼

NSString是不可變物件。雖然在程式中修改了str的值,但是此處的修改實際上是系統重新分配了空間,定義了字串,然後str重新指向了一個新的地址。這也是為何修改之後地址不一致的原因:

2018-12-06 22:02:41.350812+0800 TestClock[884:17969] str = 0x106ec1290
2018-12-06 22:02:41.350919+0800 TestClock[884:17969] after str = 0x106ec12d0
複製程式碼

再來看可變物件的例子:

- (void)testChangeAble
{
    NSMutableString *mutStr = [NSMutableString stringWithString:@"abc"];
    NSLog(@"mutStr = %p",mutStr);
    [mutStr appendString:@"def"];
    NSLog(@"after mutStr = %p",mutStr);
}
複製程式碼

NSMutableString是可變物件。程式中改變了mutStr的值,且修改前後mutStr的地址一致:

2018-12-06 22:10:08.457179+0800 TestClock[1000:21900] mutStr = 0x600002100540
2018-12-06 22:10:08.457261+0800 TestClock[1000:21900] after mutStr = 0x600002100540
複製程式碼
不可變物件用strong

上面說了,可變物件使用strong,不可變物件使用copy。那麼,如果不可變物件使用strong來修飾,會有什麼問題呢?寫程式碼測試一下:

@property (nonatomic, strong) NSString *strongStr;
複製程式碼

首先明確一點,既然型別是NSString,那麼則代表我們不希望testStr被改變,否則直接使用可變物件NSMutableString就可以了。另外需要提醒的一點是,NSMutableString是NSString的子類,對繼承瞭解的應該都知道,子類是可以用來初始化父類的。

介紹完之後,來看一段程式碼。

- (void)testStrongStr
{
    NSString *tempStr = @"123";
    NSMutableString *mutString = [NSMutableString stringWithString:tempStr];
    self.strongStr = mutString;  // 子類初始化父類
    NSLog(@"self str = %p  mutStr = %p",self.strongStr,mutString);   // 兩者指向的地址是一樣的
    [mutString insertString:@"456" atIndex:0];
    NSLog(@"self str = %@  mutStr = %@",self.strongStr,mutString);  // 兩者的值都會改變,不可變物件的值被改變
}
複製程式碼

注意:**我們定義的不可變物件strongStr,在開發者無感知的情況下被篡改了。**所謂無感知,是因為開發者沒有顯示的修改strongStr的值,而是再修改其他變數的值時,strongStr被意外的改變。這顯然不是我們想得到的,而且也是危險的。專案中出現類似的bug時,通常都很難定位。這就是不可變物件使用strong修飾所帶來的風險。

可變物件用copy

上面說了不可變物件使用strong的問題,那麼可變物件使用copy有什麼問題呢?還是寫程式碼來驗證一下:

@property (nonatomic, copy) NSMutableString *mutString;
複製程式碼

這裡還是強調一下,既然屬性型別是可變型別,說明我們期望再程式中能夠改變mutString的值,否則直接使用NSString了。

看一下測試程式碼:

- (void)testStrCopy
{
    NSString *str = @"123";
    self.mutString = [NSMutableString stringWithString:str];
    NSLog(@"str = %p self.mutString = %p",str,self.mutString); // 兩者的地址不一樣
    [self.mutString appendString:@"456"]; // 會崩潰,因為此時self.mutArray是NSString型別,是不可變物件
}
複製程式碼

執行程式後,會崩潰,崩潰原因是:

[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xed877425eeef9883
複製程式碼

即 self.mutString沒有appendString方法。self.mutString是NSMutableString型別,為何沒有appendString方法呢?這就是使用copy造成的。看一下

self.mutString = [NSMutableString stringWithString:str];
複製程式碼

這行程式碼到底發生了什麼。這行程式碼實際上完成了兩件事:

// 首先宣告一個臨時變數
NSMutableString *tempString = [NSMutableString stringWithString:str];
// 將該臨時變數copy,賦值給self.mutString
self.mutString = [tempString copy];
複製程式碼

注意,通過[tempString copy]得到的self.mutString是一個不可變物件,不可變物件自然沒有appendString方法,這也是為何會崩潰的原因。

copy和mutableCopy

另外常用來做對比的是copy和mutableCopy。copy和mutableCopy之間的差異主要和深拷貝和淺拷貝有關,先看一下深拷貝、淺拷貝的概念。

深拷貝、淺拷貝

所謂淺拷貝,在Objective-C中可以理解為引用計數加1,並沒有申請新的記憶體區域,只是另外一個指標指向了該區域。深拷貝正好相反,深拷貝會申請新的記憶體區域,原記憶體區域的引用計數不變。看圖來說明深拷貝和淺拷貝的區別。

image

首先A指向一塊記憶體區域,現在設定B = A

image

現在B和A指向了同一塊記憶體區域,即為淺拷貝。

再來看深考貝

image

首先A指向一塊記憶體區域,現在設定B = A

image

A和B指向的不是同一塊記憶體區域,只是這兩塊記憶體區域中的內容是一樣的,即為深拷貝。

可變物件的copy、mutableCopy

可變物件的copy和mutableCopy都是深拷貝。以可變物件NSMutableString和NSMutableArray為例,測試程式碼:

- (void)testMutableCopy
{
    NSMutableString *str1 = [NSMutableString stringWithString:@"abc"];
    NSString *str2 = [str1 copy];
    NSMutableString *str3 = [str1 mutableCopy];
    NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
    
    NSMutableArray *array1 = [NSMutableArray arrayWithObjects:@"a",@"b", nil];
    NSArray *array2 = [array1 copy];
    NSMutableArray *array3 = [array1 mutableCopy];
    NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
}
複製程式碼

輸出結果:

2018-12-07 13:01:27.525064+0800 TestClock[9357:143436] str1 = 0x60000086d8f0 str2 = 0xc8c1a5736a50d5fe str3 = 0x60000086d9b0
2018-12-07 13:01:27.525198+0800 TestClock[9357:143436] array1 = 0x600000868000 array2 = 0x60000067e5a0 array3 = 0x600000868030
複製程式碼

可以看到,只要是可變物件,無論是集合物件,還是非集合物件,copy和mutableCopy都是深拷貝。

不可變物件的copy、mutableCopy

不可變物件的copy是淺拷貝,mutableCopy是深拷貝。以NSString和NSArray為例,測試程式碼如下:

- (void)testCopy
{
    NSString *str1 = @"123";
    NSString *str2 = [str1 copy];
    NSMutableString *str3 = [str1 mutableCopy];
    NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
    
    NSArray *array1 = @[@"1",@"2"];
    NSArray *array2 = [array1 copy];
    NSMutableArray *array3 = [array1 mutableCopy];
    NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
}
複製程式碼

輸出結果:

2018-12-07 13:06:29.439108+0800 TestClock[9442:147133] str1 = 0x1045612b0 str2 = 0x1045612b0 str3 = 0x6000017e4450
2018-12-07 13:06:29.439236+0800 TestClock[9442:147133] array1 = 0x6000019f5c80 array2 = 0x6000019f5c80 array3 = 0x6000017e1170
複製程式碼

可以看到,只要是不可變物件,無論是集合物件,還是非集合物件,copy都是淺拷貝,mutableCopy都是深拷貝。

自定義物件如何支援copy方法

專案開發中經常會有自定義物件的需求,那麼自定義物件是否可以copy呢?如何支援copy?

自定義物件可以支援copy方法,我們所需要做的是:自定義物件遵守NSCopying協議,且實現copyWithZone方法。NSCopying協議是系統提供的,直接使用即可。

遵守NSCopying協議:

@interface Student : NSObject <NSCopying>
{
    NSString *_sex;
}

@property (atomic, copy) NSString *name;

@property (nonatomic, copy) NSString *sex;

@property (nonatomic, assign) int age;

@end
複製程式碼

實現CopyWithZone方法:

- (instancetype)initWithName:(NSString *)name age:(int)age sex:(NSString *)sex
{
    if(self = [super init]){
        self.name = name;
        _sex = sex;
        self.age = age;
    }
    return self;
}

- (instancetype)copyWithZone:(NSZone *)zone
{
    // 注意,copy的是自己,因此使用自己的屬性
    Student *stu = [[Student allocWithZone:zone] initWithName:self.name age:self.age sex:_sex];
    return stu;
}
複製程式碼

測試程式碼:

- (void)testStudent
{
    Student *stu1 = [[Student alloc] initWithName:@"Wang" age:18 sex:@"male"];
    Student *stu2 = [stu1 copy];
    NSLog(@"stu1 = %p stu2 = %p",stu1,stu2);
}
複製程式碼

輸出結果:

stu1 = 0x600003a41e60 stu2 = 0x600003a41fc0
複製程式碼

這裡是一個深拷貝,根據copyWithZone方法的實現,應該很容易明白為何是深拷貝。

除了NSCopying協議和copyWithZone方法,對應的還有NSMutableCopying協議和mutableCopyWithZone方法,實現都是類似的,不做過多介紹。

相關文章