iOS 程式碼規範

J_Knight_發表於2017-06-14

利用上週的業餘時間把這篇規範整理了出來,我會將這篇規範作為我們iOS團隊的程式碼規範,並且還會根據讀者的反饋,專案的實踐和研究的深入做不定時更新,還希望各位朋友看了多多指正和批評。

這篇規範一共分為三個部分:

  1. 核心原則:介紹了這篇程式碼規範所遵循的核心原則。
  2. 通用規範:不侷限於iOS的通用性的程式碼規範(使用C語言和Swift語言)。
  3. iOS規範:僅適用於iOS的程式碼規範(使用Objective-C語言)。

一. 核心原則

原則一:程式碼應該簡潔易懂,邏輯清晰

因為軟體是需要人來維護的。這個人在未來很可能不是你。所以首先是為人編寫程式,其次才是計算機:

  • 不要過分追求技巧,降低程式的可讀性。
  • 簡潔的程式碼可以讓bug無處藏身。要寫出明顯沒有bug的程式碼,而不是沒有明顯bug的程式碼。

原則二:面向變化程式設計,而不是面向需求程式設計。

需求是暫時的,只有變化才是永恆的。 本次迭代不能僅僅為了當前的需求,寫出擴充套件性強,易修改的程式才是負責任的做法,對自己負責,對公司負責。

原則三:先保證程式的正確性,防止過度工程

過度工程(over-engineering):在正確可用的程式碼寫出之前就過度地考慮擴充套件,重用的問題,使得工程過度複雜。 引用《王垠:程式設計的智慧》裡的話:

  1. 先把眼前的問題解決掉,解決好,再考慮將來的擴充套件問題。
  2. 先寫出可用的程式碼,反覆推敲,再考慮是否需要重用的問題。
  3. 先寫出可用,簡單,明顯沒有bug的程式碼,再考慮測試的問題。

二. 通用規範

運算子


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. 多個不同的運算子同時存在時應該使用括號來明確優先順序

在多個不同的運算子同時存在的時候應該合理使用括號,不要盲目依賴操作符優先順序。 因為有的時候不能保證閱讀你程式碼的人就一定能瞭解你寫的算式裡面所有操作符的優先順序。

來看一下這個算式:2 << 2 + 1 * 3 - 4

這裡的<<是移位操作直觀上卻很容易認為它的優先順序很高,所以就把這個算式誤認為:(2 << 2) + 1 * 3 - 4

但事實上,它的優先順序是比加減法還要低的,所以該算式應該等同於:2 << 2 + 1 * 3 - 4。 所以在以後寫這種複雜一點的算式的時候,儘量多加一點括號,避免讓其他人誤解(甚至是自己)。

變數


1. 一個變數有且只有一個功能,儘量不要把一個變數用作多種用途

2. 變數在使用前應初始化,防止未初始化的變數被引用

3. 區域性變數應該儘量接近使用它的地方

推薦這樣寫:

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. 必須列出所有分支(窮舉所有的情況),而且每個分支都必須給出明確的結果。

推薦這樣寫:

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. 引數為nil
   if (!dict) {
     if (err) *err = [JSONModelError errorInputIsNil];
     return nil;
    }

    //方法2. 引數不是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. 條件表示式如果很長,則需要將他們提取出來賦給一個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. 條件語句的判斷應該是變數在左,常量在右

推薦這樣寫:

if ( count == 6) {
}
複製程式碼

或者

if ( object == nil) {
}
複製程式碼

或者

if ( !object ) {
}
複製程式碼

不推薦這樣寫:

if ( 6 == count) {
}
複製程式碼

或者

if ( nil == object ) {
}
複製程式碼

5. 每個分支的實現程式碼都必須被大括號包圍

推薦這樣寫:

if (!error) {
  return success;
}
複製程式碼

不推薦這樣寫:

if (!error)
    return success;
複製程式碼

或者

if (!error) return success; 
複製程式碼

6. 條件過多,過長的時候應該換行

推薦這樣寫:

if (condition1() && 
    condition2() && 
    condition3() && 
    condition4()) {
  // Do something複製程式碼

不推薦這樣寫:

if (condition1() && condition2() && condition3() && condition4()) {
  // Do something
}
複製程式碼

for語句


1. 不可在for迴圈內修改迴圈變數,防止for迴圈失去控制。

for (int index = 0; index < 10; index++){
   ...
   logicToChange(index)
}
複製程式碼

2. 避免使用continue和break。

continue和break所描述的是“什麼時候不做什麼”,所以為了讀懂二者所在的程式碼,我們需要在頭腦裡將他們取反。

其實最好不要讓這兩個東西出現,因為我們的程式碼只要體現出“什麼時候做什麼”就好了,而且通過適當的方法,是可以將這兩個東西消滅掉的:

2.1 如果出現了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的條件取反,併合併到主迴圈裡

在while裡的block其實就相當於“不存在”,既然是不存在的東西就完全可以在最開始的條件語句中將其排除。

while裡的break:

while (condition1) {
  ...
  if (condition2) {
    break;
  }
}

複製程式碼

取反併合併到主條件:

while (condition1 && !condition2) {
  ...
}
複製程式碼

####  2.3 在有返回值的方法裡消除break:將break轉換為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分支,在將來就無法通過編譯器來檢查新增的列舉型別了。

函式


1. 一個函式的長度必須限制在50行以內

通常來說,在閱讀一個函式的時候,如果視需要跨過很長的垂直距離會非常影響程式碼的閱讀體驗。如果需要來回滾動眼球或程式碼才能看全一個方法,就會很影響思維的連貫性,對閱讀程式碼的速度造成比較大的影響。最好的情況是在不滾動眼球或程式碼的情況下一眼就能將該方法的全部程式碼映入眼簾。

2. 一個函式只做一件事(單一原則)

每個函式的職責都應該劃分的很明確(就像類一樣)。

推薦這樣寫:

dataConfiguration()
viewConfiguration()
複製程式碼

不推薦這樣寫:

void dataConfiguration()
{   
   ...
   viewConfiguration()
}
複製程式碼

3. 對於有返回值的函式(方法),每一個分支都必須有返回值

推薦這樣寫:

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. 對輸入引數的正確性和有效性進行檢查,引數錯誤立即返回

推薦這樣寫:

void function(param1,param2)
{
      if(param1 is unavailable){
           return;
      }
    
      if(param2 is unavailable){
           return;
      }

     //Do some right thing
}

複製程式碼

5. 如果在不同的函式內部有相同的功能,應該把相同的功能抽取出來單獨作為另一個函式

原來的呼叫:

void logic() {
  a();
  b();
  if (logic1 condition) {
    c();
  } else {
    d();
  }
}

複製程式碼

將a,b函式抽取出來作為單獨的函式

void basicConfig() 
{
  a();
  b();
}
  
void logic1() 
{
  basicConfig();
  c();
}

void logic2() 
{
  basicConfig();
  d();
}
複製程式碼

6. 將函式內部比較複雜的邏輯提取出來作為單獨的函式

一個函式內的不清晰(邏輯判斷比較多,行數較多)的那片程式碼,往往可以被提取出去,構成一個新的函式,然後在原來的地方呼叫它這樣你就可以使用有意義的函式名來代替註釋,增加程式的可讀性。

舉一個傳送郵件的例子:

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();
複製程式碼

8. 避免使用全域性變數,類成員(class member)來傳遞資訊,儘量使用區域性變數和引數。

在一個類裡面,經常會有傳遞某些變數的情況。而如果需要傳遞的變數是某個全域性變數或者屬性的時候,有些朋友不喜歡將它們作為引數,而是在方法內部就直接訪問了:

 class A {
   var x;

   func updateX() 
   {
      ...
      x = ...;
   }

   func printX() 
   {
     updateX();
     print(x);
   }
 }

複製程式碼

我們可以看到,在printX方法裡面,updateX和print方法之間並沒有值的傳遞,乍一看我們可能不知道x從哪裡來的,導致程式的可讀性降低了。

而如果你使用區域性變數而不是類成員來傳遞資訊,那麼這兩個函式就不需要依賴於某一個類,而且更加容易理解,不易出錯:

func updateX() -> String
 {
    x = ...;
    return x;
 }

 func printX() 
 {
   String x = updateX();
   print(x);
 }
複製程式碼

註釋


優秀的程式碼大部分是可以自描述的,我們完全可以用程程式碼本身來表達它到底在幹什麼,而不需要註釋的輔助。

但並不是說一定不能寫註釋,有以下三種情況比較適合寫註釋:  1. 公共介面(註釋要告訴閱讀程式碼的人,當前類能實現什麼功能)。 2. 涉及到比較深層專業知識的程式碼(註釋要體現出實現原理和思想)。  3. 容易產生歧義的程式碼(但是嚴格來說,容易讓人產生歧義的程式碼是不允許存在的)。

除了上述這三種情況,如果別人只能依靠註釋才能讀懂你的程式碼的時候,就要反思程式碼出現了什麼問題。

最後,對於註釋的內容,相對於“做了什麼”,更應該說明“為什麼這麼做”。

Code Review


換行、註釋、方法長度、程式碼重複等這些是通過機器檢查出來的問題,是無需通過人來做的。

而且除了審查需求的實現的程度,bug是否無處藏身以外,更應該關注程式碼的設計。比如類與類之間的耦合程度,設計的可擴充套件性,複用性,是否可以將某些方法抽出來作為介面等等。

三. iOS規範

變數


1. 變數名必須使用駝峰格式

類,協議使用大駝峰:

HomePageViewController.h
<HeaderViewDelegate>
複製程式碼

物件等區域性變數使用小駝峰:

NSString *personName = @"";
NSUInteger totalCount = 0;
複製程式碼

2. 變數的名稱必須同時包含功能與型別

UIButton *addBtn //新增按鈕
UILabel *nameLbl //名字標籤
NSString *addressStr//地址字串
複製程式碼

3. 系統常用類作例項變數宣告時加入字尾

型別 字尾
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. 常量以相關類名作為字首

推薦這樣寫:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
複製程式碼

不推薦這樣寫:

static const NSTimeInterval fadeOutTime = 0.4;
複製程式碼

2. 建議使用型別常量,不建議使用#define預處理命令

首先比較一下這兩種宣告常量的區別:

  • 預處理命令:簡單的文字替換,不包括型別資訊,並且可被任意修改。
  • 型別常量:包括型別資訊,並且可以設定其使用範圍,而且不可被修改。

使用預處理雖然能達到替換文字的目的,但是本身還是有侷限性的:

  • 不具備型別資訊。
  • 可以被任意修改。

3. 對外公開某個常量:

如果我們需要傳送通知,那麼就需要在不同的地方拿到通知的“頻道”字串(通知的名稱),那麼顯然這個字串是不能被輕易更改,而且可以在不同的地方獲取。這個時候就需要定義一個外界可見的字串常量。

推薦這樣寫:

//標頭檔案
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
複製程式碼
//實現檔案
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. 巨集定義中如果包含表示式或變數,表示式和變數必須用小括號括起來。

#define MY_MIN(A, B)  ((A)>(B)?(B):(A))
複製程式碼

CGRect函式


其實iOS內部已經提供了相應的獲取CGRect各個部分的函式了,它們的可讀性比較高,而且簡短,推薦使用:

推薦這樣寫:

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型別建立typedef

如果我們需要重複建立某種block(相同引數,返回值)的變數,我們就可以通過typedef來給某一種塊定義屬於它自己的新型別

例如:

int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value)
{
     // Implementation
     return someInt;
}

複製程式碼

這個Block有一個bool引數和一個int引數,並返回int型別。我們可以給它定義型別:

typedef int(^EOCSomeBlock)(BOOL flag, int value);

再次定義的時候,就可以通過簡單的賦值來實現:

EOCSomeBlock block = ^(BOOL flag, int value){
     // Implementation
};

複製程式碼

定義作為引數的Block:

- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;

複製程式碼

這裡的Block有一個NSData引數,一個NSError引數並沒有返回值

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;”
複製程式碼

通過typedef定義Block簽名的好處是:如果要某種塊增加引數,那麼只修改定義簽名的那行程式碼即可。

字面量語法


儘量使用字面量值來建立 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. 屬性的關鍵字推薦按照 原子性,讀寫,記憶體管理的順序排列

推薦這樣寫:

@property (nonatomic, readwrite, copy) NSString *name;
@property (nonatomic, readonly, copy) NSString *gender;
@property (nonatomic, readwrite, strong) UIView *headerView;
複製程式碼

3. Block屬性應該使用copy關鍵字

推薦這樣寫:

typedef void (^ErrorCodeBlock) (id errorCode,NSString *message);
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock;//將block拷貝到堆中
複製程式碼

4. 形容詞性的BOOL屬性的getter應該加上is字首

推薦這樣寫:

@property (assign, getter=isEditable) BOOL editable;
複製程式碼

5. 使用getter方法做懶載入

例項化一個物件是需要耗費資源的,如果這個物件裡的某個屬性的例項化要呼叫很多配置和計算,就需要懶載入它,在使用它的前一刻對它進行例項化:

- (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方法可能會產生某些副作用,例如如果它修改了全域性變數,可能會產生難以排查的錯誤。

6. 除了init和dealloc方法,建議都使用點語法訪問屬性

使用點語法的好處:

setter:

  1. setter會遵守記憶體管理語義(strong, copy, weak)。
  2. 通過在內部設定斷點,有助於除錯bug。
  3. 可以過濾一些外部傳入的值。
  4. 捕捉KVO通知。

getter:

  1. 允許子類化。
  2. 通過在內部設定斷點,有助於除錯bug。
  3. 實現懶載入(lazy initialization)。

注意:

  1. 懶載入的屬性,必須通過點語法來讀取資料。因為懶載入是通過重寫getter方法來初始化例項變數的,如果不通過屬性來讀取該例項變數,那麼這個例項變數就永遠不會被初始化。
  2. 在init和dealloc方法裡面使用點語法的後果是:因為沒有繞過setter和getter,在setter和getter裡面可能會有很多其他的操作。而且如果它的子類過載了它的setter和getter方法,那麼就可能導致該子類呼叫其他的方法。

7. 不要濫用點語法,要區分好方法呼叫和屬性訪問

推薦這樣寫:

view.backgroundColor = [UIColor orangeColor]; 
[UIApplication sharedApplication].delegate; 
複製程式碼

不推薦這樣寫:

[view setBackgroundColor:[UIColor orangeColor]]; 
UIApplication.sharedApplication.delegate; 
複製程式碼

8. 儘量使用不可變物件

建議儘量把對外公佈出來的屬性設定為只讀,在實現檔案內部設為讀寫。具體做法是:

  • 在標頭檔案中,設定物件屬性為readonly
  • 在實現檔案中設定為readwrite

這樣一來,在外部就只能讀取該資料,而不能修改它,使得這個類的例項所持有的資料更加安全。而且,對於集合類的物件,更應該仔細考慮是否可以將其設為可變的。

如果在公開部分只能設定其為只讀屬性,那麼就在非公開部分儲存一個可變型。所以當在外部獲取這個屬性時,獲取的只是內部可變型的一個不可變版本,例如:

在公共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屬性設定為不可變的set。然後,提供了來增加和刪除這個set裡的元素的公共介面。

在實現檔案裡:


@interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

@end

@implementation EOCPerson {
     NSMutableSet *_internalFriends;  //實現檔案裡的可變集合
}

- (NSSet*)friends 
{
     return [_internalFriends copy]; //get方法返回的永遠是可變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;
}

複製程式碼

我們可以看到,在實現檔案裡,儲存一個可變set來記錄外部的增刪操作。

這裡最重要的程式碼是:

- (NSSet*)friends 
{
   return [_internalFriends copy];
}
複製程式碼

這個是friends屬性的獲取方法:它將當前儲存的可變set複製了一不可變的set並返回。因此,外部讀取到的set都將是不可變的版本。

方法


1. 方法名中不應使用and,而且簽名要與對應的引數名保持高度一致

推薦這樣寫:

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
複製程式碼

不推薦這樣寫:

- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
複製程式碼
- (instancetype)initWith:(int)width and:(int)height;
複製程式碼

2. 方法實現時,如果引數過長,則令每個引數佔用一行,以冒號對齊。

- (void)doSomethingWith:(NSString *)theFoo
                   rect:(CGRect)theRect
               interval:(CGFloat)theInterval
{
   //Implementation
}
複製程式碼

3. 私有方法應該在實現檔案中申明。

@interface ViewController ()
- (void)basicConfiguration;
@end

@implementation ViewController
- (void)basicConfiguration
{
   //Do some basic configuration
}
@end
複製程式碼

4. 方法名用小寫字母開頭的單片語合而成

- (NSString *)descriptionWithLocale:(id)locale;
複製程式碼

5. 方法名字首

  • 重新整理檢視的方法名要以refresh為首。
  • 更新資料的方法名要以update為首。

推薦這樣寫:

- (void)refreshHeaderViewWithCount:(NSUInteger)count;
- (void)updateDataSourceWithViewModel:(ViewModel*)viewModel;
複製程式碼

面向協議程式設計


如果某些功能(方法)具備可複用性,我們就需要將它們抽取出來放入一個抽象介面檔案中(在iOS中,抽象介面即協議),讓不同型別的物件遵循這個協議,從而擁有相同的功能。

因為協議是不依賴於某個物件的,所以通過協議,我們可以解開兩個物件之間的耦合。如何理解呢?我們來看一下下面這個例子:

現在有一個需求:在一個UITableViewController裡面拉取feed並展示出來。

方案一:

定義一個拉取feed的類ZOCFeedParser,這個類有一些代理方法實現feed相關功能:

@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裡面傳入ZOCFeedParser,並遵循其代理方法,實現feed的拉取功能。

@interface ZOCTableViewController : UITableViewController<ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser; 
@end 
複製程式碼

具體應用:

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,現在我們實現了需求:在ZOCTableViewController裡面存放了一個ZOCFeedParser物件來處理feed的拉取功能。

但這裡有一個嚴重的耦合問題:ZOCTableViewController只能通過ZOCFeedParser物件來處理feed的拉取功能。 於是我們重新審視一下這個需求:其實我們實際上只需要ZOCTableViewController拉取feed就可以了,而具體是由哪個物件來拉取,ZOCTableViewController並不需要關心。

也就是說,我們需要提供給ZOCTableViewController的是一個更範型的物件,這個物件具備了拉取feed的功能就好了,而不應該僅僅侷限於某個具體的物件(ZOCFeedParser)。所以,剛才的設計需要重新做一次修改:

方案二:

首先需要在一個介面檔案ZOCFeedParserProtocol.h裡面定義抽象的,具有拉取feed功能的協議:

@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僅僅是需要遵循上面這個協議就具備了拉取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
複製程式碼

這樣一來,ZOCTableViewControllerZOCFeedParser之間就沒有直接的關係了。以後,如果我們想:

  • 給這個feed拉取器增加新的功能:僅需要修改ZOCFeedParserProtocol.h檔案。
  • 更換一個feed拉取器例項:建立一個新型別來遵循ZOCFeedParserProtocol.h即可。

iOS 中委託的設計


1. 要區分好代理和資料來源的區別

在iOS開發中的委託模式包含了delegate(代理)和datasource(資料來源)。雖然二者同屬於委託模式,但是這兩者是有區別的。這個區別就是二者的資訊流方向是不同的:

  • delegate :事件發生的時候,委託者需要通知代理。(資訊流從委託者到代理)
  • datasource:委託者需要從資料來源拉取資料。(資訊流從資料來源到委託者)

然而包括蘋果也沒有做好榜樣,將它們徹底的區分開。就拿UITableView來說,在它的delegate方法中有一個方法:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
複製程式碼

這個方法正確地體現了代理的作用:委託者(tableview)告訴代理(控制器)“我的某個cell被點選了”。但是,UITableViewDelegate的方法列表裡還有這個方法:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
複製程式碼

該方法的作用是 由控制器來告訴tabievlew的行高,也就是說,它的資訊流是從控制器(資料來源)到委託者(tableview)的。準確來講,它應該是一個資料來源方法,而不是代理方法。

在UITableViewDataSource中,就有標準的資料來源方法:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
複製程式碼

這個方法的作用就是讓tableview向控制器拉取一個section數量的資料。

所以,在我們設計一個檢視控制元件的代理和資料來源時,一定要區分好二者的區別,合理地劃分哪些方法屬於代理方法,哪些方法屬於資料來源方法。

2. 代理方法的第一個引數必須為委託者

代理方法必須以委託者作為第一個引數(參考UITableViewDelegate)的方法。其目的是為了區分不同委託著的例項。因為同一個控制器是可以作為多個tableview的代理的。若要區分到底是哪個tableview的cell被點選了,就需要在``

  • (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath``方法中做個區分。

向代理髮送訊息時需要判斷其是否實現該方法

最後,在委託著向代理髮送訊息的時候,需要判斷委託著是否實現了這個代理方法:

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) { 
 [self.delegate signUpViewControllerDidPressSignUpButton:self]; 
} 
複製程式碼

3. 遵循代理過多的時候,換行對齊顯示

@interface ShopViewController () <UIGestureRecognizerDelegate,
                                  HXSClickEventDelegate,
                                  UITableViewDelegate,
                                  UITableViewDataSource>
複製程式碼

4. 代理的方法需要明確必須執行和可不執行

代理方法在預設情況下都是必須執行的,然而在設計一組代理方法的時候,有些方法可以不是必須執行(是因為存在預設配置),這些方法就需要使用@optional關鍵字來修飾:

@protocol ZOCServiceDelegate <NSObject>@optional- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries
@end 
複製程式碼


1. 類的名稱應該以三個大寫字母為字首;建立子類的時候,應該把代表子類特點的部分放在字首和父類名的中間

推薦這樣寫:


//父類
ZOCSalesListViewController

//子類
ZOCDaySalesListViewController
ZOCMonthSalesListViewController
複製程式碼

2. initializer && dealloc

推薦:

  • 將 dealloc 方法放在實現檔案的最前面
  • 將init方法放在dealloc方法後面。如果有多個初始化方法,應該將指定初始化方法放在最前面,其他初始化方法放在其後。

2.1 dealloc方法裡面應該直接訪問例項變數,不應該用點語法訪問

2.2 init方法的寫法:

-  init方法返回型別必須是instancetype,不能是id。

  • 必須先實現[super init]。
- (instancetype)init 
{ 
    self = [super init]; // call the designated initializer 
    if (self) { 
        // Custom initialization return self; 
} 
複製程式碼

2.3 指定初始化方法

指定初始化方法(designated initializer)是提供所有的(最多的)引數的初始化方法,間接初始化方法(secondary initializer)有一個或部分引數的初始化方法。

注意事項1:間接初始化方法必須呼叫指定初始化方法。

@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:如果直接父類有指定初始化方法,則必須呼叫其指定初始化方法

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 
    if (self) {
    }
    return self; 
}
複製程式碼

注意事項3:如果想在當前類自定義一個新的全能初始化方法,則需要如下幾個步驟

  1. 定義新的指定初始化方法,並確保呼叫了直接父類的初始化方法。
  2. 過載直接父類的初始化方法,在內部呼叫新定義的指定初始化方法。
  3. 為新的指定初始化方法寫文件。

看一個標準的例子:

@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 
複製程式碼

在這裡,過載父類的初始化方法並在內部呼叫新定義的指定初始化方法的原因是你不能確定呼叫者呼叫的就一定是你定義的這個新的指定初始化方法,而不是原來從父類繼承來的指定初始化方法。

假設你沒有過載父類的指定初始化方法,而呼叫者卻恰恰呼叫了父類的初始化方法。那麼呼叫者可能永遠都呼叫不到你自己定義的新指定初始化方法了。

而如果你成功定義了一個新的指定初始化方法並能保證呼叫者一定能呼叫它,你最好要在文件中明確寫出哪一個才是你定義的新初始化方法。或者你也可以使用編譯器指令__attribute__((objc_designated_initializer))來標記它。

3. 所有返回類物件和例項物件的方法都應該使用instancetype

將instancetype關鍵字作為返回值的時候,可以讓編譯器進行型別檢查,同時適用於子類的檢查,這樣就保證了返回型別的正確性(一定為當前的類物件或例項物件)

推薦這樣寫:

@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name; 
@end 
複製程式碼

不推薦這樣寫:

@interface ZOCPerson
+ (id)personWithName:(NSString *)name; 
@end 
複製程式碼

###  4. 在類的標頭檔案中儘量少引用其他標頭檔案

有時,類A需要將類B的例項變數作為它公共API的屬性。這個時候,我們不應該引入類B的標頭檔案,而應該使用向前宣告(forward declaring)使用class關鍵字,並且在A的實現檔案引用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"

複製程式碼

這樣做有什麼優點呢:

  • 不在A的標頭檔案中引入B的標頭檔案,就不會一併引入B的全部內容,這樣就減少了編譯時間。
  • 可以避免迴圈引用:因為如果兩個類在自己的標頭檔案中都引入了對方的標頭檔案,那麼就會導致其中一個類無法被正確編譯。

但是個別的時候,必須在標頭檔案中引入其他類的標頭檔案:

主要有兩種情況:

  1. 該類繼承於某個類,則應該引入父類的標頭檔案。
  2. 該類遵從某個協議,則應該引入該協議的標頭檔案。而且最好將協議單獨放在一個標頭檔案中。

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. 把類的實現程式碼分散到便於管理的多個分類中

一個類可能會有很多公共方法,而且這些方法往往可以用某種特有的邏輯來分組。我們可以利用Objecctive-C的分類機制,將類的這些方法按一定的邏輯劃入幾個分割槽中。

舉個?:

先看一個沒有使用無分類的類:

#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

複製程式碼

其中,FriendShip分類的實現程式碼可以這麼寫:


// 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

複製程式碼

注意:在新建分類檔案時,一定要引入被分類的類檔案。

通過分類機制,可以把類程式碼分成很多個易於管理的功能區,同時也便於除錯。因為分類的方法名稱會包含分類的名稱,可以馬上看到該方法屬於哪個分類中。

利用這一點,我們可以建立名為Private的分類,將所有私有方法都放在該類裡。這樣一來,我們就可以根據private一詞的出現位置來判斷呼叫的合理性,這也是一種編寫“自我描述式程式碼(self-documenting)”的辦法。

單例


1. 單例不能作為容器物件來使用

單例物件不應該暴露出任何屬性,也就是說它不能作為讓外部存放物件的容器。它應該是一個處理某些特定任務的工具,比如在iOS中的GPS和加速度感測器。我們只能從他們那裡得到一些特定的資料。

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; //判斷記憶體地址
    } 
  
    if (![object isKindOfClass:[ZOCPerson class]]) { 
     return NO; //是否為當前類或派生類 
     } 

     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; 
} 
複製程式碼

方法文件


一個函式(方法)必須有一個字串文件來解釋,除非它:

  • 非公開,私有函式。
  • 很短。
  • 顯而易見。

而其餘的,包括公開介面,重要的方法,分類,以及協議,都應該伴隨文件(註釋):

  • 以/開始
  • 第二行識總結性的語句
  • 第三行永遠是空行
  • 在與第二行開頭對齊的位置寫剩下的註釋。

建議這樣寫:

/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;
複製程式碼

多用佇列,少用同步鎖來避免資源搶奪


多個執行緒執行同一份程式碼時,很可能會造成資料不同步。建議使用GCD來為程式碼加鎖的方式解決這個問題。

####方案一:使用序列同步佇列來將讀寫操作都安排到同一個佇列裡:

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);

//讀取字串
- (NSString*)someString 
{

         __block NSString *localSomeString;
         dispatch_sync(_syncQueue, ^{
            localSomeString = _someString;
        });
         return localSomeString;

}

//設定字串
- (void)setSomeString:(NSString*)someString 
{

     dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

複製程式碼

這樣一來,讀寫操作都在序列佇列進行,就不容易出錯。

但是,還有一種方法可以讓效能更高:

####方案二:將寫操作放入柵欄快中,讓他們單獨執行;將讀取操作併發執行。

_syncQueue = dispatch_queue_create("com.custom.queue", DISPATCH_QUEUE_CONCURRENT);

//讀取字串
- (NSString*)someString 
{

     __block NSString *localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}
複製程式碼
//設定字串
- (void)setSomeString:(NSString*)someString 
{

     dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });

}

複製程式碼

顯然,資料的正確性主要取決於寫入操作,那麼只要保證寫入時,執行緒是安全的,那麼即便讀取操作是併發的,也可以保證資料是同步的。

這裡的dispatch_barrier_async方法使得操作放在了同步佇列裡“有序進行”,保證了寫入操作的任務是在序列佇列裡。

實現description方法列印自定義物件資訊


在列印我們自己定義的類的例項物件時,在控制檯輸出的結果往往是這樣的:

object = <EOCPerson: 0x7fd9a1600600>
複製程式碼

這裡只包含了類名和記憶體地址,它的資訊顯然是不具體的,遠達不到除錯的要求。

但是!如果在我們自己定義的類覆寫description方法,我們就可以在列印這個類的例項時輸出我們想要的資訊。

例如:


- (NSString*)description 
{
     return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}

複製程式碼

在這裡,顯示了記憶體地址,還有該類的所有屬性。

而且,如果我們將這些屬性值放在字典裡列印,則更具有可讀性:

- (NSString*)description 
{

     return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
   
    @{    @"title":_title,
       @"latitude":@(_latitude),
      @"longitude":@(_longitude)}
    ];
}
複製程式碼

輸出結果:

location = <EOCLocation: 0x7f98f2e01d20, {

    latitude = "51.506";
   longitude = 0;
       title = London;
}>
複製程式碼

我們可以看到,通過重寫description方法可以讓我們更加了解物件的情況,便於後期的除錯,節省開發時間。

NSArray& NSMutableArray


1. addObject之前要非空判斷。

2. 取下標的時候要判斷是否越界。

3. 取第一個元素或最後一個元素的時候使用firtstObject和lastObject

NSCache


1. 構建快取時選用NSCache 而非NSDictionary

如果我們快取使用得當,那麼應用程式的響應速度就會提高。只有那種“重新計算起來很費事的資料,才值得放入快取”,比如那些需要從網路獲取或從磁碟讀取的資料。

在構建快取的時候很多人習慣用NSDictionary或者NSMutableDictionary,但是作者建議大家使用NSCache,它作為管理快取的類,有很多特點要優於字典,因為它本來就是為了管理快取而設計的。

2. NSCache優於NSDictionary的幾點:

  • 當系統資源將要耗盡時,NSCache具備自動刪減緩衝的功能。並且還會先刪減“最久未使用”的物件。
  • NSCache不拷貝鍵,而是保留鍵。因為並不是所有的鍵都遵從拷貝協議(字典的鍵是必須要支援拷貝協議的,有侷限性)。
  • 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. 手動標明警告和錯誤

手動明確一個錯誤:

- (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); 
     } elsereturn NAN; 
 } 
} 
複製程式碼

參考文獻:

  1. 王垠:程式設計的智慧
  2. 美團點評技術團隊:聊聊clean code
  3. 禪與 Objective-C 程式設計藝術
  4. J_Knight 的文集:iOS - 《Effective Objective-C 2.0》
  5. 蝴蝶之夢天使:iOS程式碼程式設計規範-根據專案經驗彙總
  6. 高家二少爺:Objective-C高質量程式碼參考規範

本篇已經同步到個人部落格:傳送門

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章