原文地址:http://chars.tech/2017/07/09/...ios
爲了可以將咱們項目中的代碼可以在後續開發者使用(重用代碼),一般使用的方法是將代碼按照功能模塊編寫成API。那麼咱們就頗有必要了解Objective-C語言中常見的編程範式(paradigm),同時還需瞭解各類可能碰到的陷阱。git
Objective-C沒有其餘語言的那種內置命名空間(namespace)機制。所以,咱們只能本身想辦法來解決命名衝突問題。最經常使用的解決方式就是,仿照其餘語言(C++)創建本身的namespace,例如,使用前綴。編程
所選前綴能夠是與公司、應用程序或兩者皆有關聯之名。例如,ZAKER User Interface可使用ZUI做爲前綴。使用Cocoa建立應用程序時必定要注意,Apple宣稱其保留使用全部「兩字母前綴」(two-letter prefix)的權利,因此開發者選用的前綴應該是三個字母的。若是開發者使用了兩個字母做前綴,那麼頗有可能開發者自定義的API和Apple的API衝突。api
不只僅是類名,應用程序中的全部名稱都應該加前綴。若是要爲既有類新增「分類」(category),那麼必定要給「分類」及「分類」中的方法加上前綴。另外,類的實現文件中所用的純C函數及全局變量也應該注意添加前綴。數組
若是使用了第三方庫編寫本身的代碼,並準備將其發佈爲程序庫供他人開發應用程序所用,則尤爲要注意重複符號問題。這種狀況下爲了不使用者使用了與你相同的第三方庫,應該爲第三方庫都加上你本身的前綴。安全
類、方法和變量的命名是Objective-C編程的重要環節。若是命名方式好,能夠提升代碼可讀性,減小沒必要要的註釋。
初學者一般會以爲Objective-C是門很繁瑣的語言,由於其語法結構使得代碼讀起來和句子同樣。命名中通常都帶有「in」、「for」、「with」等介詞,特別是在命名時還要講究英文語法。例如:數據結構
NSString *text = @"This is a good idea."; NSString *newText = [text stringByReplacingOccurrencesOfString:@"idea" withString:@"think"];
上面的代碼雖然用了比較囉嗦的方式描述一個看上去很簡單的表達式。對於執行替換的那個方法,代碼讀起來就像平常語言裏的那個句子:「Take text and give me a new string by replacing the occurrences of the string 'idea' with the string 'think'」。
這個句子準確描述了開發者想作的事。在命名不像Objective-C這般繁瑣的語言中,相似的程序可能會寫成:dom
string text = "This is a good idea."; string new Text = text.replace("idea", "think");
上面代碼這樣寫,看起來方法名簡潔不少,可是帶來的代碼不可讀性倒是很是大的。首先,咱們不知道 text.replace 方法的兩個參數到底按照什麼順序解讀(除非查看方法聲明);再者,這兩個參數誰替換誰?編程語言
另外,和大多數語言同樣,Objective-C也是採用「駝峯式大小寫命名法」(camel casing)——以小寫字母開頭,其後每一個單詞首字母大寫。ide
清晰的方法名從左至右讀起來好似一段文章。並非說非得按照那些命名規則來給方法起名,不過這樣作能夠令代碼變得更好維護,使他人更容易讀懂。
雖然相似C++或Java中那種函數命名簡單,可是,若想知道每一個參數的用途,就得查看函數原型,這會令代碼難於讀懂。
NSString這個類展現了一套良好的命名習慣。下面列舉幾個方法及命名原因:
1)+ (instancetype)string;
工廠方法(factory method),用於建立新的空字符串。方法名清晰地描述了返回值的類型。
2)+ (instancetype)stringWithString:(NSString *)string;
工廠方法,根據某字符串建立出與以內容相同的新字符串。與建立空字符串所用的那個工廠方法同樣,方法名的第一個單詞也指明瞭返回類型。
3)+ (instancetype)localizedStringWithFormat:(NSString *)format, ...;
工廠方法,根據特定格式建立出新的「本地化字符串」(localized string)。返回值類型是方法名的第二個單詞(string),由於其前面還有個修飾語(localized)用來描述其邏輯含義。此方法的返回值依然是「字符串」(string),只不過是一種通過本地化處理的特殊字符串。
4)- (NSUInteger)lengthOfBytesUsingEncoding:(NSStringEncoding)enc;
若字符串是以給定的編碼格式(ASCII、UTF八、UTF16)來編碼的,則返回其字節數組長度。此方法與length類似,但該方法還需一個參數,該參數緊跟着方法名中描述其類型的那個名詞(encoding)。
所以,咱們能夠總結成幾條方法命名規則:
1)若是方法的返回值是新建立的,那麼方法名的首個詞應該是返回值的類型,除非前面還有修飾語,例如localizedString。屬性的存取方法不遵循這種命名方式,由於通常認爲這些方法不會建立新對象。即使有時返回內部對象的一份拷貝,咱們也認爲那至關於原有對象。這些存取方法應該按照其所對應的屬性來命名。
2)應該把表示參數類型的名詞放在參數前面。
3)若是方法要在當前對象上執行操做,那麼就應該包含動詞;若執行操做時還須要參數,則應該在動詞後面加上一個或多個名詞。
4)不要使用str這種簡稱,應該使用string這樣的全稱。
5)boolean屬性應加is前綴。若是某方法返回非屬性的boolean值,那麼應該根據其功能,選用has或is當前綴。
6)將get這個前綴留給那些藉由「輸出參數」來保存返回值的方法,好比說,把返回值填充到「C語言式數組」(C-style array)裏的那種方法就可使用這個詞作前綴。
不只僅是方法,類和協議也應該加上前綴,避免命名空間衝突。例如:
目前有不少編程語言都有「異常」(exception)機制,Objective-C也不例外。
「自動引用計數」(ARC, Automatic Reference Counting)在默認狀況下不是「異常安全的」。這意味着:若是拋出異常,那麼本應該在做用域末尾釋放的對象如今卻不會自動釋放了。若是想生成「異常安全」的代碼,能夠經過設置編譯器的標誌來實現,不過這將引入額外代碼,在不拋出異常時,也照樣要執行這部分代碼。須要打開的編譯器標誌叫作-fobjc-arc-exception
。
Objective-C如今所採用的辦法是:只在極其罕見的狀況下拋出異常,異常拋出以後,無須考慮恢復問題,並且應用程序此時也應該退出。這就是說,不用再編寫複雜的「異常安全」代碼了。
異常只應該用於極其嚴重的錯誤,好比,你編寫了某個抽象基類,它的正確用法是先從中繼承一個子類,而後使用這個子類。在這種狀況下,若是有人直接使用了這個抽象基類,那麼能夠考慮拋出異常。與其餘語言不一樣,Objective-C中沒辦法將某個類標識爲「抽象類」。要想達成相似效果,最好的辦法是在那些子類必須覆寫的超類方法裏拋出異常。
異常只用於處理嚴重錯誤(fatal error),對於其餘錯誤,Objective-C語言所用的編程範式爲:令方法返回nil/0,或使用NSError,以代表有錯誤發生。
NSError對象裏封裝了三條信息:
錯誤發生的範圍,也就是產生錯誤的根源,一般用一個特有的全局變量來定義。例如,URL-handling-subsystem,在從URL中解析或獲取數據時若是出錯了,那麼就使用NSURLErrorDomain來表示錯誤範圍。
獨有的錯誤碼,用以指明在某個範圍內具體發生了何種錯誤。某個特定範圍內可能會發生一系列相關錯誤,這些錯誤狀況一般採用enum來定義。
有關此錯誤的額外信息,其中或許包含一段「本地化描述」,或許還包含有致使該錯誤發生的另一個錯誤,經由此種信息,可將相關錯誤串成一條「錯誤鏈」。
設計類的時候,應充分使用屬性來封裝數據。而在使用屬性時,則可將其聲明爲readonly
。默認狀況下,屬性是readwrite
。
由於若是把可變對象(mutable object)放入collection以後又修改其內容,那麼很容易就會破壞set的內部數據結構,使其失去固有的語義。故此,咱們應該儘可能減小對象中的可變內容。具體到編程實踐中,則應該儘可能把對外公佈出來的屬性設爲readonly
,並且只在有必要時纔將屬性對外公佈。
定義類的公共API時,須要注意,對象裏表示各類collection的那些屬性究竟應該設成可變的,仍是不可變的。若是某個屬性能夠爲外界所增刪,那麼這個屬性就須要用可變的set來實現。在這種狀況下,一般應該提供一個readonly屬性供外界使用,該屬性將返回不可變的set,而此set則是內部那個可變set的一份拷貝。
// ZKRPointOfInterest.h #import <UIKit/UIKit.h> @interface ZKRPointOfInterest : NSObject @property (nonatomic, copy, readonly) NSString *identifier; @property (nonatomic, copy, readonly) NSString *title; @property (nonatomic, assign, readonly) CGFloat latitude; @property (nonatomic, assign, readonly) CGFloat longitude; @property (nonatomic, strong, readonly) NSSet *locations; - (instancetype)initWithIdentifier:(NSString *)identifier title:(NSString *)title latitude:(CGFloat)latitude longitude:(CGFloat)longitude; - (void)addLocation:(ZKRPointOfInterest *)location; - (void)removeLocation:(ZKRPointOfInterest *)location; @end // ZKRPointOfInterest.m #import "ZKRPointOfInterest.h" @implementation ZKRPointOfInterest { NSMutableSet *_internalLocations; } - (instancetype)initWithIdentifier:(NSString *)identifier title:(NSString *)title latitude:(CGFloat)latitude longitude:(CGFloat)longitude { self = [super init]; if (self) { } return self; } - (NSSet *)locations { return [_internalLocations copy]; } - (void)addLocation:(ZKRPointOfInterest *)location { if (location) { [_internalLocations addObject:location]; } } - (void)removeLocation:(ZKRPointOfInterest *)location { [_internalLocations removeObject:location]; } @end
注意:不要在返回的對象上查詢類型以肯定其是否可變。(即便不用isKindOfClass:
方法來判斷返回值類型是否可變)
在調試程序時,常常須要打印並查看對象信息。一種辦法是編寫代碼把對象的所有屬性都log到日誌中。NSLog(@"object=%@", object);
在構建須要打印到日誌的字符串時,object對象會收到description消息,該方法所返回的描述信息將取代「格式字符串」(format string)裏的「%@」。
NSArray *obj = @[@"A string", @(123)]; NSLog(@"object=%@", obj);
輸出:
object=( "A string", 123 )
若是在自定義類上這麼作,那麼則輸出的信息倒是以下:
object=<ZKRSqure: 0x7656d8a90060>
若是想要像上面NSArray那樣打印出有用的信息,那麼咱們就應該在本身的類中覆寫description方法,不然打印信息時就會調用NSObject類所實現的默認方法。此方法定義在NSObject協議裏,不過NSObject類也實現了它。
- (NSString *)description { return [NSString stringWithFormat:@"<%@: %p, \"%f %f\">", [self class], self, _width, _height]; }
使用結果:
ZKRRectangle *rectangle = [[ZKRRectangle alloc] initWithWidth:5.0 height:7.0]; NSLog(@"%@", rectangle); //Output <ZKRRectangle: 0x60000002fc20, "5.000000 7.000000">
NSObject協議中還有個須要注意的方法,就是debugDescription
,此方法用意與description
類似。兩者區別在於,debugDescription
方法是開發者在調試器(debugger)中以控制檯命令打印對象時才調用的。在NSObject類的默認實現中,它只是直接調用description
。
全部對象均要初始化,在初始化時,有些對象可能無須開發者向其提供額外信息,不過通常來講仍是須要提供的。一般狀況下,對象若不知道必要的信息,則沒法完成其工做。例如,UITAbleViewCell類初始化該類對象時,須要指明其樣式及標識符,標識符可以區分不一樣類型的單元格。因爲這種對象的建立成本較高,因此繪製表格時可依照標識符來複用,以提高程序效率。這種可爲對象提供必要信息以便其能完成工做的初始化方法叫作「全能初始化方法」(designated initializer)。
若是建立類實例的方式不止一種,那麼這個類就會有多個初始化方法。可是,咱們仍然須要選定一個做爲全能初始化方法,令其餘初始化方法都來調用它。例如,NSDate類
- (instancetype)init NS_DESIGNATED_INITIALIZER; - (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; - (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs; - (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs; - (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
在上面幾個初始化方法中,initWithTimeIntervalSinceReferenceDate:
是全能初始化方法。只有在全能初始化方法中,纔會存儲內部數據。這樣的話,當底層數據存儲機制改變時,只需修改此方法的代碼就好,無須改動其餘初始化方法。
示例代碼:
// ZKRRectangle.h #import <UIKit/UIKit.h> @interface ZKRRectangle : NSObject<NSCopying> @property (nonatomic, assign, readonly) CGFloat width; @property (nonatomic, assign, readonly) CGFloat height; - (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height; @end // ZKRRectangle.m #import "ZKRRectangle.h" @implementation ZKRRectangle - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { _width = [[aDecoder decodeObjectForKey:@"width"] floatValue]; _height = [[aDecoder decodeObjectForKey:@"height"] floatValue]; } return self; } - (instancetype)init { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:height: instad." userInfo:nil]; return [self initWithWidth:0 height:0]; } - (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height { self = [super init]; if (self) { _width = width; _height = height; } return self; } @end // ZKRSquare.h #import "ZKRRectangle.h" @interface ZKRSquare : ZKRRectangle - (instancetype)initWithDimension:(CGFloat)dimension; @end // ZKRSquare.m #import "ZKRSquare.h" @implementation ZKRSquare - (instancetype)init { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instad." userInfo:nil]; return [self initWithDimension:0]; } - (instancetype)initWithDimension:(CGFloat)dimension { return [super initWithWidth:dimension height:dimension]; } - (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instad." userInfo:nil]; CGFloat dimension = MIN(width, height); return [self initWithDimension:dimension]; } @end
使用對象時常常須要拷貝它。在Objective-C中,此操做經過copy方法完成。若是想令本身的類支持拷貝操做,那就要實現NSCopying協議,該協議只有一個方法:
- (id)copyWithZone:(nullable NSZone *)zone;
爲何會出現NSZone呢?由於之前開發程序時,會據此把內容分紅不一樣的「區」(zone),而對象會建立在某個區裏面。如今不用了,每一個程序只有一個區:「默認區」(default zone)。因此說,儘管必須實現這個方法,可是你沒必要擔憂其中的zone參數。
copy方法由NSObject實現,該方法只是以「默認區」爲參數來調用copyWithZone:
。咱們老是想覆寫copy方法,其實真正須要實現的是copyWithZone:
方法。若想使某個類支持拷貝功能,只需聲明該類聽從NSCopying協議,並實現其中的那個方法便可。
- (id)copyWithZone:(NSZone *)zone { ZKRRectangle *copy = [[[self class] allocWithZone:zone] initWithWidth:_width height:_height]; return copy; }
說到copy方法,除了NSString這樣的不可變類型的copy,與之相似的還有NSMutableString類的mutableCopy
方法。與copyWithZone:
方法相對應的可變內容的copy方法mutableCopyWithZone:
方法來自於NSMutableCopying
協議。若是你的類分爲可變版本(mutable)與不可變版本(immutable),那麼就應該實現NSMutableCopying協議。若採用此模式,則在可變類中覆寫copyWithZone:
方法時,不要返回可變的拷貝,而應該返回一份不可變的版本。不管當前實例是否可變,須要獲取其可變版本的拷貝,均應調用mutableCopy方法;獲取不可變版本的拷貝,則總應該經過copy方法。
深拷貝就是在拷貝對象自身時,將其底層數據也一併複製過去。
淺拷貝就是在拷貝對象時,只拷貝容器對象自己,而不復制其中數據。