看到Effective這個詞,大家一定會想到《Effective C++》、《Effective Java》等業界名著,那些書里匯聚了多項實用技巧,又系統而深入的講解了各種編程知識。那么,《Effective Objective-C 2.0》也是如此。
作為Mac OS X與iOS應用程序的開發語言,Objective-C作為首選。那么,它有哪些需要注意的呢?
起源
Objective-C與C++、Java一樣,是面向對象的語言,是由Smalltalk演化而來。Smalltalk是消息型語言的鼻祖。消息與函數調用之間的區別看上去就像這樣:
//Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
//Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);
關鍵區別在于:使用消息結構的語言,其運行時所應執行的代碼由運行環境來決定;而使用函數調用的語言,則由編譯器決定。
Objective-C是C的“超集”(superset),所以C語言中的所有功能在編寫Objective-C代碼時依然適用。理解C語言的內存模型(memory model),有助于理解Objective-C的內存模型及其“引用計數”(reference counting)機制的工作原理。Objective-C語言中的指針是用來指示對象的。
關于使用頭文件
主要使用 import
關鍵字。然而,我們在 .h
文件中一般首選使用 @class
關鍵字,它能“向前聲明”一個類。對于不需要知道類細節的情況下我們使用它。否則不會輕易使用 import
來引入整個頭文件。
過多的引入頭文件,會增加編譯時間。這就是我們多使用 @class
關鍵字的直接原因。
除非確有必要,否則不要引入頭文件。一般來說,應在某個類的頭文件中使用“向前聲明”來提及別的類,并在實現文件中引入那些類的頭文件。這樣做可以盡量降低類之間的耦合(coupling)。
有時無法使用“向前聲明”,比如要聲明某個類遵循一項協議。這種情況下,盡量把“該類遵循某協議”的這條聲明移至“class-continuation分類”中。如果不行的話,就把協議單獨放在一個頭文件中,然后將其引入。
字面量語法
在編寫Objective-C程序時,總會用到某幾個類,它們屬于Foundation框架。雖然從技術上來說,不用Foundation框架也能寫出Objective-C代碼,但是實際上卻經常要用到此框架。這幾個類是NSString、NUNumber、NSArray、NSDictionary。從類名上即可看出各自所表示的數據結構。
Objective-C以語法繁雜而著稱。不過從Objective-C 1.0起,有一種簡單的方式能創建NSString 對象。這就是“字符串字面量”(string literal),其語法如下:
NSString *string = @"Effective Objective-C 2.0";
字面數值
NSNumber *number = [NSNumber numberWithInt:10];
//等價于
NSNumber *number = @10;
更多表示:
NSNumber *intNumber = @11;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.1415926;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'ABC';
字面量語法也適用于下述表達式
int x =5;
float y = 6.5f
NSNumber *expressionNumber = @(x * y);
字面量數組
NSarray *animals = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", @"badger", nil];
// 等價于
NSarray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
使用數組
NSString *dog = [animals objectAtIndex:1];
// 等價于
NSString *dog = animals[1];
字面量字典
NSDictionary *personData = [NSDictionary dictionaryWithObjectsAnsKeys:@"Matt", @"firstName", @"Galloway", @"lastName", [NSNumber numberWithInt:28], @"age", nil];
// 等價于
NSDictionary *personData = @{@"firstName":@"Matt", @"lastName":@"Galloway", @"age":[NSNumber numberWithInt:28]};
使用字典
NSString *lastName = [personData objectForKey:@"lastName"];
// 等價于
NSString *lastName = personData[@"lastName"];
可變數組和字典
[mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
[mutableDictionary setObject:@"Galloway" forKey:@"lastName"];
// 等價于
mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";
局限性
字面量語法有個小小的限制,就是除了字符串以外,所創建出來的對象必須屬于Foundation框架才行。如果自定義了這些類的子類,則無法用字面量語法創建其對象。要想創建自定義子類的實例,必須采用“非字面量語法”(nonliteral syntax)。
使用字面量語法創建出來的字符串、數組、字典對象都是不可變的(immutable)。若想要可變版本的對象,則需要復制一份:
NSMutableArray *mutable = [@[@1, @2, @3, @4] mutableCopy];
這么做會多調用一個方法,而且還要再創建一個對象,不過使用字面量語法所帶來的好處還是多于上述缺點的。
用字面量語法創建數組或字典時,若值中有nil,則會拋出異常。因此,務必確保值里不含nil。
多用類型常量 少用#define預處理指令
編寫代碼時經常要定義常量。掌握了Objective-C與其C語言的基礎的人,也許會用這種方法來做:
#define ANIMATION_DURATION 0.3
上述預處理指令會把源代碼中的ANIMATION_DURATION字符串替換為0.3.預處理過程會把碰到的所有ANIMATION_DURATION一律替換成0.3,這樣的話,假設此指令聲明在某個頭文件中,那么所有引入了這個頭文件的代碼,其ANIMATION_DURATION都會被替換。
要解決此問題,應該設法利用編譯器的某些特性才對。
static const NSTimeInterval kAnimationDuration = 0.3;
用此方式定義的常量包含類型信息,其好處的清楚地描述了常量的含義。
常用的命名法是:
- 若常量局限于某”編譯單元”(translation unit,也就是“實現文件”,implementation file)之內,則在前面加字母k;
- 若常量在類之外可見,則通常以類名為前綴。
定義常量的位置很重要。在頭文件里聲明預處理指令,這樣會增加常量名稱互相沖突的可能性。
在頭文件中使用extern來聲明全局常量,并在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類名做前綴。
枚舉使用
枚舉只是一種常量命名方式。某個對象所經歷的各種狀態就可以定義為一個簡單的枚舉集(enumeration set)。
enum IHConnectionState {
IHConnectionStateDisconnected,
IHConnectionStateConnecting,
IHConnectionStateConnected
};
默認情況下,枚舉起始值為0,以后依次遞增,1,2,3...
其實還可以我們自己指定枚舉值:
enum IHConnectionState {
IHConnectionStateDisconnected = 1,
IHConnectionStateConnecting,
IHConnectionStateConnected
};
也可以定義為位移值:
enum UIViewAutoresizing {
UIViewAutoresizing = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
關于枚舉,Foundation框架中定義了一些輔助的宏,用這些來定義枚舉類型時,也可以指定用于保存枚舉值的底層數據類型。
typedef NS_ENUM(NSUInteger, IHConnectionState) {
IHConnectionStateDisconnected = 1,
IHConnectionStateConnecting,
IHConnectionStateConnected
};
typedef NS_OPTIONS(NSUInteger, IHPermittedDirection) {
IHPermittedDirectionUp = 1 << 0,
IHPermittedDirectionDown = 1 << 1,
IHPermittedDirectionLeft = 1 << 2,
IHPermittedDirectionRight = 1 << 3
};
這些宏的定義如下:
#if(__cplusplus && __cplusplus >= 201103L && (__has_extension(cxx_strong_enums) || __has_feature(objc_fixed_enum))) || (!__cplusplus && __has_feature(objc_fixed_enum))
#define NS_ENUM(_type, _name)
enum _name:_type _name; enum _name:_type
#if (__cplusplus)
#define NS_OPTIONS(_type, _name)
type _name; enum:_type
#else
#define NS_OPTIONS(_type, _name)
enum _name:_type _name; enum _name:_type
#endif
#else
#define NS_ENUM(_type, _name) _type _name; enum
#define NS_OPTIONS(_type, _name) _type _name; enum
#endif
第一個#if
用于判斷編譯器是否支持新式枚舉。如果不支持,那么就用老式語法來定義枚舉。
在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch語句并未處理所有枚舉。