集合深淺拷貝以及經常遇到的坑(面試常問)

来源:http://www.cnblogs.com/mddblog/archive/2017/07/25/7236138.html
-Advertisement-
Play Games

引言 根據拷貝內容的不同,分為深淺拷貝 深拷貝:指針賦值,且內容拷貝 淺拷貝:只是簡單的指針賦值 蘋果為什麼這麼設計呢?總結起來很簡單:即安全又省記憶體。但是要理解或者避免踩一些坑,還需要看下麵的介紹 記憶體 不得不先說到記憶體,又不得不說記憶體分區: "程式底層——程式如何在RAM ROM運行,記憶體分配與 ...


引言

根據拷貝內容的不同,分為深淺拷貝

  • 深拷貝:指針賦值,且內容拷貝
  • 淺拷貝:只是簡單的指針賦值

蘋果為什麼這麼設計呢?總結起來很簡單:即安全又省記憶體。但是要理解或者避免踩一些坑,還需要看下麵的介紹

記憶體

不得不先說到記憶體,又不得不說記憶體分區:程式底層——程式如何在RAM ROM運行,記憶體分配與分區

看下麵圖片:

memoryZone

obj1是定義在函數外部的全局變數,處於全局區;obj2是定義在函數內的局部變數,處於棧區。它們都指向了處於堆區的對象。

obj1與obj2是指針,它們指向的對象是內容,那麼現在再看深淺拷貝的現象,或者說執行的結果:淺拷貝只是多個指針指向同一對象內容,深拷貝就是每個指針都指向了一個對象內容,互不影響。

自定義對象需要自己實現NSCoping協議,一般情況下,自定義對象都是可變對象,本節討論的也都是針對系統對象
指針也是會存在堆區的,比如在block裡面我們知道,如果指針使用了__block修飾,那麼指針會存放在堆區。

返回值的一些基本規則

無論是集合對象還是非集合對象,在收到copy和mutableCopy消息時,都遵守以下規則:

  • 1 copy返回immutable對象;
  • 2 mutableCopy返回mutable對象;

那麼很簡單,可變與不可變對象的轉變:

  • 不可變對象→可變對象的轉換:不可變對象.mutableCopy。
  • 可變->不可變:可變對象.copy;

集合拷貝

系統提供的集合類型,比如字典、數組、NSSet等集合類型記憶體基本都是如下結構:集合記憶體結構圖

arrMemory

我們可以上面代碼(代碼處於方法內)做個分析,加深對記憶體的理解。@"123"、@"456"是const屬性,因此處於常量區,指針str1、str2、arr局部變數指針處於棧區,@[]數組內容存放位置處於堆區,數組裡面的內容存放的是指針str1與str2,當然處於堆區

其實arr = @[str1,str2]相當於[arr addObject:str1];[arr addObject:str2];,數組裡面有兩個強指針指向了對象@"123"@"456"

圖中只是字元串是常量所以在常量區,如果他們是NSDate、UIView等等則會處於堆區

下麵的分析也是基於三種程度的拷貝,記為CopyLevel,拷貝層次,簡寫CL1、CL2、CL3

  • CL1:arr數組指針,如果只發生這層拷貝,則和非集合對象一樣,是淺拷貝
  • CL2:arr數組指針指向的的內容,即存儲的對象指針。發生本層拷貝,從非集合角度來說已經發生了內容拷貝,即深拷貝。但從集合角度來說,還是淺拷貝。
  • CL3:arr數組裡面存儲的指針指向的內容,如果發生本層拷貝,可以叫做集合的單層深拷貝。

毫無疑問,CL1是肯定會進行的。重點就在於CL2於CL3.

不可變集合的copy與mutableCopy

下麵代碼,不可變集合arrM1的copy與mutableCopy。arrM2:mutableCopy,arr:copy

arrTwoCopy

  • 根據第一行列印結果:arrM2和arr都進行CL1拷貝
  • 第二行列印結果:arrM2與arrM1結果不同,說明進行了數組拷貝;arr與arrM1結果相同,說明沒有,進行數組拷貝
  • 第三行列印結果:都相同,說明指向的內容沒有發生拷貝

可變集合的copy與mutableCopy

下麵代碼,可變集合arrM1的copy與mutableCopy。arrM2:mutableCopy,arr:copy

arrMTwoCopy

  • 根據第一行列印結果:arrM2和arr都進行CL1拷貝
  • 第二行列印結果:結果均不同,說明都進行了數組拷貝;
  • 第三行列印結果:都相同,說明指向的內容沒有發生拷貝

一般結論

我們知道,對於非集合對象,有如下結論:

// 不可變,線程安全
[immutableObject copy] // 淺複製
[immutableObject mutableCopy] // 深複製,對於集合則是只拷⻉貝數組的內容,數組的內容是指針,而指針的內容不會被拷⻉

// 可變對象,線程不安全
[mutableObject copy] //深複製,對於集合則是只拷⻉貝數組的內容,數組的內容是指針,而指針的內容不會被拷⻉
[mutableObject mutableCopy] //深複製,對於集合則是只拷⻉貝數組的內容,數組的內容是指針,而指針的內容不會被拷⻉

集合的單層深拷貝,CL3層的拷貝(one-level-deep copy)

我們需要使用- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;方法,且flag為YES。

arrOneLevelCopy

可以看到,三行列印結果都不一樣,即發生了CL3層的拷貝。

此方法執行後,arrM1集合里的每個對象都會收到 copyWithZone: 消息。如果集合里的對象遵循 NSCopying 協議,那麼對象就會被深拷貝到新的集合,如果沒有遵循就直接崩潰了。

等一等,好像有另一個問題:此方法只是會給集合的每個對象發送copyWithZone:方法,那麼對於不可變對象,copyWithZone:的執行還是淺拷貝。讀者大概也註意到了,圖中示例代碼,arrM1數組存的也是可變對象dict1,所以有CL3層的拷貝。那如果arrM1存的不是可變對象呢?結果就是沒有CL3層的拷貝,大家可以用代碼測試下!

為啥叫單層深複製呢? 因為它只給arrM1數組存的對象發送了copyWithZone:方法,而沒有對dict1發送copyWithZone:方法,dict1也是集合,它裡面也存放著對象呢。。。即集合裡面存放的集合。。。好繞,哈哈

另外,除了此方法,集合的解檔歸檔,也是可以實現單層深拷貝的。

繞的東西就到這裡,下麵看些感興趣的東西:

一些坑

  • Mutable變copy的坑

有一點需要註意了:copy返回值為不可變對象,如果使用可變對象的介面就會crash。例如:

- (void)arrMCopyTest {
    NSMutableArray *arrM = [NSMutableArray arrayWithObjects:@"123",@"456", nil];
    NSMutableArray *arr = [arrM copy];
    // 下麵代碼崩潰
    [arr addObject:@"789"];
}

[arrM copy];返回的是不可變類型,即NSArray,向一個NSArray對象發送addObject消息當然方法找不到崩潰。

另一個問題,arr是NSMutableArray類型,它指向父類NSArray編譯器為什麼不報錯呢?copy返回的是id類型,編譯器不會對id(俗稱萬能指針)進行類型檢查,所以會經常看到推薦使用instancetype,而不是id

下麵的類似錯誤就很常見了:

@property (nonatomic, copy) NSMutableArray *arr;

- (void)arrMCopyTest {
    NSMutableArray *arrM = [NSMutableArray arrayWithObjects:@"123",@"456", nil];
    self.arr = arrM;
    // 下麵代碼崩潰
    [self.arr addObject:@"789"];
}

因為self.arr為copy修飾,那麼self.arr = arrM就相當於_arr = [arrM copy]

  • 屬性指定為copy,卻沒有被copy
@property (nonatomic, copy) NSString *str;
- (void)viewDidLoad {
    NSMutableString *str = [NSMutableString stringWithFormat:@"123"];
    // self.str = str;
    _str = str;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
        [str appendString:@"456"];
        NSLog(@"change");
    });
}

這裡在block裡面對str進行操作,居然沒有對它進行__block修飾!!!感興趣可以看看這篇博客:iOS中block的使用、實現底層、迴圈引用、存儲位置

列印結果:

2017-07-23 00:33:06.344 CopyTest[95611:31912803] 123
2017-07-23 00:33:07.518 CopyTest[95611:31912803] change
2017-07-23 00:33:08.636 CopyTest[95611:31912803] 123456

都是123456,self.str被意外改變了,如果將代碼_str = str;-->self.str = str;值就不會改變了。因為相當於_str = [str copy];

所以建議除了在初始化時(init方法中),蘋果推薦我們使用_下劃線的方式直接訪問變數,其它地方儘量使用self.來訪問。另外我們還經常getter或者setter方法裡面做一些自定義操作,如果_方式則這些自定義操作就不會被執行。而且在block裡面使用_方式訪問變數會更隱蔽的引起迴圈引用的問題!

  • setter方法
@property (nonatomic, copy) NSString *str;

- (void)setStr:(NSString *)str {
    // _str = str; 不要這樣寫
    _str = [str copy];
}

講了這些,大家會不會猛然想到

@property (nonatomic, weak) id delegate;
_delegate = obj;

這樣會不會造成_delegate為指向的對象引用計數為0時,系統還會不會將_delegate置為nil?答案是,您多慮了,會的。這和copy不一樣。為啥不一樣?牽涉到runtime哈希表什麼的就不在展開了。。。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [1]基本用法 [2]多行字元串 [3]變數占位符 [4]標簽模板 [5]raw() ...
  • /* 此文不斷更新中 */ 使用 .col-md-offset-* 類可以將列向右側偏移 ...
  • 倒計時主要用到的知識點:1、設置時間間隔的setInterval可以被clearInterval取消 2、毫秒轉換為時分格式 這個是效果圖 下麵是js中的函數 第二個是html資源,為了方便我css直接寫在html中了 需要代碼的小伙伴可以自行下載 鏈接:http://pan.baidu.com/s ...
  • 【來源】由於自己非電腦出身,所以對於底層的一些常識的認識不夠;近期開始自修《網易雲課堂》的大學四年電腦,碰到了一個通過三角函數計算角度的問題;為了讓自己重溫三角函數知識,引出了之後一些列的實踐和思考,而且最後我用的非三角函數知識; 【思考】對於時鐘這種插件,《慕課網》上有很多講解,也看了一些,怎 ...
  • 一說到數組排序,最直觀的想法就是用sort啊! 請問不用使用sort方法還可以使用什麼方法進行數組排序? 比如 : 快速排序法、合併排序法、冒泡排序法、選擇排序法、插入排序法、布爾排序法、交互排序、選擇排序、二分法排序..... 等等一下,在我們瞭解這些排序方法之前,為了更好的理解,先讓我們探索一下 ...
  • 互動朋友圈模型設計 根據該模型生出資料庫後的表結構如下: 如有問題,請留言! ...
  • s中判斷對象是否存在,寫法有很多種: 出自:http://www.ruanyifeng.com/blog/2011/05/how_to_judge_the_existence_of_a_global_object_in_javascript.html ...
  • 查找電腦埠的占用情況 例如查埠號5037(adb的埠)的占用情況 一、使用 netstat -ano命令 找到埠號為5037的,並且找到對應進程的PID 然後我們區windows任務管理器裡面找PID是5736的進程是什麼 找到為eclipse,說明找對了,因為eclipse會調用adb ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...