這篇規(guī)范一共分為三個部分:
- 核心原則:介紹了這篇代碼規(guī)范所遵循的核心原則。
- 通用規(guī)范:不局限于iOS的通用性的代碼規(guī)范(使用C語言和Swift語言)速址。
- iOS規(guī)范:僅適用于iOS的代碼規(guī)范(使用Objective-C語言)舅世。
一. 核心原則
原則一:代碼應(yīng)該簡潔易懂舵稠,邏輯清晰
因為軟件是需要人來維護的猫妙。這個人在未來很可能不是你喧务。所以首先是為人編寫程序串绩,其次才是計算機:
- 不要過分追求技巧缺虐,降低程序的可讀性。
- 簡潔的代碼可以讓bug無處藏身礁凡。要寫出明顯沒有bug的代碼高氮,而不是沒有明顯bug的代碼。
原則二:面向變化編程顷牌,而不是面向需求編程剪芍。
需求是暫時的,只有變化才是永恒的窟蓝。 本次迭代不能僅僅為了當(dāng)前的需求罪裹,寫出擴展性強,易修改的程序才是負(fù)責(zé)任的做法运挫,對自己負(fù)責(zé)状共,對公司負(fù)責(zé)。
原則三:先保證程序的正確性谁帕,防止過度工程
過度工程(over-engineering):在正確可用的代碼寫出之前就過度地考慮擴展峡继,重用的問題,使得工程過度復(fù)雜匈挖。 引用《王垠:編程的智慧》里的話:
- 先把眼前的問題解決掉碾牌,解決好,再考慮將來的擴展問題儡循。
- 先寫出可用的代碼舶吗,反復(fù)推敲,再考慮是否需要重用的問題贮折。
- 先寫出可用裤翩,簡單,明顯沒有bug的代碼,再考慮測試的問題踊赠。
二. 通用規(guī)范
運算符
1. 運算符與變量之間的間隔
1.1 一元運算符與變量之間沒有空格:
!bValue
~iValue
++iCount
*strSource
&fSum
1.2 二元運算符與變量之間必須有空格
fWidth = 5 + 5;
fLength = fWidth * 2;
fHeight = fWidth + fLength;
for(int i = 0; i < 10; I++)
2. 多個不同的運算符同時存在時應(yīng)該使用括號來明確優(yōu)先級
在多個不同的運算符同時存在的時候應(yīng)該合理使用括號呵扛,不要盲目依賴操作符優(yōu)先級。 因為有的時候不能保證閱讀你代碼的人就一定能了解你寫的算式里面所有操作符的優(yōu)先級筐带。
來看一下這個算式:2 << 2 + 1 * 3 - 4
這里的<<
是移位操作直觀上卻很容易認(rèn)為它的優(yōu)先級很高今穿,所以就把這個算式誤認(rèn)為:(2 << 2) + 1 * 3 - 4
但事實上,它的優(yōu)先級是比加減法還要低的伦籍,所以該算式應(yīng)該等同于:2 << 2 + 1 * 3 - 4蓝晒。 所以在以后寫這種復(fù)雜一點的算式的時候,盡量多加一點括號帖鸦,避免讓其他人誤解(甚至是自己)芝薇。
變量
1. 一個變量有且只有一個功能,盡量不要把一個變量用作多種用途
2. 變量在使用前應(yīng)初始化作儿,防止未初始化的變量被引用
3. 局部變量應(yīng)該盡量接近使用它的地方
推薦這樣寫:
func someFunction() {
let index = ...;
//Do something With index
...
...
let count = ...;
//Do something With count
}
不推薦這樣寫:
func someFunction() {
let index = ...;
let count = ...;
//Do something With index
...
...
//Do something With count
}
if語句
1. 必須列出所有分支(窮舉所有的情況)洛二,而且每個分支都必須給出明確的結(jié)果。
推薦這樣寫:
var hintStr;
if (count < 3) {
hintStr = "Good";
} else {
hintStr = "";
}
不推薦這樣寫:
var hintStr;
if (count < 3) {
hintStr = "Good";
}
2. 不要使用過多的分支攻锰,要善于使用return來提前返回錯誤的情況
推薦這樣寫:
- (void)someMethod {
if (!goodCondition) {
return;
}
//Do something
}
不推薦這樣寫:
- (void)someMethod {
if (goodCondition) {
//Do something
}
}
比較典型的例子我在JSONModel里遇到過:
-(id)initWithDictionary:(NSDictionary*)dict error:(NSError)err{
//方法1\. 參數(shù)為nil
if (!dict) {
if (err) *err = [JSONModelError errorInputIsNil];
return nil;
}
//方法2\. 參數(shù)不是nil晾嘶,但也不是字典
if (![dict isKindOfClass:[NSDictionary class]]) {
if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
return nil;
}
//方法3\. 初始化
self = [self init];
if (!self) {
//初始化失敗
if (err) *err = [JSONModelError errorModelIsInvalid];
return nil;
}
//方法4\. 檢查用戶定義的模型里的屬性集合是否大于傳入的字典里的key集合(如果大于,則返回NO)
if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
return nil;
}
//方法5\. 核心方法:字典的key與模型的屬性的映射
if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
return nil;
}
//方法6\. 可以重寫[self validate:err]方法并返回NO娶吞,讓用戶自定義錯誤并阻攔model的返回
if (![self validate:err]) {
return nil;
}
//方法7\. 終于通過了垒迂!成功返回model
return self;
}
可以看到,在這里妒蛇,首先判斷出各種錯誤的情況然后提前返回机断,把最正確的情況放到最后返回。
3. 條件表達(dá)式如果很長材部,則需要將他們提取出來賦給一個BOOL值
推薦這樣寫:
let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) {
// Do something
}
不推薦這樣寫:
if ( sessionName.hasPrefix("Swift") && (sessionDateCompontents.year == 2014) ) {
// Do something
}
4. 條件語句的判斷應(yīng)該是變量在左毫缆,常量在右
推薦這樣寫:
if ( count == 6) {
}
或者
if ( object == nil) {
}
或者
if ( !object ) {
}
不推薦這樣寫:
if ( 6 == count) {
}
或者
if ( nil == object ) {
}
5. 每個分支的實現(xiàn)代碼都必須被大括號包圍
推薦這樣寫:
if (!error) {
return success;
}
不推薦這樣寫:
if (!error)
return success;
或者
if (!error) return success;
6. 條件過多,過長的時候應(yīng)該換行
推薦這樣寫:
if (condition1() &&
condition2() &&
condition3() &&
condition4()) {
// Do something
}
不推薦這樣寫:
if (condition1() && condition2() && condition3() && condition4()) {
// Do something
}
for語句
1. 不可在for循環(huán)內(nèi)修改循環(huán)變量乐导,防止for循環(huán)失去控制。
for (int index = 0; index < 10; index++){
...
logicToChange(index)
}
2. 避免使用continue和break浸颓。
continue和break所描述的是“什么時候不做什么”物臂,所以為了讀懂二者所在的代碼,我們需要在頭腦里將他們?nèi)》础?/p>
其實最好不要讓這兩個東西出現(xiàn)产上,因為我們的代碼只要體現(xiàn)出“什么時候做什么”就好了棵磷,而且通過適當(dāng)?shù)姆椒ǎ强梢詫⑦@兩個東西消滅掉的:
2.1 如果出現(xiàn)了continue晋涣,只需要把continue的條件取反即可
var filteredProducts = Array<String>()
for level in products {
if level.hasPrefix("bad") {
continue
}
filteredProducts.append(level)
}
我們可以看到仪媒,通過判斷字符串里是否含有“bad”這個prefix來過濾掉一些值。其實我們是可以通過取反谢鹊,來避免使用continue的:
for level in products {
if !level.hasPrefix("bad") {
filteredProducts.append(level)
}
}
2.2 消除while里的break:將break的條件取反算吩,并合并到主循環(huán)里
在while里的block其實就相當(dāng)于“不存在”留凭,既然是不存在的東西就完全可以在最開始的條件語句中將其排除。
while里的break:
while (condition1) {
...
if (condition2) {
break;
}
}
取反并合并到主條件:
while (condition1 && !condition2) {
...
}
2.3 在有返回值的方法里消除break:將break轉(zhuǎn)換為return立即返回
有些朋友喜歡這樣做:在有返回值的方法里break之后偎巢,再返回某個值蔼夜。其實完全可以在break的那一行直接返回。
func hasBadProductIn(products: Array<String>) -> Bool
{
var result = false
for level in products {
if level.hasPrefix("bad") {
result = true
}
}
return result
}
遇到錯誤條件直接返回:
func hasBadProductIn(products: Array<String>) -> Bool
{
for level in products {
if level.hasPrefix("bad") {
return true
}
}
return false
}
這樣寫的話不用特意聲明一個變量來特意保存需要返回的值压昼,看起來非常簡潔求冷,可讀性高。
Switch語句
1. 每個分支都必須用大括號括起來
推薦這樣寫:
switch (integer) {
case 1: {
// ...
}
break;
case 2: {
// ...
break;
}
case 3: {
// ...
break;
}
default:{
// ...
break;
}
}
2. 使用枚舉類型時窍霞,不能有default分支匠题,除了使用枚舉類型以外,都必須有default分支
RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;
switch (menuType) {
case RWTLeftMenuTopItemMain: {
// ...
break;
}
case RWTLeftMenuTopItemShows: {
// ...
break;
}
case RWTLeftMenuTopItemSchedule: {
// ...
break;
}
}
在Switch語句使用枚舉類型的時候但金,如果使用了default分支韭山,在將來就無法通過編譯器來檢查新增的枚舉類型了。
函數(shù)
1. 一個函數(shù)的長度必須限制在50行以內(nèi)
通常來說傲绣,在閱讀一個函數(shù)的時候掠哥,如果視需要跨過很長的垂直距離會非常影響代碼的閱讀體驗。如果需要來回滾動眼球或代碼才能看全一個方法秃诵,就會很影響思維的連貫性续搀,對閱讀代碼的速度造成比較大的影響。最好的情況是在不滾動眼球或代碼的情況下一眼就能將該方法的全部代碼映入眼簾菠净。
2. 一個函數(shù)只做一件事(單一原則)
每個函數(shù)的職責(zé)都應(yīng)該劃分的很明確(就像類一樣)禁舷。
推薦這樣寫:
dataConfiguration()
viewConfiguration()
不推薦這樣寫:
void dataConfiguration()
{
...
viewConfiguration()
}
3. 對于有返回值的函數(shù)(方法),每一個分支都必須有返回值
推薦這樣寫:
int function()
{
if(condition1){
return count1
}else if(condition2){
return count2
}else{
return defaultCount
}
}
不推薦這樣寫:
int function()
{
if(condition1){
return count1
}else if(condition2){
return count2
}
}
4. 對輸入?yún)?shù)的正確性和有效性進(jìn)行檢查毅往,參數(shù)錯誤立即返回
推薦這樣寫:
void function(param1,param2)
{
if(param1 is unavailable){
return;
}
if(param2 is unavailable){
return;
}
//Do some right thing
}
5. 如果在不同的函數(shù)內(nèi)部有相同的功能牵咙,應(yīng)該把相同的功能抽取出來單獨作為另一個函數(shù)
原來的調(diào)用:
void logic() {
a();
b();
if (logic1 condition) {
c();
} else {
d();
}
}
將a攀唯,b函數(shù)抽取出來作為單獨的函數(shù)
void basicConfig()
{
a();
b();
}
void logic1()
{
basicConfig();
c();
}
void logic2()
{
basicConfig();
d();
}
6. 將函數(shù)內(nèi)部比較復(fù)雜的邏輯提取出來作為單獨的函數(shù)
一個函數(shù)內(nèi)的不清晰(邏輯判斷比較多洁桌,行數(shù)較多)的那片代碼,往往可以被提取出去侯嘀,構(gòu)成一個新的函數(shù)另凌,然后在原來的地方調(diào)用它這樣你就可以使用有意義的函數(shù)名來代替注釋,增加程序的可讀性戒幔。
舉一個發(fā)送郵件的例子:
openEmailSite();
login();
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
send();
中間的部分稍微長一些吠谢,我們可以將它們提取出來:
void writeEmail(title, content,receiver,attachment)
{
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
}
然后再看一下原來的代碼:
openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();
7. 避免使用全局變量,類成員(class member)來傳遞信息诗茎,盡量使用局部變量和參數(shù)工坊。
在一個類里面,經(jīng)常會有傳遞某些變量的情況。而如果需要傳遞的變量是某個全局變量或者屬性的時候王污,有些朋友不喜歡將它們作為參數(shù)罢吃,而是在方法內(nèi)部就直接訪問了:
class A {
var x;
func updateX()
{
...
x = ...;
}
func printX()
{
updateX();
print(x);
}
}
我們可以看到,在printX方法里面玉掸,updateX和print方法之間并沒有值的傳遞刃麸,乍一看我們可能不知道x從哪里來的,導(dǎo)致程序的可讀性降低了司浪。
而如果你使用局部變量而不是類成員來傳遞信息泊业,那么這兩個函數(shù)就不需要依賴于某一個類,而且更加容易理解啊易,不易出錯:
func updateX() -> String
{
x = ...;
return x;
}
func printX()
{
String x = updateX();
print(x);
}
注釋
優(yōu)秀的代碼大部分是可以自描述的吁伺,我們完全可以用程代碼本身來表達(dá)它到底在干什么,而不需要注釋的輔助租谈。
但并不是說一定不能寫注釋篮奄,有以下三種情況比較適合寫注釋: 1. 公共接口(注釋要告訴閱讀代碼的人,當(dāng)前類能實現(xiàn)什么功能)割去。 2. 涉及到比較深層專業(yè)知識的代碼(注釋要體現(xiàn)出實現(xiàn)原理和思想)烁涌。 3. 容易產(chǎn)生歧義的代碼(但是嚴(yán)格來說莲祸,容易讓人產(chǎn)生歧義的代碼是不允許存在的)。
除了上述這三種情況,如果別人只能依靠注釋才能讀懂你的代碼的時候仗扬,就要反思代碼出現(xiàn)了什么問題润文。
最后十绑,對于注釋的內(nèi)容歪赢,相對于“做了什么”,更應(yīng)該說明“為什么這么做”宜雀。
Code Review
換行切平、注釋、方法長度辐董、代碼重復(fù)等這些是通過機器檢查出來的問題悴品,是無需通過人來做的。
而且除了審查需求的實現(xiàn)的程度简烘,bug是否無處藏身以外他匪,更應(yīng)該關(guān)注代碼的設(shè)計。比如類與類之間的耦合程度夸研,設(shè)計的可擴展性,復(fù)用性依鸥,是否可以將某些方法抽出來作為接口等等亥至。
三. iOS規(guī)范
變量
1. 變量名必須使用駝峰格式
類,協(xié)議使用大駝峰:
HomePageViewController.h
<HeaderViewDelegate>
對象等局部變量使用小駝峰:
NSString *personName = @"";
NSUInteger totalCount = 0;
2. 變量的名稱必須同時包含功能與類型
UIButton *addBtn //添加按鈕
UILabel *nameLbl //名字標(biāo)簽
NSString *addressStr//地址字符串
3. 系統(tǒng)常用類作實例變量聲明時加入后綴
類型 | 后綴 |
---|---|
UIViewController | VC |
UIView | View |
UILabel | Lbl |
UIButton | Btn |
UIImage | Img |
UIImageView | ImagView |
NSArray | Array |
NSMutableArray | Marray |
NSDictionary | Dict |
NSMutableDictionary | Mdict |
NSString | Str |
NSMutableString | Mstr |
NSSet | Set |
NSMutableSet | Mset |
常量
1. 常量以相關(guān)類名作為前綴
推薦這樣寫:
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
不推薦這樣寫:
static const NSTimeInterval fadeOutTime = 0.4;
2. 建議使用類型常量,不建議使用#define預(yù)處理命令
首先比較一下這兩種聲明常量的區(qū)別:
- 預(yù)處理命令:簡單的文本替換姐扮,不包括類型信息絮供,并且可被任意修改。
- 類型常量:包括類型信息茶敏,并且可以設(shè)置其使用范圍壤靶,而且不可被修改。
使用預(yù)處理雖然能達(dá)到替換文本的目的惊搏,但是本身還是有局限性的:
- 不具備類型信息贮乳。
- 可以被任意修改。
3. 對外公開某個常量:
如果我們需要發(fā)送通知恬惯,那么就需要在不同的地方拿到通知的“頻道”字符串(通知的名稱)向拆,那么顯然這個字符串是不能被輕易更改,而且可以在不同的地方獲取酪耳。這個時候就需要定義一個外界可見的字符串常量浓恳。
推薦這樣寫:
//頭文件
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
//實現(xiàn)文件
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
不推薦這樣寫:
#define CompanyName @"Apple Inc."
#define magicNumber 42
宏
1. 宏、常量名都要使用大寫字母碗暗,用下劃線‘_’分割單詞颈将。
#define URL_GAIN_QUOTE_LIST @"/v1/quote/list"
#define URL_UPDATE_QUOTE_LIST @"/v1/quote/update"
#define URL_LOGIN @"/v1/user/login”
2. 宏定義中如果包含表達(dá)式或變量,表達(dá)式和變量必須用小括號括起來言疗。
#define MY_MIN(A, B) ((A)>(B)?(B):(A))
CGRect函數(shù)
其實iOS內(nèi)部已經(jīng)提供了相應(yīng)的獲取CGRect各個部分的函數(shù)了晴圾,它們的可讀性比較高,而且簡短洲守,推薦使用:
推薦這樣寫:
CGRect frame = self.view.frame;
CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);
CGRect frame = CGRectMake(0.0, 0.0, width, height);
而不是
CGRect frame = self.view.frame;
CGFloat x = frame.origin.x;
CGFloat y = frame.origin.y;
CGFloat width = frame.size.width;
CGFloat height = frame.size.height;
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };
范型
建議在定義NSArray和NSDictionary時使用泛型疑务,可以保證程序的安全性:
NSArray<NSString *> *testArr = [NSArray arrayWithObjects:@"Hello", @"world", nil];
NSDictionary<NSString *, NSNumber *> *dic = @{@"key":@(1), @"age":@(10)};
Block
為常用的Block類型創(chuàng)建typedef
如果我們需要重復(fù)創(chuàng)建某種block(相同參數(shù),返回值)的變量梗醇,我們就可以通過typedef來給某一種塊定義屬于它自己的新類型
例如:
int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value)
{
// Implementation
return someInt;
}
這個Block有一個bool參數(shù)和一個int參數(shù)知允,并返回int類型。我們可以給它定義類型:
typedef int(^EOCSomeBlock)(BOOL flag, int value);
再次定義的時候叙谨,就可以通過簡單的賦值來實現(xiàn):
EOCSomeBlock block = ^(BOOL flag, int value){
// Implementation
};
定義作為參數(shù)的Block:
- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;
這里的Block有一個NSData參數(shù)温鸽,一個NSError參數(shù)并沒有返回值
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;”
通過typedef定義Block簽名的好處是:如果要某種塊增加參數(shù),那么只修改定義簽名的那行代碼即可手负。
字面量語法
盡量使用字面量值來創(chuàng)建 NSString , NSDictionary , NSArray , NSNumber 這些不可變對象:
推薦這樣寫:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;NSNumber *buildingZIPCode = @10018;
不推薦這樣寫:
NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill" ];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018];
屬性
1. 屬性的命名使用小駝峰
推薦這樣寫:
@property (nonatomic, readwrite, strong) UIButton *confirmButton;
2. 屬性的關(guān)鍵字推薦按照原子性涤垫,讀寫,內(nèi)存管理的順序排列
推薦這樣寫:
@property (nonatomic, readwrite, copy) NSString *name;
@property (nonatomic, readonly, copy) NSString *gender;
@property (nonatomic, readwrite, strong) UIView *headerView;
3. Block屬性應(yīng)該使用copy關(guān)鍵字
推薦這樣寫:
typedef void (^ErrorCodeBlock) (id errorCode,NSString *message);
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock;//將block拷貝到堆中
4. 形容詞性的BOOL屬性的getter應(yīng)該加上is前綴
推薦這樣寫:
@property (assign, getter=isEditable) BOOL editable;
5. 使用getter方法做懶加載
實例化一個對象是需要耗費資源的竟终,如果這個對象里的某個屬性的實例化要調(diào)用很多配置和計算蝠猬,就需要懶加載它,在使用它的前一刻對它進(jìn)行實例化:
- (NSDateFormatter *)dateFormatter
{
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[_dateFormatter setLocale:enUSPOSIXLocale];
[_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];
}
return _dateFormatter;
}
但是也有對這種做法的爭議:getter方法可能會產(chǎn)生某些副作用统捶,例如如果它修改了全局變量榆芦,可能會產(chǎn)生難以排查的錯誤柄粹。
6. 除了init和dealloc方法,建議都使用點語法訪問屬性
使用點語法的好處:
setter:
1. setter會遵守內(nèi)存管理語義(strong, copy, weak)匆绣。
2. 通過在內(nèi)部設(shè)置斷點驻右,有助于調(diào)試bug。
3. 可以過濾一些外部傳入的值崎淳。
4. 捕捉KVO通知堪夭。
getter:
1. 允許子類化。
2. 通過在內(nèi)部設(shè)置斷點拣凹,有助于調(diào)試bug森爽。
3. 實現(xiàn)懶加載(lazy initialization)。
注意:
- 懶加載的屬性咐鹤,必須通過點語法來讀取數(shù)據(jù)拗秘。因為懶加載是通過重寫getter方法來初始化實例變量的,如果不通過屬性來讀取該實例變量祈惶,那么這個實例變量就永遠(yuǎn)不會被初始化雕旨。
- 在init和dealloc方法里面使用點語法的后果是:因為沒有繞過setter和getter,在setter和getter里面可能會有很多其他的操作捧请。而且如果它的子類重載了它的setter和getter方法凡涩,那么就可能導(dǎo)致該子類調(diào)用其他的方法。
7. 不要濫用點語法疹蛉,要區(qū)分好方法調(diào)用和屬性訪問
推薦這樣寫:
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;
不推薦這樣寫:
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;
8. 盡量使用不可變對象
建議盡量把對外公布出來的屬性設(shè)置為只讀活箕,在實現(xiàn)文件內(nèi)部設(shè)為讀寫。具體做法是:
- 在頭文件中可款,設(shè)置對象屬性為
readonly
育韩。 - 在實現(xiàn)文件中設(shè)置為
readwrite
。
這樣一來闺鲸,在外部就只能讀取該數(shù)據(jù)筋讨,而不能修改它,使得這個類的實例所持有的數(shù)據(jù)更加安全摸恍。而且悉罕,對于集合類的對象,更應(yīng)該仔細(xì)考慮是否可以將其設(shè)為可變的立镶。
如果在公開部分只能設(shè)置其為只讀屬性壁袄,那么就在非公開部分存儲一個可變型。所以當(dāng)在外部獲取這個屬性時媚媒,獲取的只是內(nèi)部可變型的一個不可變版本,例如:
在公共API中:
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公開的不可變集合
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
@end
在這里嗜逻,我們將friends屬性設(shè)置為不可變的set。然后缭召,提供了來增加和刪除這個set里的元素的公共接口变泄。
在實現(xiàn)文件里:
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
NSMutableSet *_internalFriends; //實現(xiàn)文件里的可變集合
}
- (NSSet*)friends
{
return [_internalFriends copy]; //get方法返回的永遠(yuǎn)是可變set的不可變型
}
- (void)addFriend:(EOCPerson*)person
{
[_internalFriends addObject:person]; //在外部增加集合元素的操作
//do something when add element
}
- (void)removeFriend:(EOCPerson*)person
{
[_internalFriends removeObject:person]; //在外部移除元素的操作
//do something when remove element
}
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName
{
if ((self = [super init])) {
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
return self;
}
我們可以看到令哟,在實現(xiàn)文件里,保存一個可變set來記錄外部的增刪操作妨蛹。
這里最重要的代碼是:
- (NSSet*)friends
{
return [_internalFriends copy];
}
這個是friends屬性的獲取方法:它將當(dāng)前保存的可變set復(fù)制了一不可變的set并返回。因此晴竞,外部讀取到的set都將是不可變的版本蛙卤。
方法
1. 方法名中不應(yīng)使用and,而且簽名要與對應(yīng)的參數(shù)名保持高度一致
推薦這樣寫:
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
不推薦這樣寫:
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height;
2. 方法實現(xiàn)時噩死,如果參數(shù)過長颤难,則令每個參數(shù)占用一行,以冒號對齊已维。
- (void)doSomethingWith:(NSString *)theFoo
rect:(CGRect)theRect
interval:(CGFloat)theInterval
{
//Implementation
}
3. 私有方法應(yīng)該在實現(xiàn)文件中申明行嗤。
@interface ViewController ()
- (void)basicConfiguration;
@end
@implementation ViewController
- (void)basicConfiguration
{
//Do some basic configuration
}
@end
4. 方法名用小寫字母開頭的單詞組合而成
- (NSString *)descriptionWithLocale:(id)locale;
5. 方法名前綴
- 刷新視圖的方法名要以
refresh
為首。 - 更新數(shù)據(jù)的方法名要以
update
為首垛耳。
推薦這樣寫:
- (void)refreshHeaderViewWithCount:(NSUInteger)count;
- (void)updateDataSourceWithViewModel:(ViewModel*)viewModel;
面向協(xié)議編程
如果某些功能(方法)具備可復(fù)用性栅屏,我們就需要將它們抽取出來放入一個抽象接口文件中(在iOS中,抽象接口即協(xié)議)堂鲜,讓不同類型的對象遵循這個協(xié)議栈雳,從而擁有相同的功能。
因為協(xié)議是不依賴于某個對象的缔莲,所以通過協(xié)議哥纫,我們可以解開兩個對象之間的耦合。如何理解呢痴奏?我們來看一下下面這個例子:
現(xiàn)在有一個需求:在一個UITableViewController
里面拉取feed并展示出來蛀骇。
方案一:
定義一個拉取feed的類ZOCFeedParser
,這個類有一些代理方法實現(xiàn)feed相關(guān)功能:
@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;@end
@interface ZOCFeedParser : NSObject
@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;
- (id)initWithURL:(NSURL *)url;
- (BOOL)start;
- (void)stop;
@end
然后在ZOCTableViewController
里面?zhèn)魅?code>ZOCFeedParser读拆,并遵循其代理方法擅憔,實現(xiàn)feed的拉取功能。
@interface ZOCTableViewController : UITableViewController<ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser;
@end
具體應(yīng)用:
NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"];
ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL];
ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser];
feedParser.delegate = tableViewController;
OK建椰,現(xiàn)在我們實現(xiàn)了需求:在ZOCTableViewController
里面存放了一個ZOCFeedParser對象來處理feed的拉取功能雕欺。
但這里有一個嚴(yán)重的耦合問題:ZOCTableViewController
只能通過ZOCFeedParser
對象來處理feed的拉取功能。 于是我們重新審視一下這個需求:其實我們實際上只需要ZOCTableViewController
拉取feed就可以了棉姐,而具體是由哪個對象來拉取屠列,ZOCTableViewController
并不需要關(guān)心。
也就是說伞矩,我們需要提供給ZOCTableViewController
的是一個更范型的對象笛洛,這個對象具備了拉取feed的功能就好了,而不應(yīng)該僅僅局限于某個具體的對象(ZOCFeedParser
)乃坤。所以苛让,剛才的設(shè)計需要重新做一次修改:
方案二:
首先需要在一個接口文件ZOCFeedParserProtocol.h
里面定義抽象的沟蔑,具有拉取feed功能的協(xié)議:
@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didFailWithError:(NSError *)error;@end
@protocol ZOCFeedParserProtocol <NSObject>
@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;
- (BOOL)start;
- (void)stop;
@end
而原來的ZOCFeedParser
僅僅是需要遵循上面這個協(xié)議就具備了拉取feed的功能:
@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol>
- (id)initWithURL:(NSURL *)url;//僅僅需要通過傳入url即可,其他事情都交給ZOCFeedParserProtocol@end
而且狱杰,ZOCTableViewController
也不直接依賴于ZOCFeedParser
對象瘦材,我們只需要傳給它一個遵循<ZOCFeedParserProtocol>
的對象即可。
@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;
@end
這樣一來仿畸,ZOCTableViewController
和ZOCFeedParser
之間就沒有直接的關(guān)系了食棕。以后,如果我們想:
- 給這個feed拉取器增加新的功能:僅需要修改
ZOCFeedParserProtocol.h
文件错沽。 - 更換一個feed拉取器實例:創(chuàng)建一個新類型來遵循
ZOCFeedParserProtocol.h
即可簿晓。
iOS 中委托的設(shè)計
1. 要區(qū)分好代理和數(shù)據(jù)源的區(qū)別
在iOS開發(fā)中的委托模式包含了delegate(代理)和datasource(數(shù)據(jù)源)。雖然二者同屬于委托模式千埃,但是這兩者是有區(qū)別的憔儿。這個區(qū)別就是二者的信息流方向是不同的:
- delegate :事件發(fā)生的時候,委托者需要通知代理放可。(信息流從委托者到代理)
- datasource:委托者需要從數(shù)據(jù)源拉取數(shù)據(jù)谒臼。(信息流從數(shù)據(jù)源到委托者)
然而包括蘋果也沒有做好榜樣,將它們徹底的區(qū)分開吴侦。就拿UITableView來說屋休,在它的delegate方法中有一個方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
這個方法正確地體現(xiàn)了代理的作用:委托者(tableview)告訴代理(控制器)“我的某個cell被點擊了”。但是备韧,UITableViewDelegate的方法列表里還有這個方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
該方法的作用是 由控制器來告訴tabievlew的行高劫樟,也就是說,它的信息流是從控制器(數(shù)據(jù)源)到委托者(tableview)的织堂。準(zhǔn)確來講叠艳,它應(yīng)該是一個數(shù)據(jù)源方法,而不是代理方法易阳。
在UITableViewDataSource中附较,就有標(biāo)準(zhǔn)的數(shù)據(jù)源方法:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
這個方法的作用就是讓tableview向控制器拉取一個section數(shù)量的數(shù)據(jù)。
所以潦俺,在我們設(shè)計一個視圖控件的代理和數(shù)據(jù)源時拒课,一定要區(qū)分好二者的區(qū)別,合理地劃分哪些方法屬于代理方法事示,哪些方法屬于數(shù)據(jù)源方法早像。
2. 代理方法的第一個參數(shù)必須為委托者
代理方法必須以委托者作為第一個參數(shù)(參考UITableViewDelegate)的方法。其目的是為了區(qū)分不同委托著的實例肖爵。因為同一個控制器是可以作為多個tableview的代理的卢鹦。若要區(qū)分到底是哪個tableview的cell被點擊了,就需要在``
* (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath``方法中做個區(qū)分劝堪。
3.向代理發(fā)送消息時需要判斷其是否實現(xiàn)該方法
最后冀自,在委托著向代理發(fā)送消息的時候揉稚,需要判斷委托著是否實現(xiàn)了這個代理方法:
if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
[self.delegate signUpViewControllerDidPressSignUpButton:self];
}
4. 遵循代理過多的時候,換行對齊顯示
@interface ShopViewController () <UIGestureRecognizerDelegate,
HXSClickEventDelegate,
UITableViewDelegate,
UITableViewDataSource>
5. 代理的方法需要明確必須執(zhí)行和可不執(zhí)行
代理方法在默認(rèn)情況下都是必須執(zhí)行的熬粗,然而在設(shè)計一組代理方法的時候搀玖,有些方法可以不是必須執(zhí)行(是因為存在默認(rèn)配置),這些方法就需要使用@optional關(guān)鍵字來修飾:
@protocol ZOCServiceDelegate <NSObject>@optional- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries;
@end
類
1. 類的名稱應(yīng)該以三個大寫字母為前綴荐糜;創(chuàng)建子類的時候巷怜,應(yīng)該把代表子類特點的部分放在前綴和父類名的中間
推薦這樣寫:
//父類
ZOCSalesListViewController
//子類
ZOCDaySalesListViewController
ZOCMonthSalesListViewController
2. initializer && dealloc
推薦:
- 將 dealloc 方法放在實現(xiàn)文件的最前面
- 將init方法放在dealloc方法后面。如果有多個初始化方法暴氏,應(yīng)該將指定初始化方法放在最前面,其他初始化方法放在其后绣张。
2.1 dealloc方法里面應(yīng)該直接訪問實例變量答渔,不應(yīng)該用點語法訪問
2.2 init方法的寫法:
- init方法返回類型必須是instancetype,不能是id侥涵。
- 必須先實現(xiàn)[super init]沼撕。
- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
2.3 指定初始化方法
指定初始化方法(designated initializer)是提供所有的(最多的)參數(shù)的初始化方法,間接初始化方法(secondary initializer)有一個或部分參數(shù)的初始化方法芜飘。
注意事項1:間接初始化方法必須調(diào)用指定初始化方法务豺。
@implementation ZOCEvent
//指定初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
location:(CLLocation *)location
{
self = [super init];
if (self) {
_title = title;
_date = date;
_location = location;
}
return self;
}
//間接初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
{
return [self initWithTitle:title date:date location:nil];
}
//間接初始化方法
- (instancetype)initWithTitle:(NSString *)title
{
return [self initWithTitle:title date:[NSDate date] location:nil];
}
@end
注意事項2:如果直接父類有指定初始化方法,則必須調(diào)用其指定初始化方法
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
}
return self;
}
注意事項3:如果想在當(dāng)前類自定義一個新的全能初始化方法嗦明,則需要如下幾個步驟
- 定義新的指定初始化方法笼沥,并確保調(diào)用了直接父類的初始化方法。
- 重載直接父類的初始化方法娶牌,在內(nèi)部調(diào)用新定義的指定初始化方法奔浅。
- 為新的指定初始化方法寫文檔。
看一個標(biāo)準(zhǔn)的例子:
@implementation ZOCNewsViewController
//新的指定初始化方法
- (id)initWithNews:(ZOCNews *)news
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// 重載父類的初始化方法
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
return [self initWithNews:nil];
}
@end
在這里诗良,重載父類的初始化方法并在內(nèi)部調(diào)用新定義的指定初始化方法的原因是你不能確定調(diào)用者調(diào)用的就一定是你定義的這個新的指定初始化方法汹桦,而不是原來從父類繼承來的指定初始化方法。
假設(shè)你沒有重載父類的指定初始化方法鉴裹,而調(diào)用者卻恰恰調(diào)用了父類的初始化方法舞骆。那么調(diào)用者可能永遠(yuǎn)都調(diào)用不到你自己定義的新指定初始化方法了。
而如果你成功定義了一個新的指定初始化方法并能保證調(diào)用者一定能調(diào)用它径荔,你最好要在文檔中明確寫出哪一個才是你定義的新初始化方法督禽。或者你也可以使用編譯器指令__attribute__((objc_designated_initializer))
來標(biāo)記它猖凛。
3. 所有返回類對象和實例對象的方法都應(yīng)該使用instancetype
將instancetype關(guān)鍵字作為返回值的時候赂蠢,可以讓編譯器進(jìn)行類型檢查,同時適用于子類的檢查辨泳,這樣就保證了返回類型的正確性(一定為當(dāng)前的類對象或?qū)嵗龑ο螅?/p>
推薦這樣寫:
@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name;
@end
不推薦這樣寫:
@interface ZOCPerson
+ (id)personWithName:(NSString *)name;
@end
4. 在類的頭文件中盡量少引用其他頭文件
有時虱岂,類A需要將類B的實例變量作為它公共API的屬性玖院。這個時候,我們不應(yīng)該引入類B的頭文件第岖,而應(yīng)該使用向前聲明(forward declaring)使用class關(guān)鍵字难菌,并且在A的實現(xiàn)文件引用B的頭文件。
// EOCPerson.h
#import <Foundation/Foundation.h>
@class EOCEmployer;
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;//將EOCEmployer作為屬性
@end
// EOCPerson.m
#import "EOCEmployer.h"
這樣做有什么優(yōu)點呢:
不在A的頭文件中引入B的頭文件蔑滓,就不會一并引入B的全部內(nèi)容郊酒,這樣就減少了編譯時間。
可以避免循環(huán)引用:因為如果兩個類在自己的頭文件中都引入了對方的頭文件键袱,那么就會導(dǎo)致其中一個類無法被正確編譯燎窘。
但是個別的時候,必須在頭文件中引入其他類的頭文件:
主要有兩種情況:
- 該類繼承于某個類蹄咖,則應(yīng)該引入父類的頭文件褐健。
- 該類遵從某個協(xié)議,則應(yīng)該引入該協(xié)議的頭文件澜汤。而且最好將協(xié)議單獨放在一個頭文件中蚜迅。
5. 類的布局
#pragma mark - Life Cycle Methods
- (instancetype)init
- (void)dealloc
- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated
#pragma mark - Override Methods
#pragma mark - Intial Methods
#pragma mark - Network Methods
#pragma mark - Target Methods
#pragma mark - Public Methods
#pragma mark - Private Methods
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - Lazy Loads
#pragma mark - NSCopying
#pragma mark - NSObject Methods
分類
1. 分類添加的方法需要添加前綴和下劃線
推薦這樣寫:
@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end
不推薦這樣寫:
@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end
2. 把類的實現(xiàn)代碼分散到便于管理的多個分類中
一個類可能會有很多公共方法,而且這些方法往往可以用某種特有的邏輯來分組俊抵。我們可以利用Objecctive-C的分類機制谁不,將類的這些方法按一定的邏輯劃入幾個分區(qū)中。
舉個??:
先看一個沒有使用無分類的類:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;
/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
分類之后:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
@end
@interface EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
@interface EOCPerson (Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end
@interface EOCPerson (Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
其中徽诲,F(xiàn)riendShip分類的實現(xiàn)代碼可以這么寫:
// EOCPerson+Friendship.h
#import "EOCPerson.h"
@interface EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"
@implementation EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person
{
/* ... */
}
- (void)removeFriend:(EOCPerson*)person
{
/* ... */
}
- (BOOL)isFriendsWith:(EOCPerson*)person
{
/* ... */
}
@end
注意:在新建分類文件時刹帕,一定要引入被分類的類文件。
通過分類機制馏段,可以把類代碼分成很多個易于管理的功能區(qū)轩拨,同時也便于調(diào)試。因為分類的方法名稱會包含分類的名稱院喜,可以馬上看到該方法屬于哪個分類中亡蓉。
利用這一點,我們可以創(chuàng)建名為Private的分類喷舀,將所有私有方法都放在該類里砍濒。這樣一來,我們就可以根據(jù)private一詞的出現(xiàn)位置來判斷調(diào)用的合理性硫麻,這也是一種編寫“自我描述式代碼(self-documenting)”的辦法爸邢。
單例
1. 單例不能作為容器對象來使用
單例對象不應(yīng)該暴露出任何屬性,也就是說它不能作為讓外部存放對象的容器拿愧。它應(yīng)該是一個處理某些特定任務(wù)的工具杠河,比如在iOS中的GPS和加速度傳感器。我們只能從他們那里得到一些特定的數(shù)據(jù)。
2. 使用dispatch_once來生成單例
推薦這樣寫:
+ (instancetype)sharedInstance
{
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
不推薦這樣寫:
+ (instancetype)sharedInstance
{
static id sharedInstance;
@synchronized(self) {
if (sharedInstance == nil) { sharedInstance = [[MyClass alloc] init];
} }
return sharedInstance;
}
相等性的判斷
判斷兩個person類是否相等的合理做法:
- (BOOL)isEqual:(id)object
{
if (self == object) {
return YES; //判斷內(nèi)存地址
}
if (![object isKindOfClass:[ZOCPerson class]]) {
return NO; //是否為當(dāng)前類或派生類
}
return [self isEqualToPerson:(ZOCPerson *)object];
}
//自定義的判斷相等性的方法
- (BOOL)isEqualToPerson:(Person *)person
{
if (!person) {
return NO;
}
BOOL namesMatch = (!self.name && !person.name) || [self.name isEqualToString:person.name];
BOOL birthdaysMatch = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}
方法文檔
一個函數(shù)(方法)必須有一個字符串文檔來解釋券敌,除非它:
- 非公開唾戚,私有函數(shù)。
- 很短待诅。
- 顯而易見叹坦。
而其余的,包括公開接口卑雁,重要的方法募书,分類,以及協(xié)議测蹲,都應(yīng)該伴隨文檔(注釋):
- 以/開始
- 第二行識總結(jié)性的語句
- 第三行永遠(yuǎn)是空行
- 在與第二行開頭對齊的位置寫剩下的注釋莹捡。
建議這樣寫:
/This comment serves to demonstrate the format of a doc string.
Note that the summary line is always at most one line long, and after the opening block comment,
and each line of text is preceded by a single space.
*/
看一個指定初始化方法的注釋:
/
* Designated initializer. *
* @param store The store for CRUD operations.
* @param searchService The search service used to query the store.
* @return A ZOCCRUDOperationsStore object.
*/
- (instancetype)initWithOperationsStore:(id<ZOCGenericStoreProtocol>)store searchService:(id<ZOCGenericSearchServiceProtocol>)searchService;
多用隊列,少用同步鎖來避免資源搶奪
多個線程執(zhí)行同一份代碼時扣甲,很可能會造成數(shù)據(jù)不同步道盏。建議使用GCD來為代碼加鎖的方式解決這個問題。
方案一:使用串行同步隊列來將讀寫操作都安排到同一個隊列里:
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
//讀取字符串
- (NSString*)someString
{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//設(shè)置字符串
- (void)setSomeString:(NSString*)someString
{
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
這樣一來文捶,讀寫操作都在串行隊列進(jìn)行,就不容易出錯媒咳。
但是粹排,還有一種方法可以讓性能更高:
方案二:將寫操作放入柵欄快中,讓他們單獨執(zhí)行涩澡;將讀取操作并發(fā)執(zhí)行顽耳。
_syncQueue = dispatch_queue_create("com.custom.queue", DISPATCH_QUEUE_CONCURRENT);
//讀取字符串
- (NSString*)someString
{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//設(shè)置字符串
- (void)setSomeString:(NSString*)someString
{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
顯然,數(shù)據(jù)的正確性主要取決于寫入操作妙同,那么只要保證寫入時射富,線程是安全的,那么即便讀取操作是并發(fā)的粥帚,也可以保證數(shù)據(jù)是同步的胰耗。
這里的dispatch_barrier_async方法使得操作放在了同步隊列里“有序進(jìn)行”,保證了寫入操作的任務(wù)是在串行隊列里芒涡。
實現(xiàn)description方法打印自定義對象信息
在打印我們自己定義的類的實例對象時柴灯,在控制臺輸出的結(jié)果往往是這樣的:
object = <EOCPerson: 0x7fd9a1600600>
這里只包含了類名和內(nèi)存地址,它的信息顯然是不具體的,遠(yuǎn)達(dá)不到調(diào)試的要求费尽。
但是赠群!如果在我們自己定義的類覆寫description方法,我們就可以在打印這個類的實例時輸出我們想要的信息旱幼。
例如:
- (NSString*)description
{
return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}
在這里查描,顯示了內(nèi)存地址,還有該類的所有屬性。
而且冬三,如果我們將這些屬性值放在字典里打印匀油,則更具有可讀性:
- (NSString*)description
{
return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
@{ @"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)}
];
}
輸出結(jié)果:
location = <EOCLocation: 0x7f98f2e01d20, {
latitude = "51.506";
longitude = 0;
title = London;
}>
我們可以看到,通過重寫description方法可以讓我們更加了解對象的情況长豁,便于后期的調(diào)試钧唐,節(jié)省開發(fā)時間。
NSArray& NSMutableArray
1. addObject之前要非空判斷匠襟。
2. 取下標(biāo)的時候要判斷是否越界钝侠。
3. 取第一個元素或最后一個元素的時候使用firtstObject和lastObject
NSCache
1. 構(gòu)建緩存時選用NSCache 而非NSDictionary
如果我們緩存使用得當(dāng),那么應(yīng)用程序的響應(yīng)速度就會提高酸舍。只有那種“重新計算起來很費事的數(shù)據(jù)帅韧,才值得放入緩存”,比如那些需要從網(wǎng)絡(luò)獲取或從磁盤讀取的數(shù)據(jù)啃勉。
在構(gòu)建緩存的時候很多人習(xí)慣用NSDictionary或者NSMutableDictionary忽舟,但是作者建議大家使用NSCache,它作為管理緩存的類淮阐,有很多特點要優(yōu)于字典叮阅,因為它本來就是為了管理緩存而設(shè)計的。
2. NSCache優(yōu)于NSDictionary的幾點:
- 當(dāng)系統(tǒng)資源將要耗盡時泣特,NSCache具備自動刪減緩沖的功能浩姥。并且還會先刪減“最久未使用”的對象。
- NSCache不拷貝鍵状您,而是保留鍵勒叠。因為并不是所有的鍵都遵從拷貝協(xié)議(字典的鍵是必須要支持拷貝協(xié)議的,有局限性)膏孟。
- NSCache是線程安全的:不編寫加鎖代碼的前提下眯分,多個線程可以同時訪問NSCache。
NSNotification
1. 通知的名稱
建議將通知的名字作為常量柒桑,保存在一個專門的類中:
// Const.h
extern NSString * const ZOCFooDidBecomeBarNotification
// Const.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";
2. 通知的移除
通知必須要在對象銷毀之前移除掉弊决。
其他
1. Xcode工程文件的物理路徑要和邏輯路徑保持一致。
2. 忽略沒有使用變量的編譯警告
對于某些暫時不用幕垦,以后可能用到的臨時變量丢氢,為了避免警告,我們可以使用如下方法將這個警告消除:
- (NSInteger)giveMeFive
{
NSString *foo;
#pragma unused (foo)
return 5;
}
3. 手動標(biāo)明警告和錯誤
手動明確一個錯誤:
- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor
{
#error Whoa, buddy, you need to check for zero here!
return (dividend / divisor);
}
手動明確一個警告:
- (float)divide:(float)dividend by:(float)divisor
{
#warning Dude, don't compare floating point numbers like this!
if (divisor != 0.0) {
return (dividend / divisor);
} else {
return NAN;
}
}