《Effective Objective-C 2.0》之《熟悉Objective-C》讀書筆記

之前閱讀過《Effective Objective-C 2.0》,覺得有些知識點忘記了,在此再把每個章節的內容都整理一遍

了解Objective-C語言的起源

大家可能知道,Objective-C語言使用的是消息結構
那我們看下消息與函數調用的卻別,看上去就像這樣:

//Messageing (Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];

//Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);

關鍵區別在于:使用消息結構的語言,其運行時所應執行的代碼由運行環境來決定;而使用函數調用的語言,則由編譯器決定。如果范例代碼中調用的函數是多態的,那么運行時就要按照“虛方法表”(virtual table)來查出到底應該執行哪個函數實現。而采用消息結構的語言,不論是否多態,總是運行時才會去查找所要執行的方法。
Objective-C的重要工作是由“運行期組件”(runtime component)而非編譯器來完成的。使用Objective-C的面向對象特性所需的全部數據結構以及函數都在運行期組件里面。舉例來說,運行期組件中含有全部內存管理方法。運行期組件本質上就是一種與開發者所編代碼相鏈接的“動態庫”(dynamic library),其代碼能把開發者編寫的所有程序粘合起來。這樣的話,只需要更新運行期組件,即可提升應用程序性能。而那種許多工作都在“編譯器”(compile time)完成的語言,若想要獲得類似的性能提升,則要重新編譯應用程序代碼。

要點
  • Objective-C為C語言添加了面向對象特性,是其超集。Objective-C使用動態綁定的消息結構,也就是說,在運行時才會檢查對象類型。接受一條消息之后,究竟應執行何種代碼,由運行期環境而非編譯器決定。
  • 理解C語言的核心概念有助于寫好Objective-C程序。尤其要掌握內存模型與指針。

在類的頭文件中盡量少引入其他頭文件

要點
  • 除非確有必要,否則不要引入頭文件。一般來說,應在某個類的頭文件中使用向前聲明來提及類別的類,并在實現文件中引入那些類的頭文件。這樣做可以盡量降低類之間的耦合(coupling)。
  • 有時無法使用向前聲明,比如要聲明某個類遵循一項協議。這種情況下,盡量把“該類遵循某協議”的這條聲明移至“class-continuation分類”中。如果不行的話,就把協議單獨放在頭文件中,然后將其引入。
什么是向前聲明

在編譯一個使用了ClassA類的文件時,不需要知道ClassA類的全部實現細節,只需要知道有一個類名叫ClassA就好。如下代碼:

#import <UIKit/UIKit.h>

@class ClassA;

@interface ClassB : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) ClassA *classA;
@end

其中@class ClassA就是向前聲明

將引入頭文件的時機盡量延后,只在確有需要時才引入,這樣就可以減少類的使用者所需引入的頭文件數量。如果直接引入頭文件,則可能要引入許多根本用不到的內容,從而增加編譯時間
同時,向前聲明也解決了兩個類相互引用的問題。假設要為ClassB類中添加新增以及刪除ClassA的方法,那么其頭文件中會加入下述定義:

- (void)addClassA:(ClassA *)classA;
- (void)removeClassA:(ClassA *)classA;

此時,若要編譯ClassB,則編譯器必須知道ClassA這個類,而要編譯ClassA,則又必須知道ClassB。如果在各自頭文件中引入對方的頭文件,則會導致“循環引用”(chicken-and-egg situation)。當解析其中一個頭文件時,編譯器會發現它引入了另一個頭文件,而那個頭文件又回過來引用第一個頭文件。使用#import而非#include指令雖然不會導致死循環,但這意味著兩個類里有一個無法被正確編譯。如果不信的話,讀者可以自己試試。
但是有的時候必須要將頭文件中引入其他頭文件。

  1. 如果你寫的類繼承自某個超類,則必須引入定義那個超類的頭文件。
  2. 同理,如果要聲明某個類遵循某個協議(protocol),那么該協議必須有完整定義,且不能使用向前聲明。

向前聲明只能告訴編輯器有個某個協議,而此時編譯器卻要知道該協議中定義的方法。
例如,要從圖形類中繼承一個矩形類,且令其遵循繪制協議:

// EOCRectangle.h

#import "EOCShape.h"
#import "EOCDrawable.h"

@interface EOCRectangle : EOCShape<EOCDrawable>
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
@end

第二條#import是難免的。鑒于此,最好把協議單獨放在一個頭文件中。要是把EOCDrawable協議放在了某個大的頭文件里,那么只要引入此協議,就必定會引入那個頭文件中的全部內容,如此一來,就像上面說的那樣,會產生相互依賴問題,而且還會增加編譯時間。

對于沒有必要協議暴露出來的情況,可以將協議放在“class-continuation分類”中。這樣的話,只要在實現文件中引入包含委托協議的頭文件即可,而不需要將其放在公共頭文件里。如下代碼:

// EOCRectangle.m

#import "EOCDrawable.h"

@interface EOCRectangle ()<EOCDrawable>

@end

此時,協議EOCDrawable就沒有暴露在頭文件中,可以避免相互依賴和增加編譯時間的問題

多用字面量語法,少用與之等價的方法

要點:
  • 應該使用字面量語法來創建字符串、數值、數組、字典。與創建此類對象的常規方法相比,那么做更加簡明扼要。
  • 應該通過取下標操作來訪問數組下標或字典中的鍵所對應的元素。
  • 用字面量語法創建數組或字典時,若值中有nil,則會拋出異常。因此。務必確保值不含nil。
字面量語法
//字符串
 NSString *someString = @"Effective Obejctive-C 2.0";
//數值
NSNumber *intNumber = @1;
NSNumber *floatNumber = @1.5f;
NSNumber *doubleFloatNumber = @1.5f;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';   
//數組
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
//字典 
NSDictionary *personData = @{@"firstNmae":@"Matt", @"lastName":@"Galloway", @"age":@28};
字面量語法取值
//數值
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
NSString *dog = animals[1];
//字典
NSDictionary *personData = @{@"firstNmae":@"Matt", @"lastName":@"Galloway", @"age":@28};
NSString *lastName = personData[@"lastName"];```
#####注意點
數組和字典使用字面量語法來初始化時,如果其他包含nil元素,會導致crash
#####局限性
使用字面量語法創建的對象為不可變的(immutable),如果需要獲得可變對象,需要復制一份,如下代碼:

NSMutableArray *mutable = @[@1, @2, @3, @4, @5].mutableCopy;


####多用類型變量,少用#define預處理命令
#####要點
* 不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只會在編譯前據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告信息,這將導致應用程序中的常量不一致。
* 在實現文件中使用static const來定義“只在編譯編譯單元內可見的常量”(translation-unit-specific constant)。由于此類常量不在全局符號表中,所以無需為其名稱加前綴。
* 在頭文件中使用extern來聲明全局常量,并在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類名做前綴。

比如,定義一個動畫時間常量

//不應該使用

define ANIMATION_DURATION 0.3

//應該使用
static const NSTimeInterval kAnimationDuration = 0.3;```

還要注意常量名稱。常用的命名是:若變量局限于某“編譯單元”(translation unit,也就是“實現文件”,implement file)之內,則在前面加字母k;若常量在類之外可見,則通常以類名為前綴。
若局限于 實現文件內,則可以用以上代碼,若作為全局變量,為了防止類名沖突,命名應該添加類名前綴,如下所示:

//EOCAnimatedView.h
extern const NSTimeInterval EOCAnimatedViewAnimationDuration;

//EOCAnimatedView.m
const NSTimeInterval EOCAnimatedViewAnimationDuration = 0.3;```
此時作為全局變量,不應該添加static來修飾。而且需要在頭文件中聲明該變量,添加extern前綴,這個可以參照C語言的語法。
這樣定義常量對于使用#define預處理命令來說,可以確保常量值不變。
####用枚舉表示狀態、選線、狀態碼
#####要點
* 應該用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
* 如果傳遞給某個方法的選項表示為枚舉類型,而多個選項又可以同時使用,那么就將各選項值定義為2的冪,以便通過按位或操作將其組合起來。
* 用NS_ENUM于NS_OPTIONS宏來定義枚舉類型,并指明底層數據類型。這樣做可以確保枚舉是用開發者所選的底層數據類型實現出來的,而不會采用編譯器所選的類型。
* 在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch語句并未處理所有枚舉。

#####樣例

//枚舉
typedef NS_ENUM(NSUInteger, EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
//枚舉使用switch
EOCConnectionState state = EOCConnectionStateDisconnected;
switch (state) {
case EOCConnectionStateDisconnected:
//Disconnected
break;
case EOCConnectionStateConnected:
//Connected
break;
case EOCConnectionStateConnecting:
//Connecting
break;
}
//選項,可以用來組合的枚舉
typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection) {
EOCPermittedDirectionUp = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 2,
};
//選項使用方法
EOCPermittedDirection direction = EOCPermittedDirectionUp | EOCPermittedDirectionDown;
if (direction & EOCPermittedDirectionUp) {
//Direction is up
}
if (direction & EOCPermittedDirectionDown) {
//Direction is down
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容