(教學思路 C#之類九)抽象類和介面
今天我們來學習抽象類和介面,因為他們在很多方面有相似的用法,所以放到一堂課中講解,看到這個標題的時候,一定有的同學會說介面,沒類字呀,它也是類嗎?從本質上介面也是類,和抽象類一樣,介面也是一種特殊的類。
作為編程設計者在程序的開發中,設計抽象類和介面就是為了讓他們用來被繼承的,當一個項目經過需求分析後,一般正規的軟體開發公司會由項目經理或經驗豐富的開發人員,先根據需求和要實現的大部分功能來設計抽象類和介面,然後再將項目功能分發到開發人員或小組,具體來實現這些抽象類和介面,這樣做的好處就是,規範了成員的命名和參數類型,合併代碼時減少錯誤同時方便修改,代碼的可讀性更強,充分利用面向對象中的三大特性,今後大家在開發時團隊協作是避免不了的,所以定好各成員的名字和功能是非常重要的,VS中也提供了一個按成員名稱查找其運用位置的功能,如果我們的命名各有不同,可以想像一下,後期負責合併的人員將多麼的痛苦,而且項目運行後的維護,也將成為一個困難。
上面說的是定義抽象類和介面的一個重要的優點,當然很多人在最初開發時,無法靈活的設計出他們,也不知道在這個項目的什麼功能該設計抽象類的什麼成員,這就得靠日積月累的開發經驗來做基礎,同時做完一個項目後,要學會總結,有可能的話,應再次優化,將重複的功能或定義的代碼使用到繼承性和多態性,只有這樣才能不斷的進步,是不是有點像毛主席修改自己的著作的感覺,這樣才能成功。在學習的最開始,同學們只要能了解他們的定義和實現就可以了。
抽象類
在類五中我介紹過抽象類,提到過抽象類的存在是因為成員中有可能存在抽象成員,抽象成員就是只有定義而沒有具體實現的成員,如果一個類中含有了抽象成員,那麼這個類一定要定義成抽象類,抽象類或抽象成員都是使用abstract作為修飾符聲明的定義。那麼什麼是抽象成員呢?沒有實現又是什麼意思呢?
舉這樣一個例子,比如人類吃飯是個方法,但是因為全世界的人類太多,吃飯使用的工具也不一樣,所以我們可以把吃飯定義成一個抽象的方法,讓繼承人類的子類去自定義吃飯的動作,如果是中國人就用筷子吃,如果是西方人就用刀叉吃;再比如還是人類吃飯,因為根據信仰和習俗的不同,我們在定義人類時,就應該把人類所吃的食物定義成一個抽象屬性,當是繼承的人們是伊斯蘭教時,食物中要排除豬肉,當是佛教時,食物中要排除葷食等,當然了我們所定義的人類就應該也是抽象類了。
咦!繼承的人類的人們自己定義怎麼吃飯、吃什麼食物,讓我們回想起類八中當子類繼承父類時,用自己的定義來實現父類的成員,這不會就是重寫吧?答案是對,這就是重寫,我們寫一個抽象方法的定義和抽象屬性的定義,讓我們來看看具體的語法:
//抽象方法沒有方法體,只有定義,使用非private關鍵字修飾
abstract public/protected void Method([,]);
//抽象屬性get和set訪問器沒有定義,其實get和set訪問器也是二個方法,也可以說抽象屬性的get、set訪問器沒有方法體。有關屬性同學們可以到類二中溫習一下。
abstract public int I {get;set;}
abstract可以修飾類、方法、屬性、索引器及事件,也就是抽象類中可以包含五種成員:抽象嵌套類、抽象方法、抽象屬性、抽象索引器、抽象事件,事件將來會講解,其他四種成員我們都有講解,抽象就是只有給他們做定義,如返回值、參數個數、成員標識符、訪問修飾符等,具體這些成員要做什麼在抽象成員中沒有定義,現在我們來想一下,為什麼抽象成員中沒有常見的欄位、構造器等?回想一下,我們在學習構造方法時,說過一個欄位即使你不賦初值,在構造的過程時,編譯器也會給欄位賦一個初值,靜態欄位調用靜態構造方法方法,實例欄位調用實例構造方法,而且構造方法是默認存在的,而且有自己的定義,欄位會有值、構造方法被默認定義,他們都有實現,所以這兩個不能定義為抽象成員,析構方法也是如此,還有以後將要學習到的委託,都不能定義成抽象成員。
這時一定又有同學問到,上節課我們不是學到了重寫父類成員時,必須把這些成員規定為虛方法嗎?必須使用virtual關鍵字嗎?對你記得沒錯,其實沒有實現的成員就可以認為是虛成員,只是這時的虛成員使用了abstract來修飾,成為抽象成員。
抽象類無法實例化,也就是無法創建抽象類的對象,原因是抽象類中有可能存在抽象成員,抽象成員沒有設定所完成的功能,所以無法創建抽象類真正的具體的對象。這些抽象成員由繼承抽象類的子類來實現,如果子類不完全實現這些抽象成員,其自身又成為了抽象類,還需要子類的子類來實現這些抽象成員。
下面我做了一段代碼,使用了抽象屬性和抽象方法,並且用子類將抽象類實現,再次調用了覆寫的屬性和方法,這個例子沒有太複雜的代碼,定義了一個Ren的抽象類,其中包括信仰屬性和吃飯的方法,有繼承的子類如中國人類、西方人類來根據自己的需要自己定義他們的含義和使用方法,本例中我只寫了一個子類。當講解到介面時,我再舉個實際計算的例子。
1
abstractclassRen2
{3
//定義了一個只寫的抽象的信仰Belief屬性,只有set訪問器4
abstractpublicstringBelief5
{6
set;7
}8
//定義一個抽象的Eat方法9
abstractpublicvoidEat();10
}11
classChinese:Ren12
{13
//定義一個食物欄位。14
stringFoot;15
//定義一個標誌,當給屬性Belief賦值錯誤時,調用Eat方法時提示錯誤。16
boolbz=true;17
//定義一個信仰欄位18
stringbelief;19
//用override關鍵字來覆寫抽象類Ren的屬性,同時給實現本類的信仰欄位。20
publicoverridestringBelief21
{22
//使用set訪問器,判斷輸入的信仰的類型,同時給食物欄位賦值,23
//當用戶輸入不正確的信仰時,標誌為False,提示錯誤。24
set25
{26
switch(value)27
{28
case"佛教":29
belief=value;30
Foot="忌葷食";31
break;32
case"伊斯蘭教":33
belief=value;34
Foot="忌豬肉";35
break;36
case"其他":37
belief=value;38
Foot="無忌口";39
break;40
default:41
Console.WriteLine("信仰應填寫:佛教、伊斯蘭教或其他");42
bz=false;43
break;44
}45
}46
}47
publicoverridevoidEat()48
{49
if(bz==true)50
Console.WriteLine("我們信仰是{0},所以我們選擇食物{1}",belief,Foot);51
else52
Console.WriteLine("操作失敗!");53
}54
}55
classProgram56
{57
staticvoidMain(string[]args)58
{59
Renren=newChinese();60
Console.Write("請填寫信仰:");61
//用戶輸入同時給屬性賦值,判斷信仰的類型62
ren.Belief=Console.ReadLine();63
//執行Eat方法64
ren.Eat();65
}66
}
這時兩次運行的結果,在第59行中,我們來可以用Chinese類來實例對象,調用Chinese類中的類成員。在這個例子中,用到了屬性,可以看到在屬性中除了給欄位賦值,還可以調用任何的成員,回憶一些在系列中講解屬性的時候,我就曾經說過,屬性就是兩個方法,get讀取方法,set賦值方法。在學習介面的時候我會實現一個介面和抽象類結合的實例。
使用抽象類要注意到兩點:一,抽象類中如果包含抽象成員,一定要使用abstract關鍵字來修飾,同時繼承他的子類使用override關鍵字覆寫這個抽象類中所有的抽象成員,並且全部實現為有具體功能或意義的成員,如果繼承抽象類的子類沒有把父類的抽象成員全部實現,其本身也將成為另一個抽象類,那麼他的實現就必須還需要一個子類繼續實現。二,如果想用父類的對象調用子類從父類中繼承的成員和被子類覆寫的成員,就必須通過子類來實例父類的對象,抽象類本身因為可能有抽象的成員,所以C#中規定抽象類無法自身實例化對象,使用語法如例子59行,上一篇類八中最後一個例子是實例類通過子類來實例其對象的例子,如果你對父類通過子類來實現還是不清楚,可以參考上節課的例子理解一下。
在定義抽象類的時候,要讓抽象類包括儘可能多的共同代碼,定義儘可能少的數據,在一個以繼承關係形成的類結構中,最成功的設計是只有最後一個子類是具體的類,其他的父類或父類的父類都應該是抽象類或介面,下面我們來看什麼是介面。
介面
介面的定義使用interface關鍵字,介面的成員只有四種:屬性、方法、委託、索引器,比抽象類少了一種成員:類,原因就在於介面中不能定義任何有實現的成員,所有的成員都是虛成員、抽象的,無論類中有沒有定義,一定會有的就是編譯器默認的無參構造函數,它是有定義的,所以介面中不能含有類。同時介面的訪問修飾符是默認省略的public,而且不能把public加入到介面的成員中,定義成員只有返回值、成員標識符和參數。介面的命名首字母應為I,這是規範,目的是當一個類繼承了一個父類和多個介面時,能夠通過I字母,明顯的區分出介面和父類,下面定義了一個介面:
interface IJK
{
void Method(int a);
}
繼承介面的類必須完全的實現介面中的定義成員,要創建介面的對象和創建抽象類的對象一樣,應該使用完全實現其定義的子類來實例化對象。
下面是一個介面的簡單實例,代碼很少,主要是讓同學們了解它的基本用法,本例中定義了一個介面,和兩個繼承介面的類,介面定義了一個有關兩個整數的操作方法,參數使用了引用參數,有關引用參數的使用,同學們可以回顧一下系列類三,PF類繼承介面後,實現了一個把這兩個數進行平方的方法,JH類繼承介面後,實現了一個把這兩個數進行值交換的方法,在入口方法中,輸出結果。
1
2
//定義了一個介面3
interfaceITwoNum4
{5
//定義了一個帶兩個引用型參數的無返回值方法,6
//不許加public修飾符。7
voidtwonum(refinta,refintb);8
}9
//PF類繼承了介面10
classPF:ITwoNum11
{12
//定義了一個對參數進行平方的方法,實現了介面中的方法13
//實現介面的覆寫方法時,可以不使用override14
publicvoidtwonum(refinta,refintb)15
{16
a=a*a;17
b=b*b;18
}19
}20
//JH類繼承了介面21
classJH:ITwoNum22
{23
//定義了一個對參數進行值交換的方法,實現了介面中的方法24
//實現介面的覆寫方法時,可以不使用override25
publicvoidtwonum(refinta,refintb)26
{27
intc=0;28
c=a;29
a=b;30
b=c;31
}32
}33
classProgram34
{35
staticvoidMain(string[]args)36
{37
//定義了要進行運算的兩個數的值38
inta=4;39
intb=5;40
Console.WriteLine("兩個數分別是a=4和b=5.");41
42
//分別用子類來實例化介面的對象43
ITwoNumitwonum1=newJH();44
ITwoNumitwonum2=newPF();45
//通過對象調用實現了介面中方法的子類覆寫方法。46
itwonum1.twonum(refa,refb);47
Console.WriteLine("交換後兩個數分別是a={0}和b={1}.",a,b);48
itwonum2.twonum(refa,refb);49
Console.WriteLine("平方後兩個數分別是a={0}和b={1}.",a,b);50
}51
}52
本例中只用到了一個類實現一個介面,當一個類實現兩個以上的介面時,介面中也許會出現成員的標識符相同的情況,此時在繼承的類中很難去區分了,遇到這種情況可以使用介面名引用其介面成員的方法進行區分,比如:
1
//定義了兩個介面IA、IB,介面中都有叫做Method的同名方法。2
interfaceIA3
{4
voidMethod();5
}6
interfaceIB7
{8
voidMethod();9
}10
//使用多重介面同名實現11
classC:IA,IB12
{13
//對於明確指定介面的成員,不需要加修飾符14
//這些成員被默認為public15
voidIA.Method()16
{17
Console.WriteLine("我是介面IA中的方法");18
}19
//此時不做註明的將視為從另一個介面中繼承來的方法20
publicvoidMethod()21
{22
Console.WriteLine("我是介面IB中的方法");23
}24
}25
classProgram26
{27
staticvoidMain(string[]args)28
{29
//----調用介面IB中的Method方法有兩種。-----30
//1.通過子類本身創建對象31
Cc=newC();32
//再用對象c調用Method方法33
c.Method();34
Console.WriteLine();35
36
//2.通過對子類的實例,創建介面IB對對象B37
IBb=newC();38
//再用對象b調用Method方法39
b.Method();40
Console.WriteLine();41
42
43
//----調用介面IA中的Method方法有兩種。-----44
//因為子類中默認的Method方法是才IB中繼承來的,所以無法通過子類對象c直接引用Method方法45
//1.使用as關鍵字,將c重新轉型為介面IA,並且將轉型後的對象引用,指定給另外一個對象a46
IAa=casIA;47
//再用對象a調用介面IA中的方法Method,此時a中就含有了c中從的IA介面繼承來的方法Method。48
a.Method();49
Console.WriteLine();50
51
52
//2.原理同1,用強行轉換將c轉換為介面IA,並且將轉型後的對象引用,指定給另外一個對象a253
IAa2=(IA)c;54
//再用對象a2調用介面IA中的方法Method,此時a2中就含有了c中從的IA介面繼承來的方法Method。55
a2.Method();56
}57
}
從這個例子我們可以看出在一個類繼承一個以上的介面或類時,用","隔開,通常習慣將類放到":"後的第一位,其後才是繼承的介面。
學過了介面和抽象類後,一定有同學現在感到很疑惑,什麼時候要定義成抽象類、什麼情況下應該定義成介面,抽象類和介面除了在定義成員時的區別外(抽象類可以包括實現了的方法,而介面中必須全部是沒有實現的抽象方法),還有什麼呢?
首先我們要先搞清楚一條原則:類是對對象的抽象,抽象類是對類的抽象,而介面是對行為的抽象。什麼是對行為的抽象,比如我們家的防盜門,它除了是門以外,還有很多的功能,比如防寒、防盜,甚至可以起到美觀的作用,如果我們要將設計這個實例,就應該把門設計成抽象類,其中可以包括很多欄位、屬性,比如材質、大小等,將防寒設計成一個介面IH,防盜再設計成另外一個介面ID,然後再設計一個防盜門的類繼承門這個抽象類和兩個防寒防盜的介面,至於你家的防盜門就是防盜門這個子類的一個對象了,你家防盜門的材質、大小,如何防盜、防寒可以自己定義。那為什麼要把防寒、防盜設計成介面呢?因為可以防寒防盜的不僅有門,防寒有衣服、被子,防盜有窗戶、防盜器,只要在一個程序集中都可以用到這個介面。
看了我上一段的講述,一定有同學稍微明白了一點,但是同學們不要把抽象類和介面想成了兩個極端的分類,我們在定義的時候只要遵循這條規律就可以了,那就是:如果行為跨越不同類的對象,也就是功能較多的時候,可使用介面;對呀一些相似的類的對象,完全可以設計抽象類,然後繼承,不一定非得再設計一個介面。
下面我們來看這個實例,用到了介面、抽象類、子類相結合,定義了兩個介面,Iflay介面中定義了canflay方法簽名,Isay介面中定義了cansay方法簽名,定義了一個抽象類animal動物類,定義了其中的一個抽象方法eat方法簽名,又定義了一個抽象類Ren人類,繼承了抽象動物類和Isay介面,實現了Isay介面中定義了cansay方法,同時又定義了一個保護Protect的抽象方法,最後定義了兩個實例類分別是DMren動漫人物類繼承了人類和介面Iflay,和bird類繼承了動物抽象類和介面Iflay,這樣Dmren就擁有了四種方法:canflay、cansay、eat、Protect,除了已經實現了的cansay方法外,其他方法必須在Dmren類中完全的實現;bird類需要實現eat和canflay方法,代碼如下:
1
//定義了介面Iflay2
interfaceIflay3
{4
//定義了canflay的方法,所以能飛的都可以繼承5
//比如飛機、鳥、飛碟6
voidcanflay();7
}8
//定義了介面ISay9
interfaceISay10
{11
//定義了cansay的方法,所以能說話的都可以繼承12
//比如電視、mp3、會說話的鸚鵡13
voidcansay();14
}15
//定義了一個動物的抽象類16
abstractclassAnimal17
{18
//定義了一個吃東西的方法19
abstractpublicvoideat();20
}21
//定義了一個人類,繼承了抽象動物類和介面isay,22
abstractclassRen:Animal,ISay23
{24
//繼承介面的子類必須完全實現介面中的成員25
//實現了介面Isay中能說話的方法26
publicvoidcansay()27
{28
Console.WriteLine("我繼承了介面Isay,我能說話");29
}30
//再次定義一個保護的方法31
abstractpublicvoidprotect();32
}33
//定義一個動漫人物類,繼承了抽象Ren類和介面Iflay34
classDMren:Ren,Iflay35
{36
//定義類中的兩個欄位name、foot,提供給類中的方法37
stringname;38
stringfoot;39
//定義構造函數,其中有兩個參數,第一個的名字DMname,第二個是同名的foot40
//在構造函數中調用下面本類中的三個方法,和基類中的cansay方法。41
//創建構造函數就是為了在其他類中,創建對象時,無需在其他類中使用DMren類的對象調用的麻煩,可以一次性使用構造方法中的所有方法成員,42
//給欄位賦值使用到了this關鍵字,因為是同名變數。43
publicDMren(stringDMname,stringfoot)44
{45
this.foot=foot;46
name=DMname;47
canflay();48
eat();49
protect();50
cansay();51
}52
publicvoidcanflay()53
{54
Console.WriteLine("我繼承了介面Iflay,我是{0},我能飛",name);55
}56
//使用override關鍵字重寫抽象類中的abstract修飾的eat方法57
publicoverridevoideat()58
{59
Console.WriteLine("我是繼承了抽象類animal,我吃"+foot);60
}61
//使用override關鍵字重寫抽象類中的abstract修飾的Protect方法62
publicoverridevoidprotect()63
{64
Console.WriteLine("我是繼承了抽象類Ren,我能保護周圍的人");65
}66
}67
//定義了一個鳥類,繼承了動物抽象類和介面Iflay68
classbird:Animal,Iflay69
{70
stringname;71
stringfoot;72
//如動漫DMren類一樣使用構造方法,在創建對象的同時,一次性調用類中的所有要執行的方法和賦值73
publicbird(stringDMname,stringfoot)74
{75
this.foot=foot;76
name=DMname;77
canflay();78
eat();79
}80
publicoverridevoideat()81
{82
Console.WriteLine("我是繼承了抽象類animal,我吃"+foot);83
}84
publicvoidcanflay()85
{86
Console.WriteLine("我繼承了介面Iflay,我是{0},我能飛",name);87
}88
}89
classProgram90
{91
staticvoidMain(string[]args)92
{93
//創建DMren對象dm,同時給構造函數傳入兩個參數94
DMrendm=newDMren("大力水手","菠菜");95
Console.WriteLine();96
97
//創建bird對象b,同時給構造函數傳入兩個參數98
birdb=newbird("鴿子","玉米");99
}100
}101
結果如下:
本例中使用了構造函數和this關鍵字,在系列類四中有詳細的講解,對抽象類和介面的學習如果想更加的深入一定要有很強的基本功,同時還要學習設計模式,能夠把繼承用好就是好的程序架構,寫到這類系列就結束了,要想學好之後的課程一定要想把類的知識學紮實了,不要急於求成。
推薦閱讀:
※下雪了,怎麼拍才美?給你10種拍攝思路
※Nature:「解碼」腸道菌群為肥胖治療提供新思路
※成功緻富的思路
※奇門斷局的思路
※趙炳南組方思路探析