Objective-C 能夠算做 Apple 平臺上「惟一的」開發語言。不少 Objective-C 的教程每每直接從 Objective-C 開始講起。不過,在我看來,這樣作有時候是不合適的。不少程序員每每已經掌握了另一種開發語言,若是對一門新語言的理解創建在他們已有的知識之上,更能 起到事半功倍的效果。既然名爲 Objective-C,它與 C 語言的聯繫更加密切,然而它又是 Objective 的。與 C 語言聯繫密切,而且是 Objective 的,咱們可以想到的另一門語言就是 C++。C++ 的開發人員也更廣泛,受衆也會更多。因而就有了本系列,從 C++ 的角度來說述 Objective-C 的相關知識。不過,相比 C++,C# 彷佛更近一些。不過,咱們仍是還用 C++ 做爲對比。這個系列不會做爲一個完整的手冊,僅僅是入門。本系列文章不會告訴你 Objective-C 裏面的循環怎麼寫,而是經過與 C++ 的對比來學習 Objective-C 一些更爲高級的內容,例如類的實現等等。若是要更好的使用 Objective-C,你須要閱讀更多資料。可是,相信在本系列基礎之上,你在閱讀其餘資料時應該會理解的更加透徹一些。 程序員
說明:本系列大體翻譯來自《From C++ to Objective-C》,你能夠在這裏找到它的英文 pdf 版本。 算法
下面來簡單介紹一下 Objective-C。 數據庫
要 說 Objective-C,首先要從 Smalltalk 提及。Smalltalk 是第一個真正意義上的面嚮對象語言。Smalltalk 出現以後,不少人都但願能在 C 語言的基礎之上增長面向對象的特性。因而就出現了兩種新語言:C++ 和 Objective-C。C++ 沒必要多說,不少人都比較熟悉。Objective-C 則比較冷門。它徹底借鑑了 Smalltalk 的思想,有着相似的語法和動態機制;相比來講,C++ 則更加靜態一些,目的在於提供能好的性能。Objective-C 最新版本是 2.0.咱們的這個系列就是以 Objective-C 2.0 爲基礎講解。 數組
Objective-C 是一門語言,而 Cocoa 是這門語言用於 MacOS X 開發的一個類庫。它們的關係相似於 C++ 和 Qt,Java 和 Spring 同樣。因此,咱們徹底能夠不使用 Cocoa,只去用 Objective-C。例如 gcc 就是一個不使用 Cocoa 的編譯器。不過在 MacOS X 平臺,幾乎全部的功能都要依賴 Cocoa 完成。咱們這裏只是作一個區別,應該分清 Objective-C 和 Cocoa 的關係。 緩存
Objective-C 是 C 語言的超集。相似於 C++,良好的 C 源代碼可以直接被 Objective-C 編譯器編譯。不一樣於 C++ 直接改變 C 語言的設計思路,Objective-C 僅僅是在 C 語言的基礎上增長了一些概念。例如,對於類的概念,C++ 是增長了一個全新的關鍵字 class,把它做爲語言內置的特性,而 Objective-C 則是將類轉換成一個 struct 去處理。因此,爲了不衝突,Objective-C的關鍵字都是以 @ 開頭。一個簡單的關鍵字列表是:@class , @interface , @implementation, @public ,@private , @protected , @try , @catch, @throw, @finally , @end , @protocol,@selector, @synchronized, @encode, @defs。Objective-C 2.0 又增長了 @optional, @required, @property, @dynamic, @synthesize 這幾個。 安全
另 外的一些值一樣也相似於關鍵字,有 nil 和 Nil, 類型 id, SEL 和 BOOL, 布爾變量 YES 和 NO。最後,特定上下文中會有一些關鍵字,分別是:in, out, inout, bycopy, byref, oneway 和 getter, setter, readwrite, readonly, assign,retain, copy, nonatomic 等。 多線程
很 多繼承自 NSObject 的函數很容易與關鍵字混淆。好比 alloc, release 和 autorelease 等。這些實際都是 NSObject 的函數。另一個須要注意的是self 和 super。self 其實是每個函數的隱藏參數,而 super 是告知編譯器使用 self 的另外語義。 併發
Objective-C 使用 // 和 /*…*/ 兩種註釋風格。 app
Objective-C 容許在代碼塊的中部聲明變量,而不只僅在塊的最開始處。 框架
BOOL, YES, NO
C++ 中使用 bool 表示布爾類型。Objective-C 中則是使用 BOOL,其值爲 YES 和 NO。
nil, Nil 和 id
簡單來講:
· 每個對象都是 id 類型的。該類型能夠做爲一種弱類型使用。id 是一個指針,因此在使用時應注意是否須要再加 *。例如 id*foo = nil,實際是定義一個指針的指針;
· nil 等價於指向對象的 NULL 指針。nil 和NULL 不該該被混用。實際上,nil 並不簡單是 NULL 指針;
· Nil 等價於指針 nil 的類。在 Objective-C 中,一個類也是一個對象(做爲元類 Meta-Class 的實例)。nil 表明 NULL 指針,但它也是一個類的對象,nil 就是 Nil類的實例。C++ 沒有對應的概念,不過,若是你熟悉 Java 的話,應該知道每個類對象都對應一個 Class 實例,相似這個。
SEL
SEL 用於存儲選擇器 selector 的值。所謂選擇器,就是不屬於任何類實例對象的函數標識符。這些值能夠由 @selector 獲取。選擇器能夠當作函數指針,但實際上它並非一個真正的指向函數的指針。
@encode
爲了更好的互操做性,Objective-C 的數據類型,甚至自定義類型、函數或方法的元類型,均可以使用 ASCII 編碼。@encode(aType) 能夠返回該類型的 C 字符串(char *)的表示。
與 C++ 相似,Objective-C一樣建議將聲明和實現區分開。Objective-C 的頭文件後綴名是 .h,源代碼後綴名是 .m。Objective-C 使用 #import 引入其它頭文件。與 #include 不一樣的是,#import 保證頭文件只被引入一次。另外,#import 不只僅針對 Objective-C 的頭文件,即使是標準 C 的頭文件,好比 stdlib.h,一樣能夠使用 #import 引入。
C++ |
|
頭文件 |
源文件 |
//In file Foo.h
#ifndef __FOO_H__ //compilation guard
#define __FOO_H__ //
class Foo
{
...
};
#endif
|
//In file Foo.cpp
#include "Foo.h"
...
|
Objective-C |
|
頭文件 |
源文件 |
//In file Foo.h
//class declaration, different from
//the "interface" Java keyword
@interface Foo : NSObject
{
...
}
@end
|
咱們前面看到的類 NSObject,NSString 都有一個前綴 NS。這是 Cocoa 框架的前綴(Cocoa 開發公司是 NeXTStep)。
Objective-C 並非「使用方括號表示函數調用」的語言。一開始很容易把
[object doSomething];
理解成
object.doSomething();
但 實際上並非這麼簡單。Objective-C 是 C 語言的超集,所以,函數和 C 語言的聲明、定義、調用是一致的。C 語言並無方法這一律念,所以方法是使用特殊語法,也就是方括號。不只僅是語法上的,語義上也是不一樣的:這並非方法調用,而是發送一條消息。看上去並沒 有什麼區別,實際上,這是 Objective-C 的強大之處。例如,這種語法容許你在運行時動態添加方法。
既然是面嚮對象語言,類和對象顯然是應該優先考慮的內容。鑑於本系列已經假定你已經熟悉 C++ 語言,天然就不會去解釋類和對象的含義。咱們直接從 Objecti-C 和 C++ 的區別開始提及。
Objetive-C 使用的是嚴格的對象模型,相比之下,C++ 的對象模型則更爲鬆散。例如,在 Objective-C 中,全部的類都是對象,而且能夠被動態管理:也就是說,你能夠在運行時增長新的類,根據類的名字實例化一個類,以及調用類的方法。這比 C++ 的 RTTI 更增強大,然後者只不過是爲一個「static」的語言增長的一點點功能而已。C++ 的 RTTI 在不少狀況下是不被推薦使用的,由於它過於依賴編譯器的實現,犧牲了跨平臺的能力。
任 何一個面向對象的語言都要管理不少類。同 Java 相似,Objective-C 有一個根類,全部的類都應該繼承自這個根類(值得注意的是,在 Java 中,你聲明一個類而不去顯式指定它繼承的父類,那麼這個類就是 Object 類的直接子類;然而,在 Objective-C 中,單根類的子類必須被顯式地說明);而 C++ 並無這麼一個類。Cocoa 中,這個根類就是 NSObject,它提供了不少運行時所必須的能力,例如內存分配等等。另外須要說明一點,單根類並非 Objective-C 語言規範要求的,它只不過是根據面向對象理論實現的。所以,全部 Java 虛擬機的實現,這個單根類都是 Object,可是在Objective-C 中,這就是與類庫相關的了:在Cocoa 中,這個單根類是 NSObject,而在 gcc 的實現裏則是 Object。
嚴格說來,每個類都應該是 NSObject 的子類(相比之下,Java 應該說,每個類都必須是 Object 的子類),所以使用 NSObject * 類型應該能夠指到全部類對象的指針。可是,實際上咱們使用的是 id 類型。這個類型更加簡短,更重要的是,id 類型是動態類型檢查的,相比來講,NSObject * 則是靜態類型檢查。Objective-C 裏面沒有泛型,那麼,咱們就能夠使用 id 很方便的實現相似泛型的機制了。在 Objective-C 裏面,指向空的指針應該聲明爲 nil,不能是 NULL。這二者雖然很類似但並不能夠互換。一個普通的 C 指針能夠指向 NULL,可是Objective-C 的類指針必須指向 nil。正如前文所說,Objective-C 裏面,類也是對象(元類 Meta-Class 的對象)。nil 所對應的類就是 Nil。
屬性和方法
在 Objective-C 裏面,屬性 attributes 被稱爲實例數據 instance data,成員函數 member functions 被稱爲方法 methods。若是沒有特殊說明,在後續文章中,這兩組術語都會被混用,你們見諒。
C++ |
Objective-C |
class Foo
{
double x;
public:
int f(int x);
float g(int x, int y);
};
int Foo::f(int x) {...}
float Foo::g(int x, int y) {...}
|
@interface Foo : NSObject
{
double x;
}
-(int) f:(int)x;
-(float) g:(int)x :(int)y;
@end
@implementation Foo
-(int) f:(int)x {...}
-(float) g:(int)x :(int)y {...}
@end
|
在 C++ 中,屬性和成員函數都在類的花括號塊中被聲明。方法的實現相似於 C 語言,只不過須要有做用於指示符(Foo::)來講明這個函數屬於哪一個類。
Objective-C 中,屬性和方法必須分開聲明。屬性在花括號中聲明,方法要跟在下面。它們的實現要在 @implementation 塊中。
這是與 C++ 的主要不一樣。在Objective-C 中,有些方法能夠不被暴露在接口中,例如 private 的。而 C++ 中,即使是 private 函數,也可以在頭文件中被看到。簡單來講,這種分開式的聲明能夠避免 private 函數污染頭文件。
實例方法以減號 – 開頭,而 static 方法以 + 開頭。注意,這並非 UML 中的 private 和 public 的區別!參數的類型要在小括號中,參數之間使用冒號 : 分隔。
Objective-C 中,類聲明的末尾不須要使用分號 ;。同時注意,Objective-C 的類聲明關鍵字是 @interface ,而不是 @class。@class 關鍵字只用於前向聲明。最後,若是類裏面沒有任何數據,那麼花括號能夠被省略。
前向聲明
爲 避免循環引用,C 語言有一個前向聲明的機制,即僅僅告訴存在性,而不理會具體實現。C++ 使用 class 關鍵字實現前向聲明。在 Objective-C 中則是使用 @class 關鍵字;另外,還能夠使用 @protocol 關鍵字來聲明一個協議(咱們會在後面說到這個概念,相似於 Java 裏面的 interface)。
C++ |
|
//In file Foo.h
#ifndef __FOO_H__
#define __FOO_H__
class Bar; //forward declaration
class Foo
{
Bar* bar;
public:
void useBar(void);
};
#endif
|
//In file Foo.cpp
#include "Foo.h"
#include "Bar.h"
void Foo::useBar(void)
{
...
}
|
Objective-C |
|
//In file Foo.h
@class Bar; //forward declaration
@interface Foo : NSObject
{
Bar* bar;
}
-(void) useBar;
@end
|
private,protected 和 public
訪問可見性是面嚮對象語言的一個很重要的概念。它規定了在源代碼級別哪些是可見的。可見性保證了類的封裝性。
C++ |
Objective-C |
class Foo
{
public:
int x;
int apple();
protected:
int y;
int pear();
private:
int z;
int banana();
};
|
@interface Foo : NSObject
{
@public :
int x;
@protected :
int y;
@private :
int z;
}
-(int) apple;
-(int) pear;
-(int) banana;
@end
|
在 C++ 中,屬性和方法能夠是 private,protected 和 public 的。默認是 private。
在 Objective-C 中,只有成員數據能夠是 private,protected 和 public 的,默認是 protected 。方法只能是 public 的。然而,咱們能夠在 @implementation 塊中實現一些方法,而不在 @interface 中聲明;或者是使用分類機制(class categories)。這樣作雖然不能阻止方法被調用,可是減小了暴露。不通過聲明實現一些方法是 Objective-C 的一種特殊屬性,有着特殊的目的。咱們會在後面進行說明。
Objective-C 中的繼承只能是 public 的,不能夠是 private 和 protected 繼承。這一點,Objective-C 更像 Java 而不是 C++。
static 屬性
Objective-C 中不容許聲明 static 屬性。可是,咱們有一些變通的方法:在實現文件中使用全局變量(也能夠添加 static 關鍵字來控制可見性,相似 C 語言)。這樣,類就能夠經過方法訪問到,而這樣的全局變量的初始化能夠在類的 initialize 方法中完成。
Objective-C 中的方法與 C++ 的函數在語法方面風格迥異。下面,咱們就來說述 Objective-C 的方法。
原型、調用、實例方法和類方法
· 以 – 開頭的是實例方法(多數狀況下都應該是實例方法);以 + 開頭的是類方法(至關於 C++ 裏面的static 函數)。Objective-C的方法都是 public 的;
· 返回值和參數的類型都須要用小括號括起來;
· 參數之間使用冒號:分隔;
· 參數能夠與一個標籤 label 關聯起來,所謂標籤,就是在 : 以前的一個名字。標籤被認爲是方法名字的一部分。這使得方法比函數更易讀。事實上,咱們應該始終使用標籤。注意,第一個參數沒有標籤,一般它的標籤就是指的方法名;
· 方法名能夠與屬性名相同,這使 getter 方法變得很簡單。
C++
// 原型
void Array::insertObject(void *anObject, unsigned int atIndex);
// shelf 是 Array 類的一個實例,book 是一個對象
shelf.insertObject(book, 2);
Objective-C(不帶 label,即直接從 C++ 翻譯來)
// 方法原型
// 方法名字是「insertObject::」
// 這裏的冒號:用來分隔參數,成爲方法名的一部分(注意,這不一樣於 C++ 的域指示符::)
-(void) insertObject:(id)anObject:(unsigned int)index
// shelf 是 Array 類的一個實例,book 是一個對象
[shelf insertObject:book:2];
Objective-C(帶有 label)
// 方法原型。「index」 有一個標籤「atIndex」
// 方法名爲「insertObject:atIndex:」
// 這樣的話,調用語句就很容易閱讀了
-(void) insertObject:(id)anObject atIndex:(unsigned int)index
// shelf 是 Array 類的一個實例,book 是一個對象
[shelf insertObject:book:2]; // 錯誤!
[shelf insertObject:book atIndex:2]; // 正確
注 意,方括號語法不該該讀做「調用 shelf 對象的 insertObject 方法」,而應該是「向 shelf 對象發送一個 insertObject 消息」。這是Objective-C 的實現方式。你能夠向任何對象發送任何消息。若是目標對象不能處理這個消息,它就會將消息忽略(這會引起一個異常,但不會終止程序)。若是接收到一個消 息,目標對象可以處理,那麼,目標對象就會調用相應的方法。若是編譯器可以知道目標對象沒有匹配的方法,那麼編譯器就會發出一個警告。鑑於 Objective-C 的前向機制,這並不會做爲一個錯誤。若是目標對象是 id 類型,那麼在編譯期就不會有警告,可是運行期可能會有潛在的錯誤。
this,self 和 super
一個消息有兩個特殊的目標對象:self 和 super。self 指當前對象(相似 C++ 的 this),super 指父對象。Objective-C 裏面沒有 this 指針,取而代之的是 self。
注意,self 不是一個關鍵字。實際上,它是每一個消息接收時的隱藏參數,其值就是當前對象。它的值能夠被改變,這一點不一樣於 C++ 的 this 指針。然而,這一點僅僅在構造函數中有用。
在方法中訪問實例變量
同 C++ 同樣,Objective-C在方法中也能夠訪問當前對象的實例變量。不一樣之處在於,C++ 須要使用 this->,而Objective-C 使用的是 self->。
C++ |
Objective-C |
class Foo
{
int x;
int y;
void f(void);
};
void Foo::f(void)
{
x = 1;
int y; // 隱藏 this->y
y = 2; // 使用局部變量 y
this->y = 3; // 顯式使用成員變量
}
|
@interface Foo : NSObject
{
int x;
int y;
}
-(void) f;
@end
@implementation Foo
-(void) f
{
x = 1;
int y; // 隱藏 super->y y = 2; // 使用局部變量 y self->y = 3; // 顯式使用成員變量 }
@end
|
原型的 id、簽名和重載
函數就是一段可以被引用的代碼,例如使用函數指針。通常的,方法名會做爲引用方法的惟一 id,可是,這就須要當心有重載的狀況。C++ 和 Objective-C 使用大相徑庭的兩種方式去區分:前者使用參數類型,後者使用參數標籤。
在 C++ 中,只要函數具備不一樣的參數類型,它們就能夠具備相同的名字。const 也能夠做爲一種重載依據。
C++
int f(int);
int f(float); // 容許,float 和 int 是不一樣類型
class Foo
{
public:
int g(int);
int g(float); // 容許,float 和 int 是不一樣類型
int g(float) const; // 容許,const 能夠做爲重載依據
};
class Bar
{
public:
int g(int); // 容許,咱們使用的是 Bar::,而不是 Foo::
}
在 Objective-C 中,全部的函數都是普通的 C 函數,不能被重載(除非指定使用 C99標準)。方法則具備不一樣的語法,重載的依據是 label。
Objective-C
int f(int);
int f(float); // 錯誤!C 函數不容許重載
@interface Foo : NSObject
{
}
-(int) g:(int) x;
-(int) g:(float) x; // 錯誤!類型不一樣不做爲重載依據,同上一個沒有區別
-(int) g:(int) x :(int) y; // 正確:兩個匿名 label
-(int) g:(int) x :(float) y; // 錯誤:同上一個沒有區別
-(int) g:(int) x andY:(int) y; // 正確:第二個 label 是 「andY」
-(int) g:(int) x andY:(float) y; // 錯誤:同上一個沒有區別
-(int) g:(int) x andAlsoY:(int) y; // 正確:第二個 label 是 「andAlsoY」
@end
基於 label 的重載能夠很明白地解釋方法的名字,例如:
@interface Foo : NSObject {}
// 方法名是「g」
-(int) g;
// 方法名是「g:」
-(int) g:(float) x;
// 方法名是「g::」
-(int) g:(float) x :(float) y;
// 方法名是「g:andY:」
-(int) g:(float) x andY:(float) y;
// 方法名是「g:andZ:」
-(int) g:(float) x andZ:(float) z;
@end
顯然,Objective-C 的方法使用 label 區分,而不是類型。利用這種機制,咱們就能夠使用選擇器 selector 來指定一個方法,而不是「成員函數指針」。
成員函數的指針:選擇器
在 Objective-C 中,方法具備包含了括號和標籤的特殊語法。普通的函數不能使用這種語法。在 Objective-C 和 C 語言中,函數指針具備相同的概念,可是對於成員函數指針則有所不一樣。
在 C++ 中,儘管語法很怪異,但確實兼容 C 語言的:成員函數指針也是基於類型的。
C++
class Foo
{
public:
int f(float x) {...}
};
Foo bar
int (Foo::*p_f)(float) = &Foo::f; // Foo::f 函數指針
(bar.*p_f)(1.2345); // 等價於 bar.f(1.2345);
在 Objective-C 中,引入了一個新的類型:指向成員函數的指針被稱爲選擇器 selector。它的類型是 SEL,值經過 @selector 得到。@selector 接受方法名(包括 label)。使用類 NSInvocation 則能夠經過選擇器調用方法。大多時候,工具方法族 performSelector: (繼承自 NSObject)更方便,約束也更大一些。其中最簡單的三個是:
-(id) performSelector:(SEL)aSelector;
-(id) performSelector:(SEL)aSelector withObject:(id)anObjectAsParameter;
-(id) performSelector:(SEL)aSelector withObject:(id)anObjectAsParameter
withObject:(id)anotherObjectAsParameter;
這些方法的返回值同被調用的函數的返回值是同樣的。對於那些參數不是對象的方法,應該使用該類型的包裝類,如 NSNumber 等。NSInvocation 也有相似的功能,而且更爲強大。
按 照前面的說法,咱們沒有任何辦法阻止在一個對象上面調用方法,即使該對象並無實現這個方法。事實上,當消息被接收到以後,方法會被當即觸發。可是,若是 對象並不知道這個方法,一個可被捕獲的異常將被拋除,應用程序並不會被終止。咱們能夠使用 respondsToSelector: 方法來檢查對象是否可被觸發方法。
最後,@selector 的值是在編譯器決定的,所以它並不會減慢程序的運行效率。
Objective-C
@interface Slave : NSObject {}
-(void) readDocumentation:(Document*)document;
// 假設 array[] 是包含 10 個 Slave 對象的數組,
// document 是一個 Document 指針
// 正常的方法調用是
for(i=0 ; i<10 ; ++i)
[array[i] readDocumentation:document];
// 下面使用 performSelector: 示例:
for(i=0 ; i<10 ; ++i)
[array[i] performSelector:@selector(readDocumentation:)
withObject:document];
// 選擇器的類型是 SEL
// 下面代碼並不比前面的高效,由於 @selector() 是在編譯器計算的
SEL methodSelector = @selector(readDocumentation:);
for(i=0 ; i<10 ; ++i)
[slaves[i] performSelector:methodSelectorwithObject:document];
// 對於一個對象「foo」,它的類型是未知的(id)
// 這種測試並非強制的,可是能夠避免沒有 readDocumentation: 方法時出現異常
if ([foo respondsToSelector:@selector(readDocumentation:)])
[foo performSelector:@selector(readDocumentation:) withObject:document];
所以,選擇器可被用做函數參數。通用算法,例如排序,就能夠使用這種技術實現。
嚴 格說來,選擇器並非一個函數指針。它的底層實現是一個 C 字符串,在運行時被註冊爲方法的標識符。當類被加載以後,它的方法會被自動註冊到一個表中,因此 @selector 能夠很好的工做。根據這種實現,咱們就能夠使用 == 來判斷內存地址是否相同,從而得出選擇器是否相同,而無需使用字符串函數。
方 法的真實地址,也就是看作 C 字符串的地址,其實能夠看做是 IMP 類型(咱們之後會有更詳細的說明)。這種類型不多使用,除了在作優化的時候。例如虛調用實際使用選擇器處理,而不是 IMP。等價於 C++ 函數指針的 Objective-C 的概念是選擇器,也不是 IMP。
最後,你應該記得咱們曾經說過 Objective-C 裏面的 self 指針,相似於 C++ 的 this 指針,是做爲每個方法的隱藏參數傳遞的。其實這裏還有第二個隱藏參數,就是 _cmd。_cmd 指的是當前方法。
@implementation Foo
-(void) f:(id)parameter // 等價於 C 函數 void f(id self, SEL _cmd,id parameter)
{
id currentObject = self;
SEL currentMethod = _cmd;
[currentObjectperformSelector:currentMethod
withObject:parameter]; // 遞歸調用
[self performSelector:_cmd withObject:parameter]; // 也是遞歸調用
}
參數的默認值
Objective-C 不容許參數帶有默認值。因此,若是某些參數是可選的,那麼就應當建立多個方法的副本。在構造函數中,這一現象成爲指定構造函數(designated initializer)。
可變參數
Objective-C 容許可變參數,語法同 C 語言同樣,使用 … 做爲最後一個參數。這實際不多用到,便是 Cocoa 裏面不少方法都這麼使用。
匿名參數
C++ 容許匿名參數,它能夠將不使用的參數類型做爲一種佔位符。Objective-C 不容許匿名參數。
原型修飾符(const,static,virtual,」= 0″,friend,throw)
在 C++ 中,還有一些能夠做爲函數原型的修飾符,但在 Objective-C 中,這都是不容許的。如下是這個的列表:
· const:方法不能使用 const 修飾。既然沒有了 const,也就不存在 mutable 了;
· static:用於區別實例方法和類方法的是原型前面的 – 和 +;
· virtual:Objective-C 中全部方法都是 virtual 的,所以沒有必要使用這個修飾符。純虛方法則是聲明爲一個典型的協議 protocol;
· friend:Objective-C 裏面沒有 friend 這個概念;
· throw:在 C++ 中,能夠指定函數會拋除哪些異常,可是 Objective-C 不能這麼作。
給 nil 發送消息
默認狀況下,給 nil 發送消息也是合法的,只不過這個消息被忽略掉了。這種機制能夠避免不少檢查指針是否爲空的狀況。不過,有些編譯器,好比 GCC,也容許你經過編譯參數的設置關閉這一特性。
將消息代理給未知對象
代理 delegation 是 Cocoa 框架中 UI 元素的一個很常見的部分。代理能夠將消息轉發給一個未知的對象。經過代理,一個對象能夠將一些任務交給另外的對象。
// 設置一個輔助對象 assistant
-(void) setAssistant:(id)slave
{
[assistant autorelease];
assistant = [slave retain];
}
// 方法 performHardWork 使用代理
-(void) performHardWork:(id)task
{
// assistant 在編譯期是未知的
// 咱們首先要檢查它是否可以響應消息
if ([assistant respondsToSelector:@selector(performHardWork:)])
[assistant performHardWork:task];
else
[self findAnotherAssistant];
}
轉發:處理未知消息
在 C++ 中,若是對象函數沒有實現,是不能經過編譯的。Objective-C 則不一樣,你能夠向對象發送任何消息。若是在運行時沒法處理,這個消息就被忽略了(同時會拋出一個異常)。除了忽略它,另外的處理辦法是將消息轉發給另外的對象。
當 編譯器被告知對象類型時,它能夠知道對象能夠處理哪些消息,所以就能夠知道消息發出後是否會失敗,也就能夠拋出異常。這也就是爲何消息在運行時被執行, 可是編譯時就能夠發出警告。這並不會引起錯誤,同時還有另外的選擇:調用 forwardInvocation: 方法。這個方法能夠將消息進行轉發。這個方法是 NSObject 的,默認不作任何操做。下面代碼就是一種實現:
-(void) forwardInvocation:(NSInvocation*)anInvocation
{
// 若是該方法被調用,意味着咱們沒法處理這個消息
// 錯誤的選擇器(也就是調用失敗的那個方法名)能夠經過
// 向 anInvocation 對象發送「selector」 得到
if ([anotherObject respondsToSelector:[anInvocation selector]])
[anInvocation invokeWithTarget:anotherObject];
else // 不要忘記調用父類的實現
[super forwardInvocation:anInvocation];
}
便是在最後,這個消息在 forwardInvocation: 中被處理,respondsToSelector: 仍是會返回 NO。事實上,respondsToSelector:並非用來檢查 forwardInvocation: 是否被調用的。
使 用這種轉發機制有時候被認爲是一種很差的習慣,由於它會隱藏掉本應引起錯誤的代碼。事實上,一些很好的設計一樣能夠使用這種機制實現,例如 Cocoa 的 NSUndoManager。它容許一種對異常友好的語法:undo manager 能夠記錄方法調用歷史,雖然它並非那些調用的接收者。
向下轉型
C++ 中,父類指針調用子類的函數時,須要有一個向下轉型的操做(downcasting),使用dynamic_cast 關鍵字。在Objective-C 中,這是沒必要要的。由於你能夠將任何消息發送給任何對象。可是,爲了不編譯器的警告,咱們也能夠使用簡單的轉型操做。Objective-C 中沒有相似 C++ 的專門的向下轉型的操做符,使用 C 風格的轉型語法就能夠了。
// NSMutableString 是 NSString 的子類
// 容許字符串修改的操做
// "appendString:" 僅在 NSMutableString 中實現
NSMutableString* mutableString = ... 初始化可變字符串 ...
NSString* string = mutableString;// 傳給 NSString 指針
// 這些調用都是合法的
[string appendString:@"foo"]; // 有編譯器警告
[(NSMutableString*)string appendString:@"foo"]; // 無警告
[(id)string appendString:@"; // 無警告
Objective-C 也有繼承的概念,可是不能多重繼承。不過,它也有別的途徑實現相似多重繼承的機制,這個咱們後面會講到。
C++ |
Objective-C |
class Foo : public Bar,
protected Wiz
{
}
|
在 C++ 中,一個類能夠繼承自一個或多個類,使用 public、protected 以及 private 修飾符。子類的函數若是要調用父類的版本,須要使用 :: 運算符,例如 Bar::,Wiz:: 等。
在 Objective-C中,一個類只能繼承一個父類,而且只能是 public 的(這和 Java 是一致的)。一樣相似 Java,若是你要在子類中調用父類的函數,須要使用 super。
Java 一樣不容許多重繼承。可是它提供了 interface 來模擬多重繼承。相似的,Objective-C 也有一樣的機制,這就是協議 protocol 和分類 categories。咱們將在後面的內容詳細講述這兩種技術。
虛方法
在 Objective-C 中,全部方法都是虛的,所以,沒有 virtual 關鍵字或其等價物。
虛方法重定義
在 Objective-C 中,你能夠定義一個沒有在 @interface 塊裏面聲明的方法。但這並非一種替代 private 的機制,由於這種方法實際是可以被調用的(回想下,Objective-C 中方法的調用是在運行期決定的)。不過,這確實可以把接口定義變得稍微乾淨了一些。
這 並非一種壞習慣,由於有時你不得不重定義父類的函數。因爲全部方法都是虛的,你無需像 C++ 同樣在聲明中顯式寫明哪些函數是 virtual 的,這種作法就成爲一種隱式的重定義。不少繼承西 NSObject 的方法都是是用這種方法重定義的。例如構造方法 init,析構方法 dealloc,view 類的 drawRect: 等等。這樣的話,接口就變得更簡潔,更易於閱讀。很差之處就是,你不能知道究竟哪些方法被重定義了。
純虛方法則是使用正式協議 formal protocols 來實現。
虛繼承
Objective-C 中不容許多重繼承,所以也就沒有虛繼承的問題。
Java 和 C# 使用接口 interface 的概念來彌補多重繼承的不足。Objective-C 也使用了相似的機制,成爲協議 protocol。在 C++ 中,這種概念是使用抽象類。協議並非真正的類:它只能聲明方法,不能添加數據。有兩種類型的協議:正式的 formal 和非正式的 informal。
正式協議
正式協議的方法,全部實現這個協議的類都必須實現。這就是一種驗證,也就是說,只要這個類說實現這個協議,那麼它確定能夠處理協議中規定的方法。一個類能夠實現任意多個協議。
C++
class MouseListener
{
public:
virtual bool mousePressed(void) = 0; // 純虛方法
virtual bool mouseClicked(void) = 0; // 純虛方法
};
class KeyboardListener
{
public:
virtual bool keyPressed(void) = 0; // 純虛方法
};
class Foo : public MouseListener, public KeyboardListener {...}
// Foo 必須實現 mousePressed, mouseClicked 和 keyPressed
// 而後 Foo 就能夠做爲鼠標和鍵盤的事件監聽器
Objective-C
@protocol MouseListener
-(BOOL) mousePressed;
-(BOOL) mouseClicked;
@end
@protocol KeyboardListener
-(BOOL) keyPressed;
@end
@interface Foo : NSObject <MouseListener, KeyboardListener>
{
...
}
@end
// Foo 必須實現 mousePressed, mouseClicked 和 keyPressed
// 而後 Foo 就能夠做爲鼠標和鍵盤的事件監聽器
C++ 中,協議能夠由抽象類和純虛函數實現。C++ 的抽象類要比 Objective-C 的協議強大的多,由於抽象類能夠帶有數據。
Objective-C 中,協議是一個特殊的概念,使用尖括號 <…> 代表。注意,尖括號在 Objective-C 中不是模板的意思,Objective-C 中沒有相似 C++ 模板的概念。
一 個類也能夠不通過協議聲明,直接實現協議規定的方法。此時,conformsToProtocol: 方法依然返回 NO。出於性能考慮,conformsToProtocol: 方法只檢查類接口的聲明,不會一個方法一個方法的對比着檢查。conformsToProtocol: 的返回值並不會做爲是否調用方法的依據。下面是這個方法的原型:
-(BOOL) conformsToProtocol:(Protocol*)protocol
// Protocol 對象能夠由 @protocol(協議名) 返回
實現了正式協議的對象的類型同協議自己是兼容的。這一機制能夠做爲協議的篩選操做。例如:
// 下面方法是 Cocoa 提供的標準方法
// 方法參數能夠是任意類型 id,可是必須兼容 NSDraggingInfo 協議
-(NSDragOperation) draggingEntered:(id )sender;
可選方法
有時咱們須要這麼一種機制:咱們的類須要實現一部分協議中規定的方法,而不是整個協議。例如在 Cocoa 中,代理的概念被普遍使用:一個類能夠給定一個輔助類,由這個輔助類去完成部分任務。
一 種實現是將一個協議分割成不少小的協議,而後這個類去實現一個協議的集合。不過這並不具備可操做性。更好的解決方案是使用非正式協議。在 Objective-C 1.0 中就有非正式協議了,Objective-C 2.0 則提出了新的關鍵字 @optional 和 @required,用以區分可選方法和必須方法。
@protocol Slave
@required // 必須部分
-(void) makeCoffee;
-(void) duplicateDocument:(Document*)document count:(int)count;
@optional // 可選部分
-(void) sweep;
@required // 又是一個必須部分
-(void) bringCoffee;
@end
非正式協議
非正式協議並非真正的協議,它對代碼沒有約束力。非正式協議容許開發者將一些方法進行歸類,從而能夠更好的組織代碼。因此,非正式協議並非協議的寬鬆版本。另一個類似的概念就是分類。
讓 咱們想象一個文檔管理的服務。假設有綠色、藍色和紅色三種文檔,一個類只能處理藍色文檔,而 Slave 類使用三個協議 manageBlueDocuments, manageGreenDocuments 和 manageRedDocuments。Slave 能夠加入一個分類 DocumentsManaging,用來聲明它可以完成的任務。分類名在小括號中被指定:
@interface Slave (DocumentsManaging)
-(void) manageBlueDocuments:(BlueDocument*)document;
-(void) trashBlueDocuments:(BlueDocument*)document;
@end
任何類均可以加入 DocumentsManaging 分類,加入相關的處理方法:
@interface PremiumSlave (DocumentsManaging)
-(void) manageBlueDocuments:(BlueDocument*)document;
-(void) manageRedDocuments:(RedDocument*)document;
@end
另外一個開發者就能夠瀏覽源代碼,找到了 DocumentsManaging 分類。若是他以爲這個分類中有些方法可能對本身,就會檢查究竟哪些可以使用。即使他不查看源代碼,也能夠在運行時指定:
if([mySlave respondsToSelector:@selector(manageBlueDocuments:)])
[mySlave manageBlueDocuments:document];
嚴格說來,除了原型部分,非正式協議對編譯器沒有什麼意義,由於它並不能約束代碼。不過,非正式協議能夠造成很好的自解釋性代碼,讓 API 更具可讀性。
Protocol 對象
運行時,協議就像是類對象,其類型是 Protocol*。例如,conformsToProtocol: 方法就須要接受一個 Protocol* 類型的參數。@protocol 關鍵字不只用於聲明協議,還能夠用於根據協議名返回 Protocol* 對象。
Protocol* myProtocol = @protocol(協議名)
遠程對象的消息傳遞
由 於 Objective-C 的動態機制,遠程對象之間的消息傳遞變得很簡單。所謂遠程對象,是指兩個或多個處於不一樣程序,甚至不一樣機器,可是能夠經過代理完成同一任務,或者交換信息 的對象。正式協議就是一種能夠確保對象提供了這種服務的有效手段。正式協議還提供了不少額外的關鍵字,能夠更好的說明各類參數。這些關鍵字分別是 in, out, inout, bycopy, byref 和 oneway。這些關鍵字僅對遠程對象有效,而且僅能夠在協議中使用。出了協議,它們就不被認爲是關鍵字。這些關鍵字被插入到在協議中聲明的方法原型之 中,提供它們所修飾的參數的額外信息。它們能夠告知,哪些是輸入參數,哪些是輸出參數,哪些使用複製傳值,哪些使用引用傳值,方法是不是同步的等等。如下 是詳細說明:
· in:參數是輸入參數;
· out:參數是輸出參數;
· inout:參數便是輸入參數,又是輸出參數;
· bycopy:複製傳值;
· byref:引用傳值;
· oneway:方法是異步的,也就是不會當即返回,所以它的返回值必須是 void。
例如,下面就是一個返回對象的異步方法:
-(oneway void) giveMeAnObjectWhenAvailable:(bycopy out id *)anObject;
默 認狀況下,參數都被認爲是 inout 的。若是參數由 const 修飾,則被當作 in 參數。爲參數選定是 in 仍是 out,能夠做爲一種優化手段。參數默認都是傳引用的,方法都是同步的(也就是不加 oneway)。對於傳值的參數,也就是非指針類型的,out 和 inout 都是沒有意義的,只有 in 是正確的選擇。
建立類的分類 categories,能夠將一個很大的類分割成若干小部分。每一個分類都是類的一部分,一個類能夠使用任意多個分類,但都不能夠添加實例數據。分類的好處是:
· 對於精益求精的開發者,分類提供了一種劃分方法的機制。對於一個很大的類,它能夠將其劃分紅不一樣的角色;
· 分類容許分開編譯,也就是說,同一個類也能夠進行多人的分工合做;
· 若是把分類的聲明放在實現文件(.m)中,那麼這個分類就只在文件做用域中可見(雖然這並無調用上的限制,若是你知道方法原型,依然能夠調用)。這樣的分類能夠取一個合適的名字,好比 FooPrivateAPI;
· 一個類能夠在不一樣程序中有不一樣的擴展,而不須要丟棄通用代碼。全部的類均可以被擴展,甚至是 Cocoa 中的類。
最後一點尤爲重要。不少開發人員都但願標準類可以提供一些對他們而言頗有用的方法。這並非一個很困難的問題,使用繼承便可實現。可是,在單繼承的環境下,這會形成出現不少的子類。僅僅爲了一個方法就去繼承顯得有些得不償失。分類就能夠很好的解決這個問題:
C++ |
Objective-C |
class MyString : public string
{
public:
// 統計元音的數目
int vowelCount(void);
};
int MyString::vowelCount(void)
{
...
}
|
@interface NSString (VowelsCounting)
// 注意並無使用 {}
-(int) vowelCount; // 統計元音的數目 @end
@implementation NSString (VowelsCounting)
-(int) vowelCount
{
...
}
@end
|
在 C++ 中,這是一個全新的類,能夠自由使用。
在 Objective-C 中,NSString 是 Cocoa 框架的一個標準類。它是使用分類機制進行的擴展,只能在當前程序中使用。注意此時並無新增長類。每個 NSString 對象均可以從這個擴展得到統計元音數目的能力,甚至常量字符串也能夠。同時注意,分類不能增長實例數據,所以沒有花括號塊。
分類也能夠使匿名的,更適合於 private 的實現:
@interface NSString ()
// 注意並無使用 {}
-(int) myPrivateMethod;
@end
@implementation NSString ()
-(int) myPrivateMethod
{
...
}
@end
混合使用協議、分類和子類的惟一限制在於,你不能同時聲明子類和分類。不過,你能夠使用兩步來繞過這一限制:
@interface Foo1 : SuperClass //ok
@end
@interface Foo2 (Category) //ok
@end
// 下面代碼會有編譯錯誤
@interface Foo3 (Category) : SuperClass
@end
// 一種解決方案
@interface Foo3 : SuperClass // 第一步
@end
@interface Foo3 (Category) // 第二步
@end
類的實例化位致使兩個問題:構造函數、析構函數和賦值運算符如何實現,以及如何分配內存。
在 C++ 中,變量默認是「自動的」:除非被聲明爲 static,不然變量僅在本身的定義塊中有意義。動態分配的內存能夠一直使用,直到調用了 free() 或者 delete。C++ 中,全部對象都遵循這一規則。
然而在Objective-C 中,全部對象都是動態分配的。其實這也是符合邏輯的,由於 C++ 更加 static,而Objective-C 則更加動態。除非可以在運行時動態分配內存,不然 Objective-C 實現不了這麼多動態的特性。
分配 allocation 和初始化 initialization 的區別
在 C++ 中,內存分配和對象初始化都是在構造函數中完成的。在 Objective-C 中,這是兩個不一樣的函數。
內存分配由類方法 alloc 完成,此時將初始化全部的實例數據。實例數據將被初始化爲 0,除了一個名爲 isa 的 NSObject 的指針。這個指針將在運行時指向對象的實際類型。實例數據根據傳入的參數初始化爲某一特定的值,這一過程將在一個實例方法 instance method 中完成。這個方法一般命名爲 init。所以,構造過程被明確地分爲兩步:內存分配和初始化。alloc 消息被髮送給類,而 init 消息則被髮送給由 alloc 建立出來的新的對象。初始化過程不是可選的,alloc 以後應該跟着 init,以後,父類的 init 也會被調用,直到 NSObject 的 init 方法。這一方法完成了不少重要的工做。
在 C++ 中,構造函數的名字是規定好的,必須與類名一致。在 Objective-C 中,初始化方法與普通方法沒有什麼區別。你能夠用任何名字,只不過一般都是選用 init 這個名字。然而,咱們仍是強烈建議,初始化方法名字必定要用 init 或者 init 開頭的字符串。
使用 alloc 和 init
調 用 alloc 以後將返回一個新的對象,而且應該給這個對象發送一個 init 消息。init 調用以後也會返回一個對象。一般,這就是初始化完成的對象。有時候,若是使用單例模式,init 可能會返回另外的對象(單例模式要求始終返回同一對象)。所以,init 的返回值不該該被忽略。一般,alloc 和 init 都會在一行上。
C++
Foo* foo = new Foo;
Objective-C
Foo* foo1 = [Foo alloc];
[foo1 init]; // 這是很差的行爲:應該使用 init 的返回值
Foo* foo2 = [Foo alloc];
foo2 = [foo2 init]; // 正確,不過看上去很囉嗦
Foo* foo3 = [[Foo alloc] init]; // 正確,這纔是一般的作法
爲檢查內存分配是否成功,C++ 能夠判斷 new 返回的指針是不是 0(若是使用的是 new(nothrow) 運算符)。在 Objective-C 中,檢查返回值是不是 nil 就已經足夠了。
初始化方法的正確示例代碼
一個正確的初始化方法應該有以下特色:
· 名字以init 開始;
· 返回可以使用的對象;
· 調用父類的 init 方法,直到 NSObject 的init 方法被調用;
· 保存[super init...] 的返回值;
· 處理構造期間出現的任何錯誤,不管是本身的仍是父類的。
下面是一些代碼:
C++
class Point2D
{
public:
Point2D(int x, int y);
private:
int x;
int y;
};
Point2D::Point2D(int anX, int anY) {x = anX; y = anY;}
...
Point2D p1(3,4);
Point2D* p2 = new Point2D(5, 6);
Objective-C
@interface Point2D : NSObject
{
int x;
int y;
}
// 注意,在 Objective-C 中,id 相似於 void*
// (id) 就是對象的「通常」類型
-(id) initWithX:(int)anX andY:(int)anY;
@end
@implementation Point2D
-(id) initWithX:(int)anX andY:(int)anY
{
// 調用父類的初始化方法
if (!(self = [super init])) // 若是父類是 NSObject,必須進行 init 操做
return nil; // 若是父類 init 失敗,返回 nil
// 父類調用成功,進行本身的初始化操做
self->x = anX;
self->y = anY;
return self; // 返回指向本身的指針
}
@end
...
Point2D* p1 = [[Point2D alloc] initWithX:3 andY:4];
self = [super init...]
在 上一篇提到的代碼中,最難以想象的可能就是這句 self = [super init...]。回想一下,self 是每一個方法的一個隱藏參數,指向當前對象。所以,這是一個局部變量。那麼,爲何咱們要改變一個局部變量的值呢?事實上,self 必需要改變。咱們將在下面解釋爲何要這樣作。
[super init] 實際上返回不一樣於當前對象的另一個對象。單例模式就是這樣一種狀況。然而, 有一個 API 能夠用一個對象替換新分配的對象。Core Data(Apple 提供的 Cocoa 裏面的一個 API)就是用了這種 API,對實例數據作一些特殊的操做,從而讓這些數據可以和數據庫的字段關聯起來。當繼承 NSManagedObject 類的時候,就須要仔細對待這種替換。在這種情形下,self 就要指向兩個對象:一個是 alloc 返回的對象,一個是 [super init] 返回的對象。修改 self 的值對代碼有必定的影響:每次訪問實例數據的時候都是隱式的。正以下面的代碼所示:
@interface B : A
{
int i;
}
@implementation B
-(id) init
{
// 此時,self 指向 alloc 返回的值
// 假設 A 進行了替換操做,返回一個不一樣的 self
id newSelf = [super init];
NSLog(@"%d", i); // 輸出 self->i 的值
self = newSelf; // 有人會認爲 i 沒有變化
NSLog(@"%d", i); // 事實上,此時的 self->i, 實際是 newSelf->i,
// 和以前的值可能不同了
return self;
}
...
B* b = [[B alloc] init];
self = [super init] 簡潔明瞭,也沒必要擔憂之後會引入 bug。然而,咱們應該注意舊的 self 指向的對象的命運:它必須被釋放。第一規則很簡單:誰替換 self 指針,誰就要負責處理舊的 self 指針。在這裏,也就是 [super init] 負責完成這一操做。例如,若是你建立 NSManagedObject 子類(這個類會執行替換操做),你就沒必要擔憂舊的 self 指針。事實上,NSManagedObject 的開發者必須考慮這種處理。所以,若是你要建立一個執行替換操做的類,你必須知道如何在初始化過程當中釋放舊有對象。這種操做同錯誤處理很相似:若是由於非 法參數、不可訪問的資源形成構造失敗,咱們要如何處理?
初始化錯誤
初始化出錯可能發生在三個地方:
1. 調用 [super init...] 以前:若是構造函數參數非法,那麼初始化應該當即中止;
2. 調用 [super init...] 期間:若是父類調用失敗,那麼當前的初始化操做也應該中止;
3. 調用 [super init...] 以後:例如資源分配失敗等。
在上面每一種情形中,只要失敗,就應該返回 nil;相應的處理應該由發生錯誤的對象去完成。這裏,咱們主要關心的是1, 3狀況。要釋放當前對象,咱們調用 [self release] 便可。
在調用 dealloc 以後,對象的析構纔算完成。所以,dealloc 的實現必須同初始化方法兼容。事實上,alloc 將全部的實例數據初始化成 0 是至關有用的。
@interface A : NSObject {
unsigned int n;
}
-(id) initWithN:(unsigned int)value;
@implementation A
-(id) initWithN:(unsigned int)value
{
// 第一種狀況:參數合法嗎?
if (value == 0) // 咱們須要一個正值
{
[self release];
return nil;
}
// 第二種狀況:父類調用成功嗎?
if (!(self = [super init])) // 便是 self 被替換,它也是父類
return nil; // 錯誤發生時,誰負責釋放 self?
// 第三種狀況:初始化可以完成嗎?
n = (int)log(value);
void* p = malloc(n); // 嘗試分配資源
if (!p) // 若是分配失敗,咱們但願發生錯誤
{
[self release];
return nil;
}
}
將構造過程合併爲 alloc+init
有 時候,alloc 和 init 被分割成兩個部分顯得很羅嗦。幸運的是,咱們也能夠將其合併在一塊兒。這主要牽扯到 Objective-C 的內存管理機制。簡單來講,做爲一個構造函數,它的名字必須以類名開頭,其行爲相似 init,但要本身實現 alloc。然而,這個對象須要註冊到 autorelease 池中,除非發送 retain 消息,不然其生命週期是有限制的。如下便是示例代碼:
// 囉嗦的寫法
NSNumber* tmp1 = [[NSNumber alloc] initWithFloat:0.0f];
...
[tmp1 release];
// 簡潔一些
NSNumber* tmp2 = [NSNumber numberWithFloat:0.0f];
...
// 無需調用 release
默認構造函數:指定初始化函數
在 Objective-C 中,默認構造函數沒有實在的意義,由於全部對象都是動態分配內存,也就是說,構造函數都是肯定的。可是,一個經常使用的構造函數確實能夠精簡代碼。事實上,一個正確的初始化過程一般相似於:
if (!(self = [super init])) // "init" 或其餘父類恰當的函數
return nil;
// 父類初始化成功,繼續其餘操做……
return self;
剪貼複製代碼是一個不良習慣。好的作法是,將共同代碼放到一個獨立的函數中,一般稱爲「指定初始化函數」。一般這種指定初始化函數會包含不少參數,由於 Objective-C 不容許參數有默認值。
-(id) initWithX:(int)x
{
return [self initWithX:x andY:0 andZ:0];
}
-(id) initWithX:(int)x andY:(int)y
{
return [self initWithX:x andY:y andZ:0];
}
// 指定初始化函數
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
{
if (!(self = [super init]))
return nil;
self->x = x;
self->y = y;
self->z = z;
return self;
}
若是指定初始化函數沒有最大數量的參數,那基本上就沒什麼用處:
// 如下代碼就有不少重複部分
-(id) initWithX:(int)x // 指定初始化函數
{
if (!(self = [super init]))
return nil;
self->x = x;
return self;
}
-(id) initWithX:(int)x andY:(int)y
{
if (![self initWithX:x])
return nil;
self->y = y;
return self;
}
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
{
if (![self initWithX:x])
return nil;
self->y = y;
self->z = z;
return self;
}
初始化列表和實例數據的默認值
Objective-C 中不存在 C++ 構造函數的初始化列表的概念。然而,不一樣於 C++,Objective-C的 alloc 會將全部實例數據初始化成 0,所以指針也會被初始化成 nil。C++ 中,對象屬性不一樣於指針,可是在 Objective-C 中,全部對象都被當作指針處理。
虛構造函數
Objective-C 中存在虛構造函數。咱們將在後面的章節中詳細講訴這個問題。
類構造函數
在 Objective-C 中,類自己就是對象,所以它也有本身的構造函數,而且也可以被重定義。它顯然是一個類函數,繼承自 NSObject,其原型是 +(void) initialize;。
第 一次使用這個類或其子類的時候,這個函數將被自動調用。但這並不意味着,對於指定的類,這個函數只被調用一次。事實上,若是子類沒有定義 +(void) initialize;,那麼 Objective-C 將調用其父類的 +(void) initialize;。
在 C++ 中,析構函數同構造函數同樣,是一個特殊的函數。在 Objective-C 中,析構函數也是一個普通的實例函數,叫作 dealloc。C++ 中,當對象被釋放時,析構函數將自動調用;Objective-C 也是相似的,可是釋放對象的方式有所不一樣。
析構函數永遠不該該被顯式調用。在 C++ 中存在這麼一種狀況:開發者本身在析構時管理內存池。可是在 Objective-C 中沒有這種限制。你能夠在 Cocoa 中使用自定義的內存區域,可是這並不會影響日常的內存的分配、釋放機制。
C++
class Point2D
{
public:
~Point2D();
};
Point2D::~Point2D() {}
Objective-C
@interface Point2D : NSObject
-(void) dealloc; // 該方法能夠被重定義
@end
@implementation Point2D
// 在這個例子中,重定義並不須要
-(void) dealloc
{
[super dealloc]; // 不要忘記調用父類代碼
}
@end
典型 cloning, copy, copyWithZone:, NSCopyObject()
在 C++ 中,定義複製運算符和相關的操做是很重要的。在 Objective-C 中,運算法是不容許重定義的,所能作的就是要求提供一個正確的複製函數。
克隆操做在 Cocoa 中要求使用 NSCopying 協議實現。該協議要求一個實現函數:
-(id) copyWithZone:(NSZone*)zone;
這個函數的參數是一個內存區,用於指明須要複製那一塊內存。Cocoa 容許使用不一樣的自定義區塊。大多數時候默認的區塊就已經足夠,不必每次都單獨指定。幸運的是,NSObject 有一個函數
-(id) copy;
封裝了 copyWithZone:,直接使用默認的區塊做爲參數。但它實際至關於 NSCopying 所要求的函數。另外,NSCopyObject() 提供一個不一樣的實現,更簡單但一樣也須要注意。下面的代碼沒有考慮 NSCopyObject():
// 若是父類沒有實現 copyWithZone:,而且沒有使用 NSCopyObject()
-(id) copyWithZone:(NSZone*)zone
{
// 建立對象
Foo* clone = [[Foo allocWithZone:zone] init];
// 實例數據必須手動複製
clone->integer = self->integer; // "integer" 是 int 類型的
// 使用子對象相似的機制複製
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 有些子對象不能複製,可是能夠共享
clone->objectToShare = [self->objectToShare retain];
// 若是有設置方法,也能夠使用
[clone setObject:self->object];
return clone;
}
注意,咱們使用的是 allocWithZone: 而不是 alloc。alloc 實際上封裝了allocWithZone:,它傳進的是默認的 zone。可是,咱們應該注意父類的 copyWithZone: 的實現。
// 父類實現了 copyWithZone:,而且沒有使用 NSCopyObject()
-(id) copyWithZone:(NSZone*)zone
{
Foo* clone = [super copyWithZone:zone]; // 建立新的對象
// 必須複製當前子類的實例數據
clone->integer = self->integer; // "integer" 是 int 類型的
// 使用子對象相似的機制複製
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 有些子對象不能複製,可是能夠共享
clone->objectToShare = [self->objectToShare retain];
// 若是有設置方法,也能夠使用
[clone setObject:self->object];
return clone;
}
NSCopyObject()
NSObject 事實上並無實現 NSCopying 協議(注意函數的原型不一樣),所以咱們不能簡單地使用 [super copy...] 這樣的調用,而是相似 [[... alloc] init] 這種標準調用。NSCopyObject() 容許更簡單的代碼,可是須要注意指針變量(包括對象)。這個函數建立一個對象的二進制格式的拷貝,其原型是:
// extraBytes 一般是 0,能夠用於索引實例數據的空間
id NSCopyObject(id anObject, unsigned int extraBytes, NSZone *zone)
二進制複製能夠複製非指針對象,可是對於指針對象,須要時刻記住它會建立一個指針所指向的數據的新的引用。一般的作法是在複製完以後重置指針。
// 若是父類沒有實現 copyWithZone:
-(id) copyWithZone:(NSZone*)zone
{
Foo* clone = NSCopyObject(self, 0, zone); // 以二進制形式複製數據
// clone->integer = self->integer; // 不須要,由於二進制複製已經實現了
// 須要複製的對象成員必須執行真正的複製
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 共享子對象必須註冊新的引用
[clone->objectToShare retain];
// 設置函數看上去應該調用 clone->object. 但其實是不正確的,
// 由於這是指針值的二進制複製。
// 所以在使用 mutator 前必須重置指針
clone->object = nil;
[clone setObject:self->object];
return clone;
}
// 若是父類實現了 copyWithZone:
-(id) copyWithZone:(NSZone*)zone
{
Foo* clone = [super copyWithZone:zone];
// 父類實現 NSCopyObject() 了嗎?
// 這對於知道如何繼續下面的代碼很重要
clone->integer = self->integer; // 僅在 NSCopyObject() 沒有使用時調用
// 若是有疑問,一個須要複製的子對象必須真正的複製
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 無論 NSCopyObject() 是否實現,新的引用必須添加
clone->objectToShare = [self->objectToShare retain];
clone->object = nil; // 若是有疑問,最好重置
[clone setObject:self->object];
return clone;
}
Dummy-cloning,mutability, mutableCopy and mutableCopyWithZone:
若是須要複製不可改變對象,一個基本的優化是僞裝它被複制了,其實是返回一個原始對象的引用。從這點上能夠區分可變對象與不可變對象。
不可變對象的實例數據不能被修改,只有初始化過程可以給一個合法值。在這種狀況下,使用「僞克隆」返回一個原始對象的引用就能夠了,由於它自己和它的複製品都不可以被修改。此時,copyWithZone: 的一個比較好的實現是:
-(id) copyWithZone:(NSZone*)zone
{
// 返回自身,增長一個引用
return [self retain];
}
retain 操做意味着將其引用加 1。咱們須要這麼作,由於當原始對象被刪除時,咱們還會持有一個複製品的引用。
「僞 克隆」並非可有可無的優化。建立一個新的對象須要進行內存分配,相對來講這是一個比較耗時的操做,若是可能的話應該注意避免這種狀況。這就是爲何須要 區別可變對象和不可變對象。由於不可變對象能夠在複製操做上作文章。咱們能夠首先建立一個不可變類,而後再繼承這個類增長可變操做。Cocoa 中不少類都是這麼實現的,好比 NSMutableString 是 NSString 的子類;NSMutableArray是 NSArray 的子類;NSMutableData 是 NSData 的子類。
然而根據咱們上面描述的內容,彷佛沒法從不可變對象安全地獲取一個徹底的克隆,由於不可變對象只能「僞克隆」本身。這個限制大大下降了不可變對象的可用性,由於它們從「真實的世界」隔離了出來。
除了 NSCopy 協議,還有一個另外的 NSMutableCopying 協議,其原型以下:
-(id) mutableCopyWithZone:(NSZone*)zone;
mutableCopyWithZone: 必須返回一個可變的克隆,其修改不能影響到原始對象。相似 NSObject 的 copy 函數,也有一個mutableCopy 函數使用默認區塊封裝了這個操做。mutableCopyWithZone:的實現相似前面的 copyWithZone: 的代碼:
// 若是父類沒有實現 mutableCopyWithZone:
-(id) mutableCopyWithZone:(NSZone*)zone
{
Foo* clone = [[Foo allocWithZone:zone] init]; // 或者可用 NSCopyObject()
clone->integer = self->integer;
// 相似 copyWithZone:,有些子對象須要複製,有些須要增長引用
// 可變子對象使用 mutableCopyWithZone: 克隆
//...
return clone;
}
不要忘記咱們能夠使用父類的 mutableCopyWithZone:
// 若是父類實現了 mutableCopyWithZone:
-(id) mutableCopyWithZone:(NSZone*)zone
{
Foo* clone = [super mutableCopyWithZone:zone];
//...
return clone;
}
Objective-C 中沒有 new 和 delete 這兩個關鍵字(new 能夠看做是一個函數,也就是 alloc+init)。它們實際是被 alloc 和 release 所取代。
內 存管理是一個語言很重要的部分。在 C 和 C++ 中,內存塊有一次分配,而且要有一次釋放。這塊內存區能夠被任意多個指針指向,但只能被其中一個指針釋放。Objective-C 則使用引用計數。對象知道本身被引用了多少次,這就像狗和狗鏈的關係。若是對象是一條狗,每一個人均可以拿狗鏈拴住它。若是有人不想再管它了,只要丟掉他手 中的狗鏈就能夠了。只要還有一條狗鏈,狗就必須在那裏;可是隻要全部的狗鏈都沒有了,那麼此時狗就自由了。換作技術上的術語,新建立的對象的引用計數器被 設置爲 1。若是代碼須要引用這個對象,就能夠發送一個 retain 消息,讓計數器加 1。當代碼不須要的時候則發送一個release 消息,讓計數器減 1。
對象能夠接收任意多的 retain 和 release 消息,只要計數器的值是正的。當計數器成 0 時,析構函數 dealloc 將被自動調用。此時再次發送 release 給這個對象就是非法的了,將引起一個內存錯誤。
這種技術並不一樣於 C++ STL 的 auto_ptr。Boost 庫提供了一個相似的引用計數器,稱爲 shared_ptr,但這並非標準庫的一部分。
明白了內存管理機制並不能很好的使用它。這一節的目的就是給出一些使用規則。這裏先不解釋 autorelease 關鍵字,由於它比較難理解。
基本規則是,全部使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增長計數器的對象都要用 [auto]release 釋放。事實上,有三種方法能夠增長引用計數器,也就意味着僅僅有有限種狀況下才要使用 release 釋放對象:
· 使用alloc 顯式實例化對象;
· 使用copy[WithZone:] 或者mutableCopy[WithZone:] 複製對象(無論這種克隆是否是僞克隆);
· 使用retain。
記住,默認狀況下,給 nil 發送消息(例如 release)是合法的,不會引發任何後果。
不同的 autorelease
前 面咱們強調了,全部使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增長計數器的對象都要用[auto]release 釋放。事實上,這條規則不只僅適用於alloc、retain 和 release。有些函數雖然不是構造函數,但也用於建立對象,例如 C++ 的二元加運算符(obj3 operator+(obj1, obj2))。在 C++ 中,返回值能夠在棧上,以便在離開做用域的時候能夠自動銷燬。但在 Objective-C 中不存在這種對象。函數使用 alloc 分配的對象,直到將其返回棧以前不能釋放。下面的代碼將解釋這種狀況:
// 第一個例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
return result;
}
// 錯誤!這個函數使用了 alloc,因此它將對象的引用計數器加 1。
// 根據前面的說法,它應該被銷燬。
// 可是這將引發內存泄露:
[calculator add:[calculator add:p1 and:p2] and:p3];
// 第一個算式是匿名的,沒有辦法 release。因此引發內存泄露。
// 第二個例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
return [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
}
// 錯誤!這段代碼實際上和上面的同樣,
// 不一樣之處在於僅僅減小了一箇中間變量。
// 第三個例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
[result release];
return result;
}
// 錯誤!顯然,這裏僅僅是在對象建立出來以後當即銷燬了。
這 個問題看起來很棘手。若是沒有 autorelease 的確如此。簡單地說,給一個對象發送 autorelease 消息意味着告訴它,在「一段時間以後」銷燬。可是這裏的「一段時間以後」並不意味着「任什麼時候間」。咱們將在後面的章節中詳細講述這個問題。如今,咱們有了 上面這個問題的一種解決方案:
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
[result autorelease];
return result; // 更簡短的代碼是:return [result autorelease];
}
// 正確!result 將在之後自動釋放
autorelease 池
上 一節中咱們瞭解到 autorelease 的種種神奇之處:它可以在合適的時候自動釋放分配的內存。可是如何才能讓便以其之道何時合適呢?這種狀況下,垃圾收集器是最好的選擇。下面咱們將着重 講解垃圾收集器的工做原理。不過,爲了瞭解垃圾收集器,就不得不深刻了解 autorelease 的機制。因此咱們要從這裏開始。當對象收到 autorelease 消息的時候,它會被註冊到一個「autorelease 池」。當這個池被銷燬時,其中的對象也就被實際的銷燬。因此,如今的問題是,這個池如何管理?
答案是豐富多彩的:若是你使用 Cocoa 開發 GUI 界面,基本不須要作什麼事情;不然的話,你應該本身建立和銷燬這個池。
擁 有圖形界面的應用程序都有一個事件循環。這個循環將等待用戶動做,使應用程序響應動做,而後繼續等待下一個動做。當你使用 Cocoa 建立 GUI 程序時,這個 autorelease 池在事件循環的一次循環開始時被自動建立,而後在循環結束時自動銷燬。這是合乎邏輯的:通常的,一個用戶動做都會觸發一系列任務,臨時變量的建立和銷燬一 般不會影響到下一個事件。若是必需要有可持久化的數據,那麼你就要手動地使用 retain 消息。
另 一方面,若是沒有 GUI,你必須本身創建 autorelease 池。當對象收到 autorelease 消息時,它可以找到最近的 autorelease 池。當池能夠被清空時,你能夠對這個池使用 release 消息。通常的,命令行界面的 Cocoa 程序都會有以下的代碼:
int main(int argc, char* argv[])
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
//...
[pool release];
return 0;
}
注 意在 Mac OS X 10.5 的 NSAutoreleasePool 類新增長了一個 drain 方法。這個方法等價於:當垃圾收集器可用時作 release 操做;不然則觸發運行垃圾收集。這對編寫在兩種狀況下都適用的代碼時是頗有用的。注意,這裏其實是說,如今有兩種環境:引用計數和垃圾回收。Mac OS 的新版本都會支持垃圾收集器,可是 iOS 卻不支持。在引用計數環境下,NSAutoreleasePool 的 release 方法會給池中的全部對象發送 release 消息,若是對象註冊了屢次,就會屢次給它發 release。drain 和 release 在應用計數環境下是等價的。在垃圾收集的環境下,release 不作任何事情,drain 則會觸發垃圾收集。
使用多個 autorelease 池
在 一個程序中使用多個 autorelease 池也是能夠的。對象收到 autorelease 消息時會註冊到最近的池。所以,若是一個函數須要建立並使用很大數量臨時對象,爲了提升性能,能夠建立一個局部的 autorelease 池。這種狀況下,這些臨時變量就能夠及時的被銷燬,從而在函數返回時就將內存釋放出來。
autorelease 的注意點
使用 autorelease 可能會有一些誤用狀況,須要咱們特別注意。
· 首先,非必要地發送多個 autorelease 相似發送多個 release 消息,在內存池清空時會引發內存錯誤;
· 其 次,即便 release 能夠由 autorelease 替代,也不能濫用 autorelease。由於 autorelease 要比正常的 release 消耗資源更多。另外,沒必要要的推遲 release 操做無疑會致使佔用大量內存,容易引發內存泄露。
autorelease 和 retain
多虧了 autorelease,方法纔可以建立可以自動釋放的對象。可是,長時間持有對象是一種很常見的需求。在這種情形下,咱們能夠向對象發送 retain 消息,而後在後面手動的 release。這樣,這個對象實際上能夠從兩個角度去看待:
· 從函數開發者的角度,對象的建立和釋放都是有計劃的;
· 從函數調用者的角度,使用了 retain 以後,對象的生命期變長了(使用 retain 將使其引用計數器加 1),爲了讓對象可以正確地被釋放,調用者必須負責將計數器再減 1。
我 們來理解一下這句話。對於一個函數的開發者,若是他不使用 autorelease,那麼,他使用 alloc 建立了一個對象並返回出去,那麼,他須要負責在合適的時候對這個對象作 release 操做。也就是說,從函數開發者的角度,這個對象的計數器始終是 1,一次 release 是可以被正常釋放的。此時,函數調用者卻使用 retain 將計數器加 1,可是開發者不知道對象的計數器已經變成 2 了,一次 release 不能釋放對象。因此,調用者必須注意維護計數器,要調用一次 release 將其恢復至 1。
Convenience constructor,virtual constructor
將 構造對象的過程分紅 alloc 和 init 兩個階段,有時候顯得很羅嗦。好在咱們有一個 convenience constructor 的概念。這種構造函數應該使用類名作前綴,其行爲相似 init,同時要實現 alloc。可是,它的返回對象須要註冊到一個內部的 autorelease 池,若是沒有給它發送 retain 消息時,這個對象始終是一個臨時對象。例如:
// 囉嗦的寫法
NSNumber* zero_a = [[NSNumber alloc] initWithFloat:0.0f];
...
[zero_a release];
...
// 簡潔一些的
NSNumber* zero_b = [NSNumber numberWithFloat:0.0f];
...
// 不須要 release
根 據咱們前面對內存管理的介紹,這種構造函數的實現是基於 autorelease 的。可是其底層代碼並不那麼簡單,由於這涉及到對 self 的正確使用。事實上,這種構造函數都是類方法,因此 self 指向的是 Class 類型的對象,就是元類類型的。在初始化方法,也就是一個實例方法中,self 指向的是這個類的對象的實例,也就是一個「普通的」對象。
編寫錯誤的這種構造函數是很容易的。例如,咱們要建立一個 Vehicle 類,包含一個 color 數據,編寫以下的代碼:
// The Vehicle class
@interface Vehicle : NSObject
{
NSColor* color;
}
-(void) setColor:(NSColor*)color;
// 簡潔構造函數
+(id) vehicleWithColor:(NSColor*)color;
其對應的實現是:
// 錯誤的實現
+(Vehicle*) vehicleWithColor:(NSColor*)color
{
// self 不能改變
self = [[self alloc] init]; // 錯誤!
[self setColor:color];
return [self autorelease];
}
記住咱們前面所說的,這裏的 self 指向的是 Class 類型的對象。
// 比較正確的實現
+(id) vehicleWithColor:(NSColor*)color
{
id newInstance = [[Vehicle alloc] init]; // 正確,可是忽略了有子類的狀況
[newInstance setColor:color];
return [newInstance autorelease];
}
我 們來改進一下。Objective-C 中,咱們能夠實現 virtual constructor。這種構造函數經過內省的機制來了解到本身究竟應該建立哪一種類的對象,是這個類自己的仍是其子類的。而後它直接建立正確的類的實 例。咱們能夠使用一個 class 方法(注意,class 在 Objective-C 中不是關鍵字);這是 NSObject 的一個方法,返回當前對象的類對象(也就是 meta-class 對象)。
@implementation Vehicle
+(id) vehicleWithColor:(NSColor*)color
{
id newInstance = [[[self class] alloc] init]; // 完美!咱們能夠在運行時識別出類
[newInstance setColor:color];
return [newInstance autorelease];
}
@interface Car : Vehicle {...}
...
// 建立一個 red Car
id car = [Car vehicleWithColor:[NSColor redColor]];
相似於初始化函數的 init 前綴,這種簡潔構造函數最好使用類名做前綴。不過也有些例外,例如 [NSColor redColor] 返回一個預約義的顏色,按照咱們的約定,使用 [NSColor colorRed] 更合適一些。
最 後,咱們要重複一下,全部使用 alloc、[mutable]copy[WithZone:] 增長引用計數器值的對象,都必須相應地調用 [auto]release。當調用簡潔構造函數時,你並無顯式調用 alloc,也就不該該調用 release。可是,在建立這種構造函數時,必定不要忘記使用 autorelease。
Setters
如 果不對 Objective-C 的內存管理機制有深入的理解,是很難寫出爭取的 setter 的。假設一個類有一個名爲 title 的 NSString 類型的屬性,咱們但願經過 setter 設置其值。這個例子雖然簡單,但已經表現出 setter 所帶來的主要問題:參數如何使用?不一樣於 C++,在 Objective-C 中,對象只能用指針引用,所以 setter 雖然只有一種原型,可是卻可 以有不少種實現:能夠直接指定,能夠使用 retain 指定,或者使用 copy。每一種實現都有特定的目的,須要考慮你 set 新的值以後,新值和舊值之間的關係(是否相互影響等)。另外,每一種實現 都要求及時釋放舊的資源,以免內存泄露。直接指定(不完整的代碼)
外面傳進來的對象僅僅使用引用,不帶有 retain。若是外部對象改變了,當前類也會知 道。也就是說,若是外部對象被釋放掉,而當前類在使用時沒有檢查是否爲 nil,那麼當前類就會持有一個非法引用。
-(void) setString:(NSString*)newString
{
... 稍後解釋內存方面的細節
self->string = newString; // 直接指定
}
使用 retain 指定(不完整的代碼)
外部對象被引用,而且使用 retain 將其引用計數器加 1。外部對象的改變對於當前類也是可見的,不過,外部對象不能被釋 放,由於當前類始終持有一個引用。
-(void) setString:(NSString*)newString
{
... 稍後解釋內存方面的細節
self-> string = [newString retain]; // 使用 retain 指定
}
複製(不完整的代碼)
外部對象實際沒有被引用,使用的是其克隆。此時,外部對象的改變對於當前類是不可變的。也就是說,當前類持有的是這個對象的克隆, 這個對象的生命週期不會比持有者更長。
-(void) setString:(NSString*)newString
{
... 稍後解釋內存方面的細節
self->string = [newString copy]; // 克隆
// 使用 NSCopying 協議
}
爲了補充完整這些代碼,咱們須要考慮這個對象在前一時刻的狀態:每一種情形下,setter 都須要釋放掉舊的資源,而後創建新的。這些代碼看起來比較麻煩。
直接指定( 完整代碼)
這是最簡單的狀況。舊的引用實際上被替換成了新的。
-(void) setString:(NSString*)newString
{
// 沒有強連接,舊值被改變了
self->string = newString; // 直接指定
}
使用 retain 指定(完整代碼)
在這種狀況下,舊值須要被釋放,除非舊值和新值是同樣的。
// ------ 不正確的實現 ------
-(void) setString:(NSString*)newString
{
self->string = [newString retain];
// 錯誤!內存泄露,沒有引用指向舊的「string」,所以再也沒法釋放
}
-(void) setString:(NSString*)newString
{
[self->string release];
self->string = [newString retain];
// 錯誤!若是 newString == string(這是可能的),
// newString 引用是 1,那麼在 [self->string release]以後
// 使用 newString 就是非法的,由於此時對象已經被釋放
}
-(void) setString:(NSString*)newString
{
if (self->string != newString)
[self->string release]; // 正確:給 nil 發送 release 是安全的
self->string = [newString retain]; // 錯誤!應該在 if 裏面
// 由於若是 string == newString,
// 計數器不會被增長
}
// ------ 正確的實現 ------
// 最佳實踐:C++ 程序員通常都會「改變前檢查」
-(void) setString:(NSString*)newString
{
// 僅在必要時修改
if (self->string != newString) {
[self->string release]; // 釋放舊的
self->string = [newString retain]; // retain 新的
}
}
// 最佳實踐:自動釋放舊值
-(void) setString:(NSString*)newString
{
[self->string autorelease]; // 即便 string == newString 也沒有關係,
// 由於 release 是被推遲的
self->string = [newString retain];
//... 所以這個 retain 要在 release 以前發生
}
// 最佳實踐:先 retain 在 release
-(void) setString:(NSString*)newString
{
[self->newString retain]; // 引用計數器加 1(除了 nil)
[self->string release]; // release 時不會是 0
self->string = newString; // 這裏就不該該再加 retain 了
}
複製(完整代碼)
不管是典型的誤用仍是正確的解決方案,都和前面使用 retain 指定同樣,只不過把 retain 換成 copy。
僞克隆
有些克隆是僞克隆,不過對結果沒有影響。
Getters
Objective-C 中,全部對象都是動態分配的,使用指針引用。通常的,getter 僅僅返回指針的值,而不該該複製對象。getter 的名字通常和數據成員的名字相同(這一點不一樣於 Java,JavaBean 規範要求以 get 開頭),這並不會引發任何問題。若是是布爾變量,則使用 is 開頭(相似 JavaBean 規範),這樣可讓程序更具可讀性。
@interface Button
{
NSString* label;
BOOL pressed;
}
-(NSString*) label;
-(void) setLabel:(NSString*)newLabel;
-(BOOL) isPressed;
@end
@implementation Button
-(NSString*) label
{
return label;
}
-(BOOL) isPressed
{
return pressed;
}
-(void) setLabel:(NSString*)newLabel {...}
@end
當返回實例數據指針時,外界就能夠很輕鬆地修改其值。這多是不少 getter 不但願的結果,由於這樣一來就破壞了封裝性。
@interface Button
{
NSMutableString* label;
}
-(NSString*) label;
@end
@implementation Button
-(NSString*) label
{
return label; // 正確,但知道內情的用戶能夠將其強制轉換成 NSMutableString,
// 從而改變字符串的值
}
-(NSString*) label
{
// 解決方案 1 :
return [NSString stringWithString:label];
// 正確:實際返回一個新的不可變字符串
// 解決方案 2 :
return [[label copy] autorelease];
// 正確:返回一個不可變克隆,其值是一個 NSString(注意不是 mutableCopy)
}
@end
必須緊身避免出現循環 retain。若是對象 A retain 對象 B,B 和 C 相互 retain,那麼 B 和 C 就陷入了循環 retain:
A → B ↔ C
如 果 A release B,B 不會真正釋放,由於 C 依然持有 B。C 也不能被釋放,由於 B 持有 C。由於只有 A 可以引用到 B,因此一旦 A release B,就再也沒有對象可以引用這個循環,這樣就不可避免的形成內存泄露。這就是爲何在一個樹結構中,通常是父節點 retain 子節點,而子節點不 retain 父節點。
Objective-C 2.0 實現了一個垃圾收集器。換句話說,你能夠將全部內存管理交給垃圾收集器,不再用關心什麼 retain、release 之類。可是,不一樣於 Java,Objective-C 的垃圾收集器是可選的:你能夠選擇關閉它,從而本身管理對象的生命週期;或者你選擇打開,從而減小不少可能有 bug 的代碼。垃圾收集器是以一個程序爲單位的,所以,打開或者關閉都會影響到整個應用程序。
如 果開啓垃圾收集器,retain、release 和autorelease 都被重定義成什麼都不作。所以,在沒有垃圾收集器狀況下編寫的代碼能夠不作任何改變地移植到有垃圾收集器的環境下,理論上只要從新編譯一遍就能夠了。「理 論上」意思是,不少狀況下涉及到資源釋放處理的時候仍是須要特別謹慎地對待。所以,編寫同時知足兩種狀況的代碼是不大容易的,通常開發者都會選擇從新編 寫。下面,咱們將逐一解釋這二者之間的區別,這些都是須要特別注意的地方。
finalize
在有垃圾收集器的環境下,對象的析構順序是未定義的,所以使用 dealloc 就不大適合了。NSObject 增長了一個 finalize 方法,將析構過程分解爲兩步:資源釋放和有效回收。一個好的 finalize 方法是至關精妙的,須要很好的設計。
weak, strong
不多會見到 __weak 和 __strong 出如今聲明中,但咱們須要對它們有必定的瞭解。
默 認狀況下,一個指針都會使用 __strong 屬性,代表這是一個強引用。這意味着,只要引用存在,對象就不能被銷燬。這是一種所指望的行爲:當全部(強)引用都去除時,對象才能被收集和釋放。不過, 有時咱們卻但願禁用這種行爲:一些集合類不該該增長其元素的引用,由於這會引發對象沒法釋放。在這種狀況下,咱們須要使用弱引用(不用擔憂,內置的集合類 就是這麼幹的),使用 __weak 關鍵字。NSHashTable 就是一個例子。當被引用的對象消失時,弱引用會自動設置爲 nil。Cocoa 的Notification Center 就是這麼一個例子,雖然這已經超出純Objective-C 的語言範疇。
NSMakeCollectable()
Cocoa 並非 Mac OS X 惟一的 API。Core Foundation 就是另一個。它們是兼容的,能夠共享數據和對象。可是 Core Foudation 是由純 C 編寫的。或許你會認爲,Objective-C 的垃圾收集器不能處理 Core Foundation 的指針。但其實是能夠的。感興趣的話能夠關注一下 NSMakeCollectable 的文檔。
AutoZone
由 Apple 開發的 Objective-C 垃圾收集器叫作 AutoZone。這是一個公開的開源庫,咱們能夠看到起源代碼。不過在 Mac OS X 10.6 中,垃圾收集器可能有了一些變化。這裏對此再也不贅述。
比 起 C++ 來,Objective-C中的異常處理更像 Java,這主要是由於 Objective-C 有一個 @finally 關鍵字。Java 中也有一個相似的 finally 關鍵字,但 C++ 中則沒有。finally 是 try()…catch() 塊的一個可選附加塊,其中的代碼是必須執行的,無論有沒有捕獲到異常。這種設計能夠很方便地寫出簡短乾淨的代碼,好比資源釋放等。除此之 外,Objective-C 中的 @try…@catch…@finally 是很經典的設計,同大多數語言沒有什麼區別。可是,不一樣於 C++ 的還有一點,Objective-C 只有對象能夠被拋除。
不帶 finally |
帶有 finally |
BOOL problem = YES;
@try {
dangerousAction();
problem = NO;
} @catch (MyException* e) {
doSomething();
cleanup();
} @catch (NSException* e) {
doSomethingElse();
cleanup();
// 從新拋出異常 @throw
}
if (!problem)
cleanup();
|
@try {
dangerousAction();
} @catch (MyException* e) {
doSomething();
} @catch (NSException* e) {
doSomethingElse();
@throw // 從新拋出異常 } @finally {
cleanup();
}
|
嚴格說來,@finally 不是必要的,可是確實是處理異常強有力的工具。正如前面的例子所示,咱們也能夠在 @catch 中將異常從新拋出。事實上,@finally 在 @try 塊運行結束以後纔會執行。對此咱們將在下面進行解釋。
int f(void)
{
printf("f: 1-you see me\n");
// 注意看輸出的字符串,體會異常處理流程
@throw [NSException exceptionWithName:@"panic"
reason:@"you don’t really want to known"
userInfo:nil];
printf("f: 2-you never see me\n");
}
int g(void)
{
printf("g: 1-you see me\n");
@try {
f();
printf("g: 2-you do not see me (in this example)\n");
} @catch(NSException* e) {
printf("g: 3-you see me\n");
@throw;
printf("g: 4-you never see me\n");
} @finally {
printf("g: 5-you see me\n");
}
printf("g: 6-you do not see me (in this example)\n");
}
最後一點,C++ 的 catch(…) 能夠捕獲任意值,可是Objective-C 中是不能夠的。事實上,只有對象能夠被拋出,也就是說,咱們能夠始終使用 id 捕獲異常。
另外注意,Cocoa 中有一個 NSException 類,推薦使用此類做爲一切異常類的父類。所以,catch(NSException *e) 至關於 C++ 的 catch(…)。
線程安全
在Objective-C 中能夠很清晰地使用 POSIX APIs 2 實現多線程。Cocoa 提供了本身的類管理多線程。有一點是須要注意的:多個線程同時訪問同一個內存區域時,可能會致使不可預料的結果。POSIX APIs 和 Cocoa 都提供了鎖和互斥對象。Objective-C提供了一個關鍵字 @synchronized,與 Java 的同名關鍵字是同樣的。
@synchronized
由 @synchronized(…) 包圍的塊會自動加鎖,保證一次只有一個線程使用。在處理併發時,這並非最好的解決方案,但倒是對大多數關鍵塊的最簡單、最輕量、最方便的解決方案。 @synchonized 要求使用一個對象做爲參數(能夠是任何對象,好比 self),將這個對象做爲鎖使用。
@implementation MyClass
-(void) criticalMethod:(id) anObject {
@synchronized(self) {
// 這段代碼對其餘 @synchronized(self) 都是互斥的
// self 是同一個對象
}
@synchronized(anObject) {
// 這段代碼對其餘 @synchronized(anObject) 都是互斥的
// anObject 是同一個對象
}
}
@end
Objective-C 中惟一的 static 對象
在 C 語言中,字符串就是字符數組,使用char* 指針。處理這種數據很是困難,而且可能引發不少 bug。C++ 的 string 類是一種解脫。在 Objective-C 中,前面咱們曾經介紹過,全部對象都不是自動的,都要在運行時分配內存。惟一不符合的就是 static 字符串。這致使能夠使用 static 的 C 字符串做爲 NSString 的參數。不過這並非一個好的主意,可能會引發內存浪費。幸運的是,咱們也有 static 的 Objective-C 字符串。在使用引號標記的 C 字符串前面加上 @ 符號,就構成了 static 的 Objective-C 字符串。
NSString* notHandy = [[NSString alloc] initWithUTF8String:"helloWorld"];
NSString* stillNotHandy = // initWithFormat 相似 sprintf()
[[NSString alloc] initWithFormat:@"%s", "helloWorld"];
NSString* handy = @"hello world";
另外,static 字符串能夠同普通對象同樣做爲參數使用。
int size = [@"hello" length];
NSString* uppercaseHello = [@"hello" uppercaseString];
NSString 和編碼
NSString 對象很是有用,由於它增長了不少好用的方法,而且支持不一樣的編碼,如 ASCII、UNICODE、ISO Latin 1等。所以,翻譯和本地化應用程序也變得很簡單。
對象描述,%@擴展,NSString 轉 C 字符串
在 Java 中,每個對象都繼承自 Object,所以都有一個 toString 方法,用於使用字符串形式描述對象自己。這種功能對於調試很是有用。Objective-C 中,相似的方法叫作 description,返回一個 NSString 對象。
C 語言的 printf 函數不能輸出 NSString。咱們能夠使用 NSLog 得到相似的功能。NSLog 相似於 printf,能夠向控制檯輸出格式化字符串。須要注意的是,NSString 的格式化符號是 %@,不是 %s。事實上,%@ 能夠用於任意對象,由於它實際是調用的 -(NSString*) description。
NSString 能夠使用 UTF8String 方法轉換成 C 風格字符串。
char* name = "Spot";
NSString* action1 = @"running";
printf("My name is %s, I like %s, and %s...\n",
name, [action1 UTF8String], [@"running again" UTF8String]);
NSLog(@"My name is %s, I like %@ and %@\n",
name, action1, @"running again");
如今,你已經瞭解到 C++ 的面向對象概念在 Objective-C 中的描述。可是,另一些 C++ 的概念並無涉及。這些概念並不相關面向對象,而是關於一些代碼編寫的問題。
引用
Objective-C 中不存在引用(&)的概念。因爲Objective-C 使用引用計數器和autorelease 管理內存,這使得引用沒有多大用處。既然對象都是動態分配的,它們惟一的引用就是指針。
內聯
Objective-C 不支持內聯 inline。對於方法而言,這是合理的,由於Objective-C 的動態性使得「凍結」某些代碼變得很困難。可是,內聯對某些用 C 編寫的函數,好比 max(), min() 仍是比較有用的。這一問題在 Objective-C++ (這是另一種相似的語言)中獲得解決。
無 論如何,GCC 編譯器仍是提供了一個非標準關鍵字 __inline 或者 __inline__,容許在 C 或者 Objective-C 中使用內聯。另外,GCC 也能夠編譯 C99 代碼,在 C99 中,一樣提供了內聯關鍵字 inline(這下就是標準的了)。所以,在基於 C99的 Objective-C 代碼中是能夠使用內聯的。若是不是爲了使用而使用內聯,而是關心性能,那麼你應該考慮 IMP 緩存。
模板
模板是獨立於繼承和虛函數的另一種機制,主要爲性能設計,已經超出了純粹的面向對象模型(你注意到使用模板能夠很巧妙的訪問到 private 變量嗎?)。Objective-C 不支持模板,由於其獨特的方法名規則和選擇器使得模板很難實現。
運算符重載
Objective-C 不支持運算符重載。
友元
Objective-C 沒有友元的概念。事實上,在 C++ 中,友元很大程度上是爲了實現運算符重載。Java 中包的概念在必定程度上相似友元,這能夠使用分類來處理。
const 方法
Objective-C 中方法不能用 const 修飾。所以也就不存在 mutable 關鍵字。
初始化列表
Objective-C 中沒有初始化列表的概念。
C++ 標準庫是其強大的一個緣由。即便它還有一些不足,可是已經可以算做是比較完備的了。這並非語言的一部分,而是屬於一種擴展,其餘語言也有相似的部分。在 Objective-C 中,你不得不在 Cocoa 裏面尋找容器、遍歷器或者其餘一些真正能夠使用的算法。
Cocoa 的容器比 C++ 更加面向對象,它不使用模板實現,只能存放對象。如今可用的容器有:
· NSArray 和 NSMutableArray:有序集合;
· NSSet 和 NSMutableSet:無序集合;
· NSDictionary和 NSMutableDictionary:鍵值對形式的關聯集合;
· NSHashTable:使用弱引用的散列表(Objective-C 2.0 新增)。
你可能會發現這其中並無 NSList 或者 NSQueue。事實上,這些容器均可以由 NSArray 實現。
不 同於 C++ 的 vector<T>,Objective-C 的 NSArray 真正隱藏了它的內部實現,僅可以使用訪問器獲取其內容。所以,NSArray 沒有義務爲內存單元優化其內容。NSArray的實現有一些妥協,以便 NSArray 可以像數組或者列表同樣使用。既然 Objective-C 的容器只能存放指針,單元維護就會比較有效率了。
NSHashTable 等價於 NSSet,但它使用的是弱引用(咱們曾在前面的章節中講到過)。這對於垃圾收集器頗有幫助。
經典的枚舉
純面向對象的實現讓 Objective-C 比 C++ 更容易實現遍歷器。NSEnumerator就是爲了這個設計的:
NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = [enumerator nextObject];
while (anObject != nil)
{
[anObject doSomethingWithString:aString];
anObject = [enumerator nextObject];
}
容 器的 objectEnumerator 方法返回一個遍歷器。遍歷器能夠使用 nextObject 移動本身。這種行爲更像 Java 而不是 C++。當遍歷器到達容器末尾時,nextObject 返回 nil。下面是最普通的使用遍歷器的語法,使用的 C 語言風格的簡寫:
NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = nil;
while ((anObject = [enumerator nextObject])) {
[anObject doSomethingWithString:aString];
}
// 雙括號可以防止 gcc 發出警告
快速枚舉
Objective-C 2.0 提供了一個使用遍歷器的新語法,隱式使用 NSEnumerator(其實和通常的 NSEnumerator 沒有什麼區別)。它的具體形式是:
NSArray* someContainer = ...;
for(id object in someContainer) { // 每個對象都是用 id 類型
...
}
for(NSString* object in someContainer) { // 每個對象都是 NSString
...// 開發人員須要處理不是 NSString* 的狀況
}
使用選擇器
Objective-C 的選擇器很強大,於是大大減小了函數對象的使用。事實上,弱類型容許用戶無需關心實際類型就能夠發送消息。例如,下面的代碼同前面使用遍歷器的是等價的:
NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSString* aString = @"foo";
[array makeObjectsPerformSelector:@selector(doSomethingWithString:)
withObject:aString];
在這段代碼中,每一個對象不必定非得是 NSString 類型,而且對象也不須要必須實現了 doSomethingWithString: 方法(這會引起一個異常:selector not recognized)。
IMP 緩存
咱們在這裏不會詳細解釋這個問題,可是的確能夠得到 C 函數的內存地址。經過僅查找一次函數地址,能夠優化同一個選擇器的屢次調用。這被稱爲 IMP 緩存,由於 Objective-C 用於方法實現的數據類型就是 IMP。
調用 class_getMethodImplementation() 就能夠得到這麼一個指針。可是請注意,這是指向實現方法的真實的指針,所以不能有虛調用。它的使用通常在須要很好的時間優化的場合,而且必須很是當心。
STL 中那一大堆通用算法在 Objective-C 中都沒有對等的實現。相反,你應該仔細查找下各個容器中有沒有你須要的算法。
本章中心是兩個可以讓代碼更簡潔的特性。它們的目的大相徑庭:鍵值對編碼能夠經過選擇第一個符合條件的實現而解決間接方法調用;屬性則可讓編譯器幫咱們生成部分代碼。鍵值對編碼其實是 Cocoa 引入的,而屬性則是 Objective-C 2.0 語言新增長的。鍵值對編碼(KVC)
原則
鍵 值對編碼意思是,可以經過數據成員的名字來訪問到它的值。這種語法很相似於關聯數組(在 Cocoa 中就是 NSDictionary),數據成員的名字就是這裏的鍵。NSObject 有一個 valueForKey: 和 setValue:forKey: 方法。若是數據成員就是對象本身,尋值過程就會向下深刻下去,此時,這個鍵應該是一個路徑,使用點號 . 分割,對應的方法是 valueForKeyPath: 和 setValue:forKeyPath:。
@interface A {
NSString* foo;
}
... // 其它代碼
@interface B {
NSString* bar;
A* myA;
}
... // 其它代碼
@implementation B
...
// 假設 A 類型的對象 a,B 類型的對象 b
A* a = ...;
B* b = ...;
NSString* s1 = [a valueForKey:@"foo"]; // 正確
NSString* s2 = [b valueForKey:@"bar"]; // 正確
NSString* s3 = [b valueForKey:@"myA"]; // 正確
NSString* s4 = [b valueForKeyPath:@"myA.foo"]; // 正確
NSString* s5 = [b valueForKey:@"myA.foo"]; // 錯誤
NSString* s6 = [b valueForKeyPath:@"bar"]; // 正確
...
這 種語法可以讓咱們對不一樣的類使用相同的代碼來處理同名數據。注意,這裏的數據成員的名字都是使用的字符串的形式。這種使用方法的最好的用處在於將數據(名 字)綁定到一些觸發器(尤爲是方法調用)上,例如鍵值對觀察(Key-Value Observing, KVO)等。
攔截
經過 valueForKey: 或者 setValue:forKey: 訪問數據不是原子操做。這個操做本質上仍是一個方法調用。事實上,這種訪問當某些方式實現的狀況下才是可用的,例如使用屬性自動添加的代碼等等,或者顯式容許直接訪問數據。
Apple 的文檔對 valueForKey: 和 setValue:forKey: 的使用有清晰的文檔:
對於 valueForKey:@」foo」 的調用:
· 若是有方法名爲 getFoo,則調用 getFoo;
· 不然,若是有方法名爲 foo,則調用 foo(這是對常見的狀況);
· 不然,若是有方法名爲 isFoo,則調用 isFoo(主要是布爾值的時候);
· 不然,若是類的 accessInstanceVariablesDirectly 方法返回 YES,則嘗試訪問 _foo 數據成員(若是有的話),不然尋找 _isFoo,而後是 foo,而後是 isFoo;
· 若是前一個步驟成功,則返回對應的值;
· 若是失敗,則調用 valueForUndefinedKey:,這個方法的默認實現是拋出一個異常。
對於 forKey:@」foo」 的調用:
· 若是有方法名爲 setFoo:,則調用 setFoo:;
· 不然,若是類的 accessInstanceVariablesDirectly 返回 YES,則嘗試直接寫入數據成員 _foo(若是存在的話),不然尋找 _isFoo,而後是 foo,而後是 isFoo;
· 若是失敗,則調用 setValue:forUndefinedKey:,其默認實現是拋出一個異常。
注 意 valueForKey: 和 setValue:forKey: 的調用能夠用於觸發任何相關方法。若是沒有這個名字的數據成員,則就是一個虛假的調用。例如, 在字符串變量上調用 valueForKey:@」length」 等價於直接調用 length 方法,由於這是 KVC 可以找到的第一個匹配。可是,KVC 的性能不如直接調用方法,因此應當儘可能避免。
原型
使 用 KVC 有必定的方法原型的要求:getters 不能有參數,而且要返回一個對象;setters 須要有一個對象做爲參數,不能有返回值。參數的類型不是很重要的,由於你能夠使用 id 做爲參數類型。注意,struct 和原生類型(int,float 等)都是支持的:Objective-C 有一個自動裝箱機制,能夠將這些原生類型封裝成 NSNumber 或者 NSValue 對象。所以,valueForKey: 返回值都是一個對象。若是須要向 setValue:forKey: 傳入 nil,須要使用 setNilValueForKey:。
高級特性
有幾點細節須要注意,儘管在這裏並不會很詳細地討論這個問題:
1. keypath 能夠包含計算值,例如求和、求平均、最大值、最小值等;使用 @ 標記;
2. 注意方法一致性,例如 valueForKey: 或者 setValue:forKey: 以及關聯數組集合中常見的 objectForKey: 和 setObject:forKey:。這裏,一樣使用 @ 進行區分。
使用屬性
在定義類時有一個屬性的概念。咱們使用關鍵字 @property 來標記一個屬性,告訴編譯器自動生成訪問代碼。屬性的主要意義在於節省開發代碼量。
訪 問屬性的語法比方法調用簡單,所以即便咱們須要編寫代碼時,咱們也能夠使用屬性。訪問屬性同方法調用的性能是同樣的,由於屬性的使用在編譯期實際就是換成 了方法調用。大多數時候,屬性用於封裝成員變量。可是,咱們也能夠提供一個「假」的屬性,看似是訪問一個數據成員,但實際不是;換句話說,看起來像是從對 象外部調用一個屬性,但實際上其實現要比一個值的管理操做要複雜得多。
屬性的描述
對屬性的描述其實是要告訴編譯器如何生成訪問器的代碼:
· 屬性從外界是隻讀的嗎?
· 若是數據成員是原生類型,可選餘地不大;若是是對象,那麼使用 copy 封裝的話,是要用強引用仍是弱引用?
· 屬性是線程安全的嗎?
· 訪問器的名字是什麼?
· 屬性應該關聯到哪個數據成員?
· 應該自動生成哪個訪問器,哪個則留給開發人員?
咱們須要兩個步驟來回答這些問題:
· 在類的@interface 塊中,屬性的聲明須要提供附屬參數;
· 在類的@implementation 塊中,訪問器能夠隱式生成,也能夠指定一個實現。
屬 性訪問器是有嚴格規定的:getter 要求必須返回所指望的類型(或者是相容類型);setter 必須返回 void,而且只能有一個指望類型的參數。訪問器的名字也是規定好的:對於數據 foo,getter 的名字是 foo,setter 的名字是 setFoo:。固然,咱們也能夠指定自定義的名字,可是不一樣於前面所說的鍵值對編碼,這個名字必須在編譯期肯定,由於屬性的使用被設計成要和方法的直接 調用同樣的性能。所以,若是類型是不相容的,是不會有裝箱機制的。
如下是帶有註釋的例子,先來有一個大致的瞭解。
@interface class Car : NSObject
{
NSString* registration;
Person* driver;
}
// registration 是隻讀的,使用 copy 設置
@property NSString* (readonly, copy) registration;
// driver 使用弱引用(沒有 retain),能夠被修改
@property Person* (assign) driver;
@end
...
@implementation
// 開發者沒有提供,由編譯期生成 registration 的代碼
@synthesize registration;
// 開發者提供了 driver 的 getter/setter 實現
@dynamic driver;
// 該方法將做爲 @dynamic driver 的 getter
-(Person*) driver {
...
}
// 該方法將做爲 @dynamic driver 的 setter
-(void) setDriver:(Person*)value {
...
}
@end
屬性的參數
屬性的聲明使用一下模板:
@property type name;
或者
@property(attributes) type name;
若是沒有給出屬性的參數,那麼將使用默認值;不然將使用給出的參數值。這些參數值能夠是:
· readwrite(默認)或者 readonly:設置屬性是可讀寫的(擁有 getter/setter)或是隻讀的(只有 getter);
· assign(默認),retain 或 copy:設置屬性的存儲方式;
· nonatomic:不生成線程安全的代碼,默認是生成的(沒有 atomic 關鍵字);
· getter=…,setter=…:改變訪問器默認的名字。
對於 setter,默認行爲是 assign;retain 或者 copy 用於數據成員被修改時的操做。在一個 -(void) setFoo:(Foo*)value 方法中,會所以生成三種不一樣的語句:
· self->foo= value ; // 簡單賦值
· self->foo= [value retain]; // 賦值,同時引用計數器加 1
· self->foo= [value copy]; // 對象拷貝(必須知足協議 NSCopying)
在有垃圾收集器的環境下,retain 同 assign 沒有區別,可是能夠加上 __weak 或者 __strong。
@property(copy,getter=getS,setter=setF:) __weak NSString* s; // 複雜聲明
注意不要忘記 setter 的冒號 : 。
屬性的自定義實現
上 一章中咱們提到的代碼中有兩個關鍵字 @synthesize 和 @dynamic。@dynamic 意思是由開發人員提供相應的代碼:對於只讀屬性須要提供 setter,對於讀寫屬性須要提供 setter 和 getter。@synthesize 意思是,除非開發人員已經作了,不然由編譯器生成相應的代碼,以知足屬性聲明。對於上次的例子,若是開發人員提供了 -(NSString*)registration,編譯器就會選擇這個實現,不會用新的覆蓋。所以,咱們可讓編譯器幫咱們生成代碼,以簡化咱們本身的 代碼輸入量。最後,若是編譯期沒有找到訪問器,並且沒有使用 @synthesize 聲明,那麼它就會在運行時添加進來。這一樣能夠實現屬性的訪問,可是即便這樣,訪問器的名字也須要在編譯期決定。若是運行期沒有找到訪問器,就會觸發一個 異常,但程序不會中止,正如同方法的缺失。當咱們使用 @synthesize 時,編譯器會被要求綁定某一特定的數據成員,並不必定是同樣的名字。
@interface A : NSObject {
int _foo;
}
@property int foo;
@implementation A
@synthesize foo=_foo; // 綁定 "_foo" 而不是 "foo"
訪問屬性的語法
爲獲取或設置屬性,咱們使用點號:這同簡單的 C 結構是一致的,也是在 keypath 中使用的語法,其性能與普通方法調用沒有區別。
@interface A : NSObject {
int i;
}
@property int i;
@interface B : NSObject {
A* myA;
}
@property(retain) A* a;
...
A* a = ...
B* b = ...;
a.i = 1; // 等價於 [a setI:1];
b.myA.i = 1;// 等價於 [[b myA] setI:1];
請注意上面例子中 A 類的使用。self->i 和 self.i 是有很大區別的:self->i 直接訪問數據成員,而 self.i 則是使用屬性機制,是一個方法調用。
高級細節
64 位編譯器上,Objective-C 運行時環境與 32 位有一些不一樣。關聯到 @property 的實例數據可能被忽略掉,例如被視爲隱式的。更多細節請閱讀 Apple 的文檔。
RTTI 即運行時類型信息,可以在運行的時候知道須要的類型信息。C++ 有時被認爲是一個「假的」面嚮對象語言。相比 Objective-C,C++ 顯得很是靜態。這有利於在運行時得到最好的性能。C++ 使用 typeinfo 庫提供運行時信息,但這不是安全的,由於這個庫依賴於編譯器的實現。通常來講,查找對象的類型是一個不多見的請求,由於語言是強類型的,通常在編譯時就已 經肯定其類型了;可是,有時候這種能力對於容器很經常使用。咱們能夠使用 dynamic_cast 和 typeid 運算符,可是程序交互則會在必定程度上受限。那麼,如何由名字獲知這個對象的類型呢?Objective-C 語言能夠很容易地實現這種操做。類也是對象,它們繼承它們的行爲。
class,superclass, isMemberOfClass, isKindOfClass
對象在運行時獲取其類型的能力稱爲內省。內省能夠有多種方法實現。
isMemberOfClass: 能夠用於回答這種問題:「我是給定類(不包括子類)的實例嗎?」,而 isKindOfClass: 則是「我是給定類或其子類的實例嗎?」使用這種方法須要一個「假」關鍵字的 class(注意,不是 @class ,@class 是用於前向聲明的)。事實上,class是 NSObject 的一個方法,返回一個 Class 對象。這個對象是元類的一個實例。請注意,nil 值的類是 Nil。
BOOL test = [self isKindOfClass:[Foo class]];
if (test)
printf("I am an instance of the Foo class\n");
注意,你能夠使用 superclass 方法獲取其父類。
conformsToProtocol
該 方法用於肯定一個對象是否和某一協議兼容。咱們前面曾經介紹過這個方法。它並非動態的。編譯器僅僅檢查每個顯式聲明,而不會檢查每個方法。若是一個 對象實現了給定協議的全部方法,但並無顯式聲明說它實現了該協議,程序運行是正常的,可是 conformsToProtocol: 會返回 NO。
respondsToSelector,instancesRespondToSelector
respondsToSelector: 是一個實例方法,繼承自 NSObject。該方法用於檢查一個對象是否實現了給定的方法。這裏如要使用 @selector。例如:
if ( [self respondsToSelector:@selector(work)] )
{
printf("I am not lazy.\n");
[self work];
}
若是要檢查一個對象是否實現了給定的方法,而不檢查繼承的方法,能夠使用類方法 instancesRespondToSelector:。例如:
if ([[self class] instancesRespondToSelector:@selector(findWork)])
{
printf("I can find a job without the help of my mother\n");
}
注意,respondsToSelector: 不能用於僅僅使用了前向聲明的類。
強類型和弱類型 id
C++ 使用的是強類型:對象必須符合其類型,不然不能經過編譯。在 Objective-C 中,這個限制就靈活得多了。若是一個對象與消息的目標對象不相容,編譯器僅僅發出一個警告,而程序則繼續運行。這個消息會被丟棄(引起一個異常),除非前 面已經轉發。若是這就是開發人員指望的,這個警告就是冗餘的;在這種情形下,使用弱類型的 id 來替代其真實類型就能夠消除警告。事實上,任何對象都是 id 類型的,而且能夠處理任何消息。這種弱類型在使用代理的時候是必要的:代理對象不須要知道本身被使用了。例如:
-(void) setAssistant:(id)anObject
{
[assistant autorelease];
assistant = [anObject retain];
}
-(void) manageDocument:(Document*)document
{
if ([assistant respondToSelector:@(manageDocument:)])
[assistant manageDocument:document];
else
printf("Did you fill the blue form ?\n");
}
在 Cocoa 中,這種代理被大量用於圖形用戶界面的設計中。它能夠很方便地把控制權由用戶對象移交給工做對象。
運行時操做 Objective-C 類
通 過添加頭文件 <objc/objc-runtime.h>,咱們能夠調用不少工具函數,用於在運行時獲取類信息、添加方法或實例變量。 Objective-C 2.0 又引入了一些新函數,比 Objective-C 1.0 更加靈活(例如使用 class_addMethod(…) 替代 class_addMethods(…)),同時廢棄了許多 1.0 的函數。這讓咱們能夠很方便的在運行時修改類。
《從 C++ 到 Objective-C》系列已經結束。再次重申一下,本系列不是一個完整的 Objective-C 的教學文檔,只是方便熟悉 C++ 或者類 C++ 的開發人員(例如廣大的 Java 程序員)可以很快的使用 Objective-C 進行簡單的開發。固然,目前 Objective-C 的最普遍應用在於 Apple 系列的開發,MacOS X、iOS 等。本系列僅僅介紹的是 Objective-C 語言自己,對於 Apple 系列的開發則沒有不少的涉及。正如你僅僅知道 C++ 的語法,不瞭解各類各樣的庫是作不出什麼東西的,學習 Objective-C 也不得不去了解 MacOS 或者 iOS 等更多的庫的使用。這一點已經不在本系列的範疇內,這一點還請你們見諒。下面是本系列的目錄:
1. 前言
2. 語法概述
3. 類和對象
4. 類和對象(續)
5. 類和對象(續二)
6. 類和對象(續三)
7. 繼承
8. 繼承(續)
9. 實例化
10. 實例化(續)
11. 實例化(續二)
12. 實例化(續三)
13. 內存管理
14. 內存管理(續)
15. 內存管理(續二)
16. 內存管理(續三)
17. 異常處理和多線程
18. 字符串和C++ 特性
19. STL 和 Cocoa
20. 隱式代碼
21. 隱式代碼(續)
22. 隱式代碼(續二)
23. 動態
24. 結語