第4章 對象的類型和動(dòng)態(tài)綁定
4.1 動(dòng)態(tài)綁定
4.1.1 什么是動(dòng)態(tài)綁定
在實(shí)際程序中會(huì)使用各種類的實(shí)例對象丁溅,這些對象都有用id類型表示窟赏,但如果這樣做拷况,程序中就會(huì)出現(xiàn)無法區(qū)分某個(gè)對象到底屬于哪個(gè)類的情況
比如:
#import <Foundation/NSObject.h>
#import<stdio.h>
@interface A:NSObject
- (void)whoareyou;
@end//
@implementation A
- (void)whoareyou{
? ? printf("i am A");
}
@end//
@interface B:NSObject
- (void)whoareyou;
@end//
@implementation B
- (void)whoareyou{
? ? printf("i? am? B");
}
@end//
intmain()
{
? ? idobj;
? ? intn;
? ? scanf("%d",&n);
? ? switch(n){
? ? ? ? case0: obj = [[Aalloc]init];break;
? ? ? ? case1: obj = [[Balloc]init];break;
? ? ? ? case2: obj = [[NSObjectalloc]init];break;
? ? }
? ? [obj? ? whoareyou];
? ? return0;
}
NSObject? ?中沒有whoareyou 方法,但編譯時(shí)沒有給出任何的警告奏寨,正常地生成了可執(zhí)行程序病瞳。
執(zhí)行該程序仍源,輸入0時(shí)終端顯示i? ? am? ? A笼踩,輸入1時(shí)終端顯示i? ? am? ? B亡嫌,輸入2時(shí)程序出現(xiàn)異常挟冠。
原因在于類NSObject的實(shí)例對象沒有實(shí)現(xiàn)方法whoareyou知染,所以運(yùn)行時(shí)出現(xiàn)了問題控淡。
編譯正常的原因是編譯時(shí)無法確定存儲(chǔ)在id中的對象的類型。
Objective-C中的消息是在運(yùn)行時(shí)才去綁定的掺炭,運(yùn)行時(shí)系統(tǒng)會(huì)首先確定接受者的類型(動(dòng)態(tài)類型識(shí)別)辫诅,然后根據(jù)消息名在類的所有的方法去找,如果沒有找到就去父類找涧狮,如果最后還是沒有找到要調(diào)用的方法炕矮,就會(huì)出現(xiàn)異常,報(bào)告上述不能識(shí)別消息的錯(cuò)誤者冤。
動(dòng)態(tài)綁定說的是在程序執(zhí)行時(shí)才確定對象的屬性和需要響應(yīng)的消息
C語言不支持對象綁定肤视,在程序執(zhí)行前基本上已經(jīng)綁定好了各種方法涉枫。
通過C語言的函數(shù)指針可以模擬動(dòng)態(tài)綁定的實(shí)現(xiàn)邢滑。
4.1.2 多態(tài)
在面向?qū)ο蟮某绦虻脑O(shè)計(jì)中,多態(tài)指的是同一操作作用于不同類的實(shí)例時(shí)拜银,將產(chǎn)生不同的結(jié)果殊鞭。
面向過程:面對不同對象使用不同的方法
面向?qū)ο螅好鎸Σ煌瑢ο笫褂孟嗤椒?/p>
利用繼承和多態(tài)方法配合能夠更簡單地定義新的圖形,增強(qiáng)了軟件的靈活性和擴(kuò)展性尼桶。
4.2 作為類型的類
4.2.1 把類作為一種類型
除了id類型操灿,也可以有
? ? ? ? ? ? Volume *v;
? ? ? ? ? ? MuteVolume *mute;
指針表示的對象,賦值操作傳遞的是指針而不是對象本身泵督,這樣可能會(huì)更改對象的值趾盐。
4.2.2 空指針nil
Objective-C 中,nil表示一個(gè)空的對象,這個(gè)對象的指針指向空小腊,nil是指向id類型的指針救鲤,值為0,初始化失敗時(shí)通常返回為nil秩冈。
比如新生成一個(gè)實(shí)例變量時(shí)本缠,alloc方法將數(shù)值類型的實(shí)例變量初始化為0,id和其他類型的指針變量則為nil入问。
4.2.3 靜態(tài)類型的檢查
雖然Objective-C中 id數(shù)據(jù)類型可以存儲(chǔ)任何類型的對象丹锹,但大多數(shù)情況下我們還是會(huì)指定變量為特定類的對象,這種情況稱為靜態(tài)類型芬失。
使用靜態(tài)類型時(shí)楣黍,編譯器在編譯時(shí)可以進(jìn)行類型檢查,避免錯(cuò)誤棱烂。
4.2.4 靜態(tài)類型檢查的總結(jié)
1.對于id類型的變量租漂,調(diào)用任何方法都可以通過編譯
2.id類型的變量和被定義為特定類的變量之間是可以相互賦值的,也可以接受參數(shù)和返回值
3.被定義為特定類的對象的變量(靜態(tài)類型)颊糜,如果調(diào)用了類或父類中未定義的方法哩治,編譯器就會(huì)警告。
4.如果是靜態(tài)類型的變量衬鱼,子類類型的實(shí)例變量可以賦值給父類锚扎,但如果使用了子類中特有的方法會(huì)警告。
5.如果是靜態(tài)類型的變量馁启,父類類型的實(shí)例變量不可以賦值給子類的實(shí)例變量驾孔,因?yàn)楦割惖淖兞繜o法應(yīng)對子類的特有方法,所以會(huì)有警告惯疙。
6.真正要判斷到底是哪個(gè)類的方法被執(zhí)行了翠勉,不要看變量聲明的類型,而要看實(shí)際執(zhí)行時(shí)這個(gè)變量的類型霉颠。
7.id類型不是(NSObject *)類型
id類型與其他類之間沒有任何繼承關(guān)系对碌。
Objective-C的靜態(tài)類型檢查是在編譯時(shí)期完成的,向一個(gè)靜態(tài)類型的對象發(fā)送消息時(shí)蒿偎,編譯器需要確保接受者可以響應(yīng)消息朽们,否則會(huì)警告怀读,當(dāng)你把一個(gè)靜態(tài)類型賦給另一個(gè)靜態(tài)類型時(shí),編譯器需要確保這種賦值是兼容的骑脱。
運(yùn)行時(shí)實(shí)際被執(zhí)行的方法同變量定義時(shí)的類型無關(guān)菜枷,只與運(yùn)行時(shí)這個(gè)變量的實(shí)際對象有關(guān)。
4.3 編程中的類型定義
4.3.1 簽名不一致時(shí)的情況
消息選擇器中并不包含參數(shù)和返回值類型的信息叁丧,消息選擇器和這樣類型消息結(jié)合起來構(gòu)成簽名啤誊,簽名被用于在運(yùn)行時(shí)標(biāo)記一個(gè)方法。接口文件方法的定義也叫做簽名(signature)
方法是被消息選擇器調(diào)用的拥娄,并不是簽名蚊锹,所以在選擇器相同的情況下,參數(shù)和返回值可以不一樣稚瘾,當(dāng)你使用id動(dòng)態(tài)綁定時(shí)牡昆,可能會(huì)出現(xiàn)錯(cuò)誤,最簡單的解決方法就是更改消息名摊欠,或者更改方法類型為靜態(tài)類型來屏蔽警告。
專欄:重載
Objective-C是動(dòng)態(tài)語言凄硼,參數(shù)的類型在運(yùn)行時(shí)才確定,所以不支持根據(jù)參數(shù)類型的不同來調(diào)用不同函數(shù)的重載狐史。Objective-C可以通過動(dòng)態(tài)綁定讓同一個(gè)消息選擇器執(zhí)行不同的功能來執(zhí)行重載骏全。
4.3.2 類的前置聲明
在我們定義一個(gè)類中姜贡,有時(shí)會(huì)將類的變量和類方法的參數(shù)和返回值類型指定為另一個(gè)類楼咳。
#import "Volume.h"
@interface AudiioPlayer:NSObject{
? ? Volume *theVolume;
}
-? ? (Volume *)Volume;
......
以上是可以的母怜,但也有一些缺點(diǎn)苹熏,頭文件中除了類名之外轨域,還有其他信息的定義,另外朱巨,引入頭文件里還可能加上了其他類的頭文件,這樣循環(huán)會(huì)加大編譯的負(fù)擔(dān)搀暑。
如果只是想在類型定義中使用一下類型跨琳,可以使用一種新的解決方法
#import <Foundaion/NSObject.h>
@class? ? Volume;
@interface AudioPlayer :NSObject
.......
編譯指令@class 告訴編譯器Volume是一個(gè)類名桂敛,這種寫法被叫做前置說明
class指令的后面可以一次性接多個(gè)類术唬,比如滚澜、
@class? ? NSString,NSArray,NSMutableArray;
@class? ? Volume,MuteVolume;
通過@class可以提升程序整體的編譯速度设捐,但同時(shí)需要注意的是蚂斤,如果不是簡單地想引入類型槐沼,就需要引入原有類的頭文件岗钩。
@class的另一個(gè)好處是凹嘲,當(dāng)多個(gè)接口出現(xiàn)類嵌套定義的時(shí)候,如果只是互相包含對方的頭文件無法解決疲恢,就必須使用前置說明來解決這種問題显拳。
4.3.3 強(qiáng)制類型轉(zhuǎn)換的使用類型
在一些情況下必須使用強(qiáng)制類型轉(zhuǎn)換杂数。
一個(gè)典型的例子就是:父類類型的指針指向了子類類型變量揍移。
在子類實(shí)例賦予父類實(shí)例時(shí)那伐,調(diào)用子類的新增函數(shù)會(huì)警告石蔗,需要使用強(qiáng)制類型轉(zhuǎn)換清除異常养距。
雖然強(qiáng)制類型轉(zhuǎn)換的功能十分強(qiáng)大棍厌,但會(huì)讓編譯器的類型檢查變的沒有意義定铜,所以要盡量少用揣炕,大部分時(shí)候需要思考設(shè)計(jì)是否合理畸陡。
4.4 實(shí)例變量的數(shù)據(jù)封裝
?4.4.1 實(shí)例變量的訪問權(quán)限
目前所說的都是對象自身來訪問丁恭,修改自己的實(shí)例變量牲览,那么對象能訪問贡必,獲取,修改另一個(gè)對象的實(shí)例變量嗎衫樊?
OBjective-C從原則上不允許從對象外直接訪問對象的實(shí)例變量,但在類的方法中可以直接訪問包含self和self以外的實(shí)例變量臀栈,格式? ? ?obj? ? ->? ? variable羡洛, variable必須是靜態(tài)類型欲侮,否則過不了編譯威蕉。
同類型檢查一樣韧涨,能不能訪問對象的實(shí)例變量也需要檢查侮繁,這個(gè)檢查在編譯時(shí)完成,因此娩贷,只能訪問使用靜態(tài)類型定義的實(shí)例對象的內(nèi)部對象,訪問類中變量的實(shí)例必須與對象所在類一致品抽,否則即便是父類也不能用這種方法直接訪問實(shí)例中的變量突倍,強(qiáng)調(diào)obj? ? ->? ? variable? ? 中variable必須是靜態(tài)類型烧颖。
4.4.2 訪問器
OC不允許在類的外部訪問類的屬性炕淮,只可以訪問同一個(gè)類的其他實(shí)例對象的變量涂圆,我們通常會(huì)定義專門的方法來訪問或修改實(shí)例對象。
例如踩衩,類中有一個(gè)float類型,變量名為weight的屬性贩汉,從類外部訪問這個(gè)屬性的方法應(yīng)該和該屬性同名
-? ? (float)weight
定義修改該屬性的方法時(shí)驱富,可以用set作為前綴,之后接要更改屬性的名稱匹舞,屬性名的第一個(gè)字母要求大寫褐鸥。
-? ? (void)? ?setweight:float(value)
這種用于讀取,修改實(shí)例對象的方法稱為訪問器或訪問方法赐稽,讀取屬性值的方法稱為get方法叫榕,修改屬性值的方法稱為setter方法。這二種方法可以簡稱為getter和setter姊舵,setter方法返回值參數(shù)類型為void晰绎。
為什么不允許直接訪問成員屬性锄弱,而要通過getter和setter方法來完成操作呢
比如 screen.frame.size = CGSizeMake(50,50);
為了封裝蚯窥,類中包含哪些變量和怎么使用這些實(shí)例變量都和類的實(shí)現(xiàn)緊密相關(guān),而如果允許直接訪問類的變量榔幸,當(dāng)類的實(shí)現(xiàn)發(fā)生了變化拨齐,實(shí)例變量被刪除或者作用發(fā)生變化時(shí),所有調(diào)用這個(gè)類的外部模板都需要改蹂匹。
如果使用getter、setter形式,那么當(dāng)類的實(shí)現(xiàn)發(fā)生改變時(shí)只需要更改getter眷蚓、setter接口,而外部需要改。
雖然子類的方法可以直接訪問父類的變量,但為了更好的封裝轧膘,我們需要更多的使用getter和setter方法訪問父類的變量,使程序低耦合。
4.4.3 實(shí)例變量的可見性
如果一定要從外部訪問對象的屬性也可以完成訪問甸鸟。我們知道默認(rèn)條件下子類是可以訪問父類的實(shí)例變量的瞧省,也有辦法讓子類不能訪問父類的變量
能否從外部訪問變量決定了訪問的可見性橡淑。Objective-C中有四種可見修飾符
@private
只能在聲明他的類中訪問掰茶,子類中不能訪問
用法:類的方法中通過-》來訪問同一個(gè)類的實(shí)例變量
@protected
能夠被聲明他的類和任何子類訪問
用法:類的方法中通過-》來訪問本類實(shí)例對象的實(shí)例變量,沒有顯示指定可見性的實(shí)例變量都默認(rèn)為protected暖混。
@public
作用范圍最大,本類和其他類都可以直接訪問
@package
可以像@public一樣訪問宴猾,而框架外則同@private一樣送淆,不允許訪問。
4.4.4 在實(shí)現(xiàn)部分中定義實(shí)例變量
Xcode 4.2之后的編譯器 clang,允許在實(shí)現(xiàn)部分中定義類的變量隅俘。
#import
@interface RGB:NSObject
- (id)initWithRed:(int)r green:(int)g blue:(int)b;
- (id)blendColor:(RGB *)color;
- (void)print;
@end//
@implementation RGB
{
? ? unsignedcharred,green,blue;
}
/*? ? ? .........? ? ? ? ? ? */
@end//
這樣定義類后为居,將實(shí)例變量放在了實(shí)現(xiàn)文件中碑隆,因此即便外部文件拿到了接口,也不清楚類中定義了哪些實(shí)例變量孕惜,同時(shí)子類也無法訪問父類的實(shí)例變量了,可以通過@property 訪問愿阐,這是一種更高層次的封裝微服。
在實(shí)現(xiàn)文件中添加實(shí)例變量是外部不可見的。
讓變量對外不可見有二種方法缨历,一種是把變量的可見屬性設(shè)為@private以蕴,另一種是將變量定義在實(shí)現(xiàn)文件糙麦,第二種封裝性更好,也更清楚地表明這些變量是不可見的
實(shí)現(xiàn)文件中定義的實(shí)例變量可見性默認(rèn)為@private丛肮。也可以用@public和@protected來重設(shè)可見性喳资。
4.5 類對象
4.5.1 什么是類對象
面向?qū)ο笳Z言對類有二種認(rèn)識(shí),一種是把類作為類型的定義腾供,程序運(yùn)行時(shí)不做為實(shí)例存在仆邓;
另一種是人為類本身也是一種實(shí)例。后一種定義的類的對象叫做類對象伴鳖,這樣類定義就分為了二種情況节值,一種是定義所生成的實(shí)例的類型,一種是定義了自身的行為榜聂,OC和smalltalk中將類作為對象看待搞疗,在C++中,類只被作為類型定義使用须肆。
類對象有自己的方法和變量匿乃,分別被稱為類方法和類變量,類的實(shí)例變量和方法叫做實(shí)例變量和實(shí)例方法以作區(qū)分豌汇。
OC中只有類方法的概念幢炸,沒有類變量,OC中類對象稱為factory拒贱。類方法稱為factory method宛徊。
類方法中的一個(gè)典型就是創(chuàng)建類的實(shí)例對象。類對象收到alloc消息后會(huì)生成類的實(shí)例逻澳。
給類名發(fā)送alloc消息來生成類的實(shí)例變量調(diào)用的是類方法闸天。
類對象是程序執(zhí)行時(shí)自動(dòng)生成,每個(gè)類只有一個(gè)類對象斜做,不需要手動(dòng)生成苞氮,所有類都繼承NSObject,所以不需要考慮alloc生成類對象的問題瓤逼。
4.5.2 類對象的類型
所有類對象都是Class 類型笼吟,Class 和 id 都是指針類型,只是一個(gè)地址不需要了解實(shí)際指向的內(nèi)容抛姑。
Nil表示空指針(Class赞厕,而不是對象),實(shí)際的值是0
NSObject 中定義了類方法class 定硝,所有的類都可以使用這個(gè)方法獲取類對象
Class theClass = flag ? [Volume class] : [Volume class];
id v = [? ? [theClass alloc]initWithMin:0 max:100 step:5]????];
類名只有在類型定義才能使用毫目,否則都是錯(cuò)的
比如 Class? ? ?theClass =? flag蔬啡?Volume:MuteVolume诲侮;
NSObject中有一個(gè)實(shí)例方法 isMemberOfClass:。這個(gè)方法的參數(shù)是類對象箱蟆,返回的是BOOL值沟绪,確定調(diào)用方法的對象是否是該類的成員
BOOL isMember = [someobj? ? ismemberofClass:[Volume? ? class]];
BOOL isMember =[someobj? ? ismemberofClass:Volume]; 是錯(cuò)誤的
4.5.3類方法的定義
實(shí)例方法在接口聲明和實(shí)現(xiàn)文件都以? “-”開頭,類方法則以 “+”開頭空猜,alloc定義如下
+ (id)alloc绽慈;
類方法的方法名可以和實(shí)例方法和實(shí)例變量的名字一樣
繼承的情況下,子類可以訪問父類的類方法辈毯。
類方法不能訪問類中定義的實(shí)例變量和實(shí)例方法坝疼。
類方法在執(zhí)行self代表了類對象自身。
調(diào)用父類的類方法時(shí)谆沃,使用super钝凶。
4.5.4 類變量
OC不支持類變量,OC通過在實(shí)現(xiàn)文件中定義靜態(tài)變量的方法來代替類變量唁影。
給函數(shù)或變量加上static修飾符耕陷,他們的作用域就會(huì)變?yōu)橹辉谄渌诘奈募行В瑂tatic用來定義一些不想對外公開的函數(shù)和變量据沈。
OC在實(shí)現(xiàn)文件中定義了靜態(tài)變量哟沫,該變量的作用域就只在該文件有效,也就是只有類的類方法和實(shí)例方法可以訪問這個(gè)變量锌介。
這種方法同樣存在問題南用,在繼承的情況下子類該如何訪問static。
static的作用域不允許子類訪問父類的靜態(tài)變量掏湾。
可以通過定義類方法來解決這個(gè)問題裹虫,給靜態(tài)變量定義訪問和設(shè)置類方法。
4.5.5 類對象的初始化
Objective-C中實(shí)例對象的生成分為二步融击,第一是通過alloc為對象分分配內(nèi)存筑公,第二是對內(nèi)存進(jìn)行初始化(賦值操作)。
類對象也一樣尊浪,但類對象是在程序執(zhí)行時(shí)生成的所以無法發(fā)送消息來進(jìn)行初始化匣屡。Objective-C的根類NSObject中存在一個(gè) initialize類方法,可以通過使用這個(gè)方法來為各類對象進(jìn)行初始化拇涤。在每個(gè)類接收到消息之前捣作,為這個(gè)類調(diào)用一次initialize,調(diào)用之前要先調(diào)用父類的initialize方法鹅士,每個(gè)類的initialize方法只被調(diào)用一次券躁。
因?yàn)樵诔跏蓟倪^程中會(huì)自動(dòng)調(diào)用父類的initalize方法,所以子類的initialize方法不用顯式調(diào)用父類的initialize方法。
#import
#include
@interface A:NSObject
+ (void)initialize;
@end//
@implementation A
+ (void)initialize{
? ? printf("i am A");
}
@end//
@interface B:A
+ (void)initialize;
+ (void)setMessage:(constchar*)msg;
- (void)sayHello;
@end//
staticconstchar*myMessage ="Hello";
@implementation B
+ (void)initialize{
? ? printf("i? ? am? ? B");
}
+ (void)setMessage:(constchar*)msg{
? ? myMessage= msg;
}
- (void)sayHello{
? ? printf("%s",myMessage);
}
@end//
intmain()
{
? ? idobj = [[Balloc]init];
? ? [objsayHello];
? ? [B setMessage:"Have a good day"];
? ? [objsayHello];
}
在對類發(fā)送消息之前也拜,父類A和子類B的initalize就已經(jīng)被調(diào)用?
I? ? am? ? A
I? ? am? ? B
say? ? Hello
Have? ? a? ? good? ? day
該程序?qū)yMessage作為類變量使用以舒,通過setter修改類方法,修改后所有類B的實(shí)例對象的myMessage都會(huì)改變慢哈。
如果類中沒有實(shí)現(xiàn)initalize方法蔓钟,其父類就會(huì)被調(diào)用二次,面向自己一次卵贱,面向子類一次滥沫,所以要確保initalize方法能夠被調(diào)用二次,模板如下:
+ (void)initialize{
? ? staticBOOLnomore =NO;
? ? if(nomore){
? ? ? ? return;
? ? }
? ? ? ? printf("i? ? am? ? B");
? ? nomore =YES;
}
通過調(diào)用initalize進(jìn)行類對象的初始化操作键俱,但并非每個(gè)類都需要initalize方法兰绣,如果沒有任何需要初始化的對象,則只需要對類對象發(fā)送一個(gè)self消息就行方妖。self定義在NSObject中狭魂,只返回消息接受者自身,沒有多余操作党觅。
4.5.6 初始化方法的返回值
上一章的類Volume的初始化方法的返回值被定義為了id類型雌澄,那么為什么不用 Volume * 類型作為返回值呢,這是不一定的杯瞻,取決于環(huán)境镐牺,派生類MuteVolume的對象發(fā)送初始化消息后,返回的是Volume類型的對象而不是MuteVolume魁莉,所以一般情況下初始化方法的返回值都被設(shè)置為id類型睬涧。
在繼承存在的環(huán)境下,我們要盡可能避免使用靜態(tài)類型把代碼寫“死”旗唁,要增加代碼的彈性畦浓。
[[Volume? ? alloc]? ? initWithMin:a? ? max:b? ? step:s];
[[[self? ? class]? ? alloc]? ? initWithMin:a? ? max:b? ? step:s];
這樣修改后子類也可以原封不動(dòng)的使用這行代碼