當前位置:網站首頁>【Effective Objective-C】—— 接口與API設計

【Effective Objective-C】—— 接口與API設計

2022-01-27 11:28:31 西郵郭富城

第15條:用前綴避免命名空間沖突

因為OC中沒有其他語言那種內置的命名空間機制,所以我們在對文件命名時要十分的注意,若是發生重名沖突,那麼應用程序相應的鏈接過程就會出錯,導致運行文件不知道究竟該調用那個文件,因為其中出現了重複的符號。例如:
5345345
這給錯誤就是因為something.osomething_else.o中都出現了各自實現的EOCTheClass類,導致編譯器無法識別,而出錯了。
當然,不僅僅是類文件中可能出現這種錯誤,程序庫中也可能出現這種錯誤,所以為了避免這種錯誤,我們唯一的辦法就是自己手動的實現命名空間,即為所有的名稱都加上適當的前綴,用以分明各種文件。
並且每個類中相應的方法也應該有該類相應的前綴,用以分別該類和其他類中的名稱相同的方法。這樣做還有個好處就是:若此符號出現在棧回溯信息中,那麼我們就很容易就能判斷問題出現在那個類的哪個方法了。

要點:

  • 選擇與你的公司、應用程序或者二者皆有關聯的名稱作為類名的前綴,並在所有代碼中均使用這一前綴。
  • 若是自己所開發的程序庫中用到了第三方庫,則應為其中的名稱加上前綴。

第16條:提供“全能初始化方法”

全能初始化方法”就是一個初始化方法,但是他的類創建對象的時候無論用什麼初始化方法創建,其中代碼都會調用某個初始化方法,那麼這個初始化方法就是“全能初始化方法”。例如:
645456
上述的各種初始化方法都調用了initWithTimeIntervalSinceReferenceDate:雖然我們看不到內部情况,但是這個方法就是“全能初始化方法”。並且,只有在全能初始化方法中,才會存儲內部數據,其他的方法只是在存儲內部數據後再進行了一些其他操作而已。
看到這裏,你可能就突然發現了問題,那麼到底該怎麼初始化方法呢?

1.重寫初始化方法:

經過上述的說明,應該大概了解了“全能初始化方法”,我們知道只有在全能初始化方法中,才會存儲內部數據,所以我們要重寫初始化方法就一定離不開調用之前的“全能初始化方法”。

大概就是你首先需要在你新的初始化方法中的調用這個類的“全能初始化方法”,如果他是繼承關系的話,那麼就使用super來調用父類的初始化方法,調用完了之後你就可以編輯你自己的想要實現的初始化方法了,反正總而言之,你全能初始化方法的調用鏈一定要維持的。
注意: 如果子類的全能初始化方法與超類方法的名稱不同,我們總應覆寫超類的全能初始化方法,避免子類調用父類的全能初始化方法。

但是,有時候我們可能不想覆寫超類的全能初始化方法,因為那樣做沒有道理,那麼我們就可以覆寫超類的全能初始化方法,讓在調用這個方法的時候拋出异常,這樣就可以確保用戶一定使用的是你自己定義的方法了,但是我覺得這樣的意義並沒有多大,唯一可能用到這種方法的,就是你想讓程序給初始化方法傳入另一個參數,而不傳入之前的的數據類型,這也是要到萬不得已的時候才使用的方法。

2.一個類有多個全能初始化方法要注意的問題:

通常我們遇到的類都只有一個全能初始化方法,但是偶爾也有類具有多個全能初始化方法,例如:我們熟知的UI控件,通常我們都是使用代碼來初始化的,但是我們還可以使用NIB拖動進行初始化,兩種初始化解碼的方法是不相同的。這個時候問題就來了,我們初始化方法到底該怎麼去寫?

由於這兩種初始化方法的解碼方式不同,而且我們也不能人為的改變其解碼的方式,那麼我們就只能順其自然,他有兩種我們也重寫兩種初始化方法,注意: 重寫的這兩種初始化方法一定是分別調用過之前的兩種全能初始化方法的,並且錶明這兩種新的初始化方法分別適用於那種情况。

反正,總的來說,我們就是要維持原來類的調用鏈,每個子類的全能初始化方法都應該調用其超類的對應方法,並逐層向上。因為其父類有兩個全能初始化方法,這兩種初始化方法定義出來的數據可能是不同的,若是你在子類中調用了錯誤的父類初始化方法,它就會可能因為數據類型的問題使程序發生錯誤。

3.要點:

  • 在類中提供一個全能初始化方法,並與文檔裏指明。其他初始化方法均應調用此方法。
  • 若全能初始化方法與超類不同,則需覆寫超類中的對應方法。
  • 如果超類的初始化方法不適用於子類,那麼應該覆寫這個超類方法,並在其中拋出异常。

第17條:實現description方法

1.簡單講解description方法:

我們在調試程序的的時候經常要打印並查看對象信息。此時我們就可以用到description方法,其實**description就是對這個對象本身的描述。**例如:
435345345
這裏的description描述的內容就會替代這個%@。又例如:
534534
則會輸出:
5435345
然而,如果是一個自定義的類,那麼它就會輸出該類的地址:
43534534
這是因為你並沒有覆寫其description方法,只有在自己的類裏覆寫description方法,這個對象的描述才會改變,否則打印信息時就會調用NSObject類所實現的默認方法。在這個description方法中,你可以輸出你想要該對象輸出的內容,例如:
534534534
它就會輸出:
45345345
我建議就是,實現description方法就和系統的那種描述方法差不多就行了,就跟上述說的Array差不多就行了。並且,description沒有什麼固定的套路輸出什麼,就比如,你一個字符串的description和一個數組的description肯定是不一樣的,你肯定得根據你要輸出的對象來考慮description方法的。

2.在description方法中使用字典輸出:

在自定義的description方法中,把待打印的信息放到字典裏面,然後將字典對象的description方法所輸出的內容包含在字符串裏並返回,這樣就可以實現簡易的信息輸出方式了,例如:5345345234
423423423
這樣寫的好處就是,可以令代碼更易維護,並且如果你向該類中新增屬性的話,你就在description方法中增加就行了。

3.debugDescription:

這個也是一種描述方法,和description差不多,就是描述的比特置不一樣,description是在函數調用類的時候觸發方法才輸出的,而debugDescription是在控制臺中使用命令打印該對象時才調用的。當然加斷點查看時也可以看到debugDescription的描述。

如果你在description不想將一些內容輸出的話,你就可以將那些數據寫在debugDescription中,讓程序員自己調試時可以方便的看到這些數據,而description方法就輸出你想要讓用戶看到的信息就行了。

4.要點:

  • 實現description方法返回一個有意義的字符串,用以描述該實例。
  • 若想在調試時打印出更詳盡的對象描述信息,則應實現debugDescription方法。

第18條:盡量使用不可變對象

不可變對象,我們第一時間想到的肯定是不可變數組那種不可變對象,但是這裏的不可變不是這樣的,它指的是這個類裏邊的屬性是不能直接被修改的,要實現這種功能,我們就需要用到我們的readonly(只讀)修飾符。默認情况下,屬性是readwrite(即可讀又可寫)的,這樣修飾出來的類都是“可變的”。就像這樣:435345
有時可能想修改封裝在對象內部的數據,但是卻不想令這些數據為外人所改動。這種情况下,通常的做法是在對象內部將readonly屬性重新聲明為readwrite。當然,如果該屬性是nonatomic的,那麼這樣做可能會產生“競爭條件”,即在對象內部寫入某屬性時,對象外的觀察者也許正讀取該屬性。若想避免此問題,我們可以在必要時通過“派發隊列”等手段,將(包括對象內部的)所有數據存取操作都設為同步操作。就像這樣:
234234
現在,這個屬性就只能用在實現代碼內部設置這些屬性了,但其實,在對象外部還可以通過“鍵值編碼”技術來設置這些屬性,就像“setValue:forKey:”方法。“點語法”也可以,因為點語法就是調用set方法的。這樣做雖說可以改動,但是卻違背了本心,還會導致數據不同而出現問題,所以不建議更改。

我們還可以通過readonly來設置一個特殊的屬性,就是該屬性在外部看是不可變、不可設置的,但是內部的實現其實又定義了一個可變的該數據類型的變量,當用戶訪問這個屬性的時候,我們覆蓋在.h處的該屬性的get方法,讓其返回在實現部分定義的變量的拷貝結果,這樣在外部看來這個變量就好像是不可變的似的,我們可以通過相應的方法實現對該變量的增加删除。
當然我們還可以直接設置為可變的類型,但是這樣的話外部就可以直接修改了,不建議這樣,同時,通過這個我們可以得出:不要在返回的對象上查詢類型以確定其是否可變。因為實現部分和定義部分可能返回的是不同的類型。

要點:

  • 盡量創建不可變的對象。
  • 若某屬性僅可於對象內部修改,則在“class-continuation分類”中將其由readonly屬性擴展為readwrite屬性。
  • 不要把可變的collection作為屬性公開,而應提供相關方法,以此修改對象中的可變collection。

第19條:使用清晰而協調的命名方式

就是說,命名時要簡潔明了,讓用戶直接知道該方法是怎麼使用的,這樣的好處就是,代碼讀起來像日常語言裏的句子。我們通常使用“駝峰命名法”,就是以小寫字母開頭,其後每個單詞首字母大寫,不論是類還是屬性都可以這樣命名。

1.給方法命名時的規則:

5435345

2.類與協議的命名:

應該為類與協議的名稱加上前綴,以避免命名空間沖突,而且應該像給方法起名時那樣把詞句組織好,使其從左至右讀起來較為通順。基本命名規則就是:命名方式應該一致,如果要從其他的類中繼承子類,那麼就要遵守其原本的命名慣例。 例如:UIView它的子類就應該是***View,錶明其來曆。

3.要點:

  • 起名時應遵從標准的OC命名規範,這樣創建出來的接口更容易為開發者所理解。
  • 方法名要言簡意賅,從左至右讀起來要像個日常用語中的句子才好。
  • 方法名裏不要使用縮略後的類型名稱。
  • 給方法名起名時的第一要務就是確保其風格與你自己的代碼或所要集成的框架相符。

第20條:為私有方法名加前綴

通常我們在寫方法時,並沒有對其進行私有共有分類,導致調試時可能很麻煩,現在為私有方法加上前綴,這樣便於修改方法或方法簽名。具體加什麼來代錶私有方法因人而异,自己怎麼舒服怎麼來,唯一注意的是:一定不要只使用_作為前綴,用p_都比那個好,因為蘋果公司使用的就是_作為私有方法的前綴的,你自己定義的私有方法名有可能就會和人家自帶的沖突。

要點:

  • 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區分開。
  • 不要單用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果公司用的。

第21條:理解Objective-C錯誤模型

很多編程語言都有“异常”機制,通過异常機制來處理錯誤,當然OC中也有。
首先我們要注意的是,“自動引用計數”在默認情况下不是“异常安全的”,就是說,如果拋出异常,那麼本應該在作用域末尾釋放的對象現在卻不會釋放了,這樣就會造成內存泄漏問題,如果想生成“异常安全”的代碼,可以通過設置編譯器的標志來實現,不過這將引入一些額外的代碼,在不拋出异常時,也照樣要執行這部分代碼。需要打開的編譯器標志叫做-fobjc-arc-exceptions

OC語言現在所采用的辦法是:只在極其罕見的情况下拋出异常,异常拋出之後,無須考慮恢複問題,而且應用程序此時也應該退出。這就是說,不用再編寫複雜的“异常安全”代碼了。

既然异常只用於處理嚴重錯誤,那麼對其他錯誤怎麼辦?在出現“不那麼嚴重的錯誤”時,OC語言所用的編程範式為:令方法返回nil/0,或是使用NSError,以錶明其中有錯誤發生。像我們之前都用過的網絡請求,其中就使用到了NSError來錶示錯誤。

1.NSError對象封裝的三條信息:

654455

2.NSError的常見用法:

NSError的另一種常見的用法是:經由方法的“輸出參數”返回給調用者。就是說用一個方法來判斷你傳過去的error是否真的有錯誤,返回Boolean值,之後你就可以根據error是否有內容,或者Boolean值來决定處理的代碼和不處理的代碼。就像這樣:4324234
doSomething:會處理一些事,並且還會將出錯的問題返回給error指針傳回給調用者,並且還會返回一個Boolean值給ret
234234
若你不想知道其出錯的問題,那麼就可以直接這樣寫。
實際上,在使用ARC時,編譯器會把方法簽名中的NSError**轉換成NSError*__autoreleasing*,也就是說,指針所指的對象會在方法執行完畢後自動釋放。因為doSomething:不能保證其調用者可以把此方法中創建的NSError釋放掉。

NSError對象裏的“錯誤範圍”、“錯誤碼”、“用戶信息”等部分應該按照具體的錯誤情况填入適當的內容。這樣的話,調用者就可以根據錯誤的類型分別處理各種錯誤了。錯誤範圍應該定義成NSString型的全局變量,而錯誤碼則定義成枚舉類型為佳。
5345345

3.要點:

  • 只有發生了可使整個應用程序崩潰的嚴重錯誤時,才應使用异常。
  • 在錯誤不那麼嚴重的情况下,可以指派“委托方法”來處理錯誤,也可以把錯誤信息放在NSError對象裏,經由“輸出參數”返回給調用者。

第22條:理解NSCopying協議

我們經常會使用copy函數,但是若是你自定義的類,他自己就不會實現這個函數,此時就需要你自己來實現了,要實現copy函數就的實現NSCopying協議,該協議只有一個方法:

- (id)copyWithZone:(NSZone *)zone;

1.為何會出現NSZone呢?

因為以前開發程序時,會據此把內存分成不同的“”,而對象會創建在某個區裏。現在不用了,每個程序只有一個區:“默認區”。所以說,盡管必須實現這個方法,到那時你不必擔心其中的zone參數。

copy方法由NSObject實現,該方法只是以“默認區”為參數來調用“copyWithZone:”。所以要實現copy函數,他才是關鍵。

2.重寫copy函數:

想要重寫copy函數,要聲明該類遵從NSCopying協議,並實現其中的方法即可,例如:5345345

3.copy和mutableCopy:

其實拷貝就是創建一個新的變量,並將其初始化內容為你要拷貝變量的內容,最終再返回這個新的變量。這裏初始化最好使用全能初始化方法。

並且,無論當前實例是否可變,若需獲取其可變版本的拷貝,均應調用mutableCopy方法,同理,若需要不可變的拷貝,則總應通過copy方法來獲取。

我們通常把拷貝方法稱為copy而非immutableCopy的原因在於,NSCopying不僅設計給那些具有可變版本和不可變版本的類來使用,而且還要供其他一些類來使用,而那些類沒有“可變”與“不可變”之分,所以說,把拷貝方法叫成immutableCopy不合適。

就是說若是你有一個自定義的類,其中有可變和不可變的類型,那我們肯定就是可變的用可變拷貝不可變的就用不可變的拷貝,那這就不能叫做純正的immutableCopy了。

4.深拷貝和淺拷貝:

深拷貝:在拷貝對象自身時,將其底層數據也一並複制過去。
淺拷貝:在拷貝對象自身時,只拷貝容器對象本身,而不複制其中的數據。
5435345

原對象類型 拷貝方式 目標對象類型 拷貝類型(深/淺)
mutable對象 copy 不可變 深拷貝
mutable對象 mutableCopy 可變 深拷貝
immutable對象 copy 不可變 淺拷貝
immutable對象 mutableCopy 可變 深拷貝

5.要點:

  • 若想令自己所寫的對象具有拷貝功能,則需實現NSCopying協議。
  • 如果自定義的對象分為可變版本和不可變版本,那麼就要同時實現NSCopying與NSMutableCopying協議。
  • 複制對象時需决定采用淺拷貝還是深拷貝,一般情况下應該盡量執行淺拷貝。
  • 如果你所寫的對象需要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。

版權聲明
本文為[西郵郭富城]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201271128304502.html

隨機推薦