為了能夠將我們項目中的代碼能夠在后續開發者使用(重用代碼),通常使用的方法是將代碼按照功能模塊編寫成API。那么我們就很有必要了解Objective-C語言中常見的編程范式(paradigm),同時還需了解各種可能碰到的陷阱。
命名
命名沖突的問題
Objective-C沒有其他語言的那種內置命名空間(namespace)機制。因此,我們只能自己想辦法來解決命名沖突問題。最常用的解決方式就是,仿照其他語言(C++)建立自己的namespace,例如,使用前綴。
所選前綴可以是與公司、應用程序或二者皆有關聯之名。例如,ZAKER User Interface可以使用ZUI作為前綴。使用Cocoa創建應用程序時一定要注意,Apple宣稱其保留使用所有“兩字母前綴”(two-letter prefix)的權利,所以開發者選用的前綴應該是三個字母的。如果開發者使用了兩個字母作前綴,那么很有可能開發者自定義的API和Apple的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這般繁瑣的語言中,類似的程序可能會寫成:
string text = "This is a good idea.";
string new Text = text.replace("idea", "think");
上面代碼這樣寫,看起來方法名簡潔很多,但是帶來的代碼不可讀性卻是非常大的。首先,我們不知道 text.replace 方法的兩個參數到底按照什么順序解讀(除非查看方法聲明);再者,這兩個參數誰替換誰?
另外,和大多數語言一樣,Objective-C也是采用“駝峰式大小寫命名法”(camel casing)——以小寫字母開頭,其后每個單詞首字母大寫。
方法命名
清晰的方法名從左至右讀起來好似一段文章。并不是說非得按照那些命名規則來給方法起名,不過這樣做可以令代碼變得更好維護,使他人更容易讀懂。
雖然類似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、UTF8、UTF16)來編碼的,則返回其字節數組長度。此方法與length相似,但該方法還需一個參數,該參數緊跟著方法名中描述其類型的那個名詞(encoding)。
因此,我們可以總結成幾條方法命名規則:
1)如果方法的返回值是新創建的,那么方法名的首個詞應該是返回值的類型,除非前面還有修飾語,例如localizedString。屬性的存取方法不遵循這種命名方式,因為一般認為這些方法不會創建新對象。即便有時返回內部對象的一份拷貝,我們也認為那相當于原有對象。這些存取方法應該按照其所對應的屬性來命名。
2)應該把表示參數類型的名詞放在參數前面。
3)如果方法要在當前對象上執行操作,那么就應該包含動詞;若執行操作時還需要參數,則應該在動詞后面加上一個或多個名詞。
4)不要使用str這種簡稱,應該使用string這樣的全稱。
5)boolean屬性應加is前綴。如果某方法返回非屬性的boolean值,那么應該根據其功能,選用has或is當前綴。
6)將get這個前綴留給那些借由“輸出參數”來保存返回值的方法,比如說,把返回值填充到“C語言式數組”(C-style array)里的那種方法就可以使用這個詞做前綴。
類與協議命名
不僅僅是方法,類和協議也應該加上前綴,避免命名空間沖突。例如:
- UIView
- UIViewController
- UITableViewDelegate
錯誤模型
目前有很多編程語言都有“異常”(exception)機制,Objective-C也不例外。
“自動引用計數”(ARC, Automatic Reference Counting)在默認情況下不是“異常安全的”。這意味著:如果拋出異常,那么本應該在作用域末尾釋放的對象現在卻不會自動釋放了。如果想生成“異常安全”的代碼,可以通過設置編譯器的標志來實現,不過這將引入額外代碼,在不拋出異常時,也照樣要執行這部分代碼。需要打開的編譯器標志叫做 -fobjc-arc-exception 。
Objective-C現在所采用的辦法是:只在極其罕見的情況下拋出異常,異常拋出之后,無須考慮恢復問題,而且應用程序此時也應該退出。這就是說,不用再編寫復雜的“異常安全”代碼了。
異常只應該用于極其嚴重的錯誤,比如,你編寫了某個抽象基類,它的正確用法是先從中繼承一個子類,然后使用這個子類。在這種情況下,如果有人直接使用了這個抽象基類,那么可以考慮拋出異常。與其他語言不同,Objective-C中沒辦法將某個類標識為“抽象類”。要想達成類似效果,最好的辦法是在那些子類必須覆寫的超類方法里拋出異常。
異常只用于處理嚴重錯誤(fatal error),對于其他錯誤,Objective-C語言所用的編程范式為:令方法返回nil/0,或使用NSError,以表明有錯誤發生。
NSError對象里封裝了三條信息:
- Error domain (錯誤范圍,其類型為字符串)
錯誤發生的范圍,也就是產生錯誤的根源,通常用一個特有的全局變量來定義。例如,URL-handling-subsystem,在從URL中解析或獲取數據時如果出錯了,那么就使用NSURLErrorDomain來表示錯誤范圍。
- Error code (錯誤碼,其類型為整數)
獨有的錯誤碼,用以指明在某個范圍內具體發生了何種錯誤。某個特定范圍內可能會發生一系列相關錯誤,這些錯誤情況通常采用enum來定義。
- User info (用戶信息,其類型為字典)
有關此錯誤的額外信息,其中或許包含一段“本地化描述”,或許還包含有導致該錯誤發生的另外一個錯誤,經由此種信息,可將相關錯誤串成一條“錯誤鏈”。
使用不可變對象
設計類的時候,應充分使用屬性來封裝數據。而在使用屬性時,則可將其聲明為 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: 方法來判斷返回值類型是否可變)
description方法
在調試程序時,經常需要打印并查看對象信息。一種辦法是編寫代碼把對象的全部屬性都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
小結
- 在類中提供一個全能初始化方法,并于文檔里指明。其他初始化方法均調用此方法。
- 若全能方法于超類不同,則需要覆寫超類中的對應方法。
- 如果超類的初始化方法不適用于子類,那么應該覆寫這個超類方法,并在其中拋出異常。
NSCopying協議
使用對象時經常需要拷貝它。在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方法。
深拷貝就是在拷貝對象自身時,將其底層數據也一并復制過去。
淺拷貝就是在拷貝對象時,只拷貝容器對象本身,而不復制其中數據。
來自:http://chars.tech/2017/07/09/ios-design-api-guide/
掃碼二維碼 獲取免費視頻學習資料
- 本文固定鏈接: http://www.wangchenghua.com/post/5757/
- 轉載請注明:轉載必須在正文中標注并保留原文鏈接
- 掃碼: 掃上方二維碼獲取免費視頻資料