以下文章翻譯自Ian Joyner所著的
《C++?? A Critique of C++ and Programming and Language Trends of the 1990s》
3/E【Ian Joyner 1996】
該篇文章已經(jīng)包含在Ian Joyner所寫(xiě)的《Objects Unencapsulated 》一書(shū)中(目前已經(jīng)有了日文的翻譯版本),該書(shū)的介紹可參見(jiàn)于:
http://www.prenhall.com/allbooks/ptr_0130142697.html
http://efsa.sourceforge.net/cgi-bin/view/Main/ObjectsUnencapsulated
http://www.accu.org/bookreviews/public/reviews/o/o002284.htm
虛擬函數(shù)
在所有對(duì)C++的批評(píng)中,虛擬函數(shù)這一部分是最復(fù)雜的。這主要是由于C++中復(fù)雜的機(jī)制所引起的。雖然本篇文章認(rèn)為多態(tài)(polymorphism)是實(shí)現(xiàn)面向?qū)ο缶幊蹋∣OP)的關(guān)鍵特性,但還是請(qǐng)你不要對(duì)此觀點(diǎn)(即虛擬函數(shù)機(jī)制是C++中的一大敗筆)感到有什么不安,繼續(xù)看下去,如果你僅僅想知道一個(gè)大概的話,那么你也可以跳過(guò)此節(jié)。【譯者注:建議大家還是看看這節(jié)會(huì)比較好】
在C++中,當(dāng)子類(lèi)改寫(xiě)/重定義(override/redefine)了在父類(lèi)中定義了的函數(shù)時(shí),關(guān)鍵字virtual使得該函數(shù)具有了多態(tài)性,但是
virtual關(guān)鍵字也并不是必不可少的(只要在父類(lèi)中被定義一次就行了)。編譯器通過(guò)產(chǎn)生動(dòng)態(tài)分配(dynamic
dispatch)的方式來(lái)實(shí)現(xiàn)真正的多態(tài)函數(shù)調(diào)用。
這樣,在C++中,問(wèn)題就產(chǎn)生了:如果設(shè)計(jì)父類(lèi)的人員不能預(yù)見(jiàn)到子類(lèi)可能會(huì)改寫(xiě)哪個(gè)函數(shù),那么子類(lèi)就不能使得這個(gè)函數(shù)具有多態(tài)性。這對(duì)于C++來(lái)說(shuō)是一個(gè)很?chē)?yán)重的缺陷,因?yàn)樗鼫p少了軟件組件(software
components)的彈性(flexibility),從而使得寫(xiě)出可重用及可擴(kuò)展的函數(shù)庫(kù)也變得困難起來(lái)。
C++同時(shí)也允許函數(shù)的重載(overload),在這種情況下,編譯器通過(guò)傳入的參數(shù)來(lái)進(jìn)行正確的函數(shù)調(diào)用。在函數(shù)調(diào)用時(shí)所引用的實(shí)參類(lèi)型必須吻合被重載的函數(shù)組(overloaded
functions)中某一個(gè)函數(shù)的形參類(lèi)型。重載函數(shù)與重寫(xiě)函數(shù)(具有多態(tài)性的函數(shù))的不同之處在于:重載函數(shù)的調(diào)用是在編譯期間就被決定了,而重寫(xiě)函數(shù)的調(diào)用則是在運(yùn)行期間被決定的。
當(dāng)一個(gè)父類(lèi)被設(shè)計(jì)出來(lái)時(shí),程序員只能猜測(cè)子類(lèi)可能會(huì)重載/重寫(xiě)哪個(gè)函數(shù)。子類(lèi)可以隨時(shí)重載任何一個(gè)函數(shù),但這種機(jī)制并不是多態(tài)。為了實(shí)現(xiàn)多態(tài),設(shè)計(jì)父類(lèi)的程序員必須指定一個(gè)函數(shù)為virtual,這樣會(huì)告訴編譯器在類(lèi)的跳轉(zhuǎn)表(class
jump
table)【譯者竊以為是vtable,即虛擬函數(shù)入口表】中建立一個(gè)分發(fā)入口。于是,對(duì)于決定什么事情是由編譯器自動(dòng)完成,或是由其他語(yǔ)言的編譯器自動(dòng)完成這個(gè)重任就放到了程序員的肩上。這些都是從最初的C++的實(shí)現(xiàn)中繼承下來(lái)的,而和一些特定的編譯器及聯(lián)結(jié)器無(wú)關(guān)。
對(duì)于重寫(xiě),我們有著三種不同的選擇,分別對(duì)應(yīng)于:“千萬(wàn)別”,“可以”及“一定要”重寫(xiě):
1、重寫(xiě)一個(gè)函數(shù)是被禁止的。子類(lèi)必須使用已有的函數(shù)。
2、函數(shù)可以被重寫(xiě)。子類(lèi)可以使用已有的函數(shù),也可以使用自己寫(xiě)的函數(shù),前提是這個(gè)函數(shù)必須遵循最初的界面定義,而且實(shí)現(xiàn)的功能盡可能的少及完善。
3、函數(shù)是一個(gè)抽象的函數(shù)。對(duì)于該函數(shù)沒(méi)有提供任何的實(shí)現(xiàn),每個(gè)子類(lèi)都必須提供其各自的實(shí)現(xiàn)。
父類(lèi)的設(shè)計(jì)者必須要決定1和3中的函數(shù),而子類(lèi)的設(shè)計(jì)者只需要考慮2就行了。對(duì)于這些選擇,程序語(yǔ)言必須要提供直接的語(yǔ)法支持。
選項(xiàng)1:
C ++并不能禁止在子類(lèi)中重寫(xiě)一個(gè)函數(shù)。即使是被聲明為private
virtual的函數(shù)也可以被重寫(xiě)。【Sakkinen92】中指出了即使在通過(guò)其他方法都不能訪問(wèn)到private virtual函數(shù),子類(lèi)也可以對(duì)其進(jìn)行重寫(xiě)。
如下所示,將輸出:class B
#include <stdio.h>
class A
{
private:
virtual void f()
{
printf("class A\n");
}
public:
void call_f()
{
f();
}
};
class B : public A
{
public:
void f()
{
printf("class B\n");
}
};
int main()
{
B b;
A* a = &b;
a->call_f();
return 0;
}
實(shí)現(xiàn)這種選擇的唯一方法就是不要使用虛擬函數(shù),但是這樣的話,函數(shù)就等于整個(gè)被替換掉了。首先,函數(shù)可能會(huì)在無(wú)意中被子類(lèi)的函數(shù)給替換掉。在同一個(gè)scope中重新宣告一個(gè)函數(shù)將會(huì)導(dǎo)致名字沖突(name
clash);編譯器將會(huì)就此報(bào)告出一個(gè)“duplicate
declaration”的語(yǔ)法錯(cuò)誤。允許兩個(gè)擁有同名的實(shí)體存在于同一個(gè)scope中將會(huì)導(dǎo)致語(yǔ)義的二義性(ambiguity)及其他問(wèn)題(可參見(jiàn)于 name
overloading這節(jié))。
下面的例子闡明了第二個(gè)問(wèn)題:
class A
{
public:
void nonvirt();
virtual void virt();
};
class B : public A
{
public:
void nonvirt();
void virt();
};
A a;
B b;
A *ap = &b;
B *bp = &b;
bp->nonvirt(); file://calls B::nonvirt as you would eXPect
ap->nonvirt(); file://calls A::nonvirt even though this object is of type B
ap->virt(); file://calls B::virt, the correct version of the routine for B
objects
在這個(gè)例子里,B擴(kuò)展或替換掉了A中的函數(shù)。B::nonvirt是應(yīng)該被B的對(duì)象調(diào)用的函數(shù)。在此處我們必須指出,C++給客戶端程序員(即使用我們這套繼承體系架構(gòu)的程序員)足夠的彈性來(lái)調(diào)用A::nonvirt或是B::nonvirt,但我們也可以提供一種更簡(jiǎn)單,更直接的方式:提供給A::
nonvirt和B::nonvirt不同的名字。這可以使得程序員能夠正確地,顯式地調(diào)用想要c調(diào)用的函數(shù),而
不是陷入了上面的那種晦澀的,容易導(dǎo)致錯(cuò)誤的陷阱中去。
具體方法如下:
class B: public A
{
public:
void b_nonvirt();
void virt();
}
B b;
B *bp = &b;
bp->nonvirt(); file://calls A::nonvirt
bp->b_nonvirt(); file://calls B::b_nonvirt
現(xiàn)在,B的設(shè)計(jì)者就可以直接的操縱B的接口了。程序要求B的客戶端(即調(diào)用B的代碼)能夠同時(shí)調(diào)用A::nonvirt和B::nonvirt,這點(diǎn)我們也做到了。就Object-Oriented
Design(OOD)來(lái)說(shuō),這是一個(gè)不錯(cuò)的做法,因?yàn)樗峁┝私训慕涌诙x(strongly defined
interface)【譯者認(rèn)為:即不會(huì)引起調(diào)用歧義的接口】。C++允許客戶端程序員在類(lèi)的接口處賣(mài)弄他們的技巧,借以對(duì)類(lèi)進(jìn)行擴(kuò)展。在上例中所出現(xiàn)的就是設(shè)計(jì)B的程序員不能阻止其他程序員調(diào)用A::nonvirt。類(lèi)B的對(duì)象擁有它們自己的nonvirt,但是即便如此,B的設(shè)計(jì)者也不能保證通過(guò)B的接口就一定能調(diào)用到正確版本的nonvirt。
C++同樣不能阻止系統(tǒng)中對(duì)其他處的改動(dòng)不會(huì)影響到B。假設(shè)我們需要寫(xiě)一個(gè)類(lèi)C,在C
中我們要求nonvirt是一個(gè)虛擬的函數(shù)。于是我們就必須回到A中將nonvirt改為虛擬的。但這又將使得我們對(duì)于B::nonvirt所玩弄的技巧又失去了作用(想想看,為什么:D)。對(duì)于C需要一個(gè)virtual的需求(將已有的nonvirtual改為virtual)使得我們改變了父類(lèi),這又使得所有從父類(lèi)繼承下來(lái)的子類(lèi)也相應(yīng)地有了改變。這已經(jīng)違背了OOP擁有低耦合的類(lèi)的理由,新的需求,改動(dòng)應(yīng)該只產(chǎn)生局部的影響,而不是改變系統(tǒng)中其他地方,從而潛在地破壞了系統(tǒng)的已有部分。
另一個(gè)問(wèn)題是,同樣的一條語(yǔ)句必須一直保持著同樣的語(yǔ)義。例如:對(duì)于諸如a->f()這樣的多態(tài)性語(yǔ)句的解釋?zhuān)到y(tǒng)調(diào)用的是由最符合a所真正指向類(lèi)型的那個(gè)f(),而不管對(duì)象的類(lèi)型到底是A,還是A的子類(lèi)。然而,對(duì)于C++的程序員來(lái)說(shuō),他們必須要清楚地了解當(dāng)f()被定義成virtual或是non-virtual時(shí),a->f()的真正涵義。所以,語(yǔ)句a->f()不能獨(dú)立于其實(shí)現(xiàn),而且隱藏的實(shí)現(xiàn)原理也不是一成不變的。對(duì)于f()聲明的一次改變將會(huì)相應(yīng)地改變調(diào)用它時(shí)的語(yǔ)義。與實(shí)現(xiàn)獨(dú)立意味著對(duì)于實(shí)現(xiàn)的改變不會(huì)改變語(yǔ)句的語(yǔ)義,或是執(zhí)行的語(yǔ)義。
如果在聲明中的改變導(dǎo)致相應(yīng)的語(yǔ)義改變,編譯器應(yīng)該能檢測(cè)到錯(cuò)誤的產(chǎn)生。程序員應(yīng)該在聲明被改變的情況下保持語(yǔ)義的不變。這反映了軟件開(kāi)發(fā)中的動(dòng)態(tài)特性,在其中你將能發(fā)現(xiàn)程序文本的永久改變。
其他另一個(gè)與a->f()相應(yīng)的,語(yǔ)義不能被保持不變的例子是:構(gòu)造函數(shù)(可參考于C++ ARM, section 10.9c, p
232)。而Eiffel和Java則不存在這樣的問(wèn)題。它們中所采用的機(jī)制簡(jiǎn)單而又清晰,不會(huì)導(dǎo)致C++中所產(chǎn)生的那些令人吃驚的現(xiàn)象。在Java中,所有的方法都是虛擬的,為了讓一個(gè)方法【譯者注:對(duì)應(yīng)于C++的函數(shù)】不能被重寫(xiě),我們可以用final修飾符來(lái)修飾這個(gè)方法。
Eiffel允許程序員指定一個(gè)函數(shù)為frozen,在這種情況下,這個(gè)函數(shù)就不能在子類(lèi)中被重寫(xiě)。
選項(xiàng)2:
是使用現(xiàn)有的函數(shù)還是重寫(xiě)一個(gè),這應(yīng)該是由撰寫(xiě)子類(lèi)的程序員所決定的。在C++中,要想擁有這種能力則必須在父類(lèi)中指定為virtual。對(duì)于OOD來(lái)說(shuō),你所決定不想作的與你所決定想作的同樣重要,你的決定應(yīng)該是越遲下越好。這種策略可以避免錯(cuò)誤在系統(tǒng)前期就被包含進(jìn)去。你作決定越早,你就越有可能被以后所證明是錯(cuò)誤的假設(shè)所包圍;或是你所作的假設(shè)在一種情況下是正確的,然而在另一種情況下卻會(huì)出錯(cuò),從而使得你所寫(xiě)出來(lái)的軟件比較脆弱,不具有重用性(reusable)【譯者注:軟件的可重用性對(duì)于軟件來(lái)說(shuō)是一個(gè)很重要的特性,具體可以參考
《Object-Oriented Software Construct》中對(duì)于軟件的外部特性的敘述,P7, Reusability, Charpter 1.2
A REVIEW OF EXTERNAL FACTORS】。
C
++要求我們?cè)诟割?lèi)中就要指定可能的多態(tài)性(這可以通過(guò)virtual來(lái)指定),當(dāng)然我們也可以在繼承鏈中的中間的類(lèi)導(dǎo)入virtual機(jī)制,從而預(yù)先判斷某個(gè)函數(shù)是否可以在子類(lèi)中被重定義。
這種做法將導(dǎo)致問(wèn)題的出現(xiàn):如那些并非真正多態(tài)的函數(shù)(not actually
polymorphic)也必須通過(guò)效率較低的table技術(shù)來(lái)被調(diào)用,而不像直接調(diào)用那個(gè)函數(shù)來(lái)的高效【譯者注:在文章的上下文中并沒(méi)有出現(xiàn)not actually
polymorphic特性的確切定義,根據(jù)我的理解,應(yīng)該是聲明為polymorphic,而實(shí)際上的動(dòng)作并沒(méi)能體現(xiàn)polymorphic這樣的一種特性】。雖然這樣做并不會(huì)引起大量的花費(fèi)(overhead),但我們知道,在OO程序中經(jīng)常會(huì)出現(xiàn)使用大量的、短小的、目標(biāo)單一明確的函數(shù),如果將所有這些都累計(jì)下來(lái),也會(huì)導(dǎo)致一個(gè)相當(dāng)可觀的花費(fèi)。C++中的
政策是這樣的:需要被重定義的函數(shù)必須被聲明為virtual。糟糕的是,C++同時(shí)也說(shuō)了,
non-virtual函數(shù)不能被重定義,這使得設(shè)計(jì)使用子類(lèi)的程序員就無(wú)法對(duì)于這些函數(shù)擁有自己的控制權(quán)。【譯者注:原作中此句顯得有待推敲,原文是這樣寫(xiě)的:it
says that non-virtual routines cannot be redefined, 我猜測(cè)作者想表達(dá)的意思應(yīng)該是:If you have
defined a non-virtual routine in base, then it cannot be virtual in the base
whether you redefined it as virtual in descendant.】
Rumbaugh等人對(duì)于C++中的虛擬機(jī)制的批評(píng)如下:C++擁有了簡(jiǎn)單實(shí)現(xiàn)繼承及動(dòng)態(tài)方法調(diào)用的特性,但一個(gè)C++的數(shù)據(jù)結(jié)構(gòu)并不能自動(dòng)成為面向?qū)ο蟮摹7椒ㄕ{(diào)用決議(method
resolution)以及在子類(lèi)中重寫(xiě)一個(gè)函數(shù)操作的前提必須是這個(gè)函數(shù)/方法已經(jīng)在父類(lèi)中被聲明為virtual。也就是說(shuō),必須在最初的類(lèi)中我們就能預(yù)見(jiàn)到一個(gè)函數(shù)是否需要被重寫(xiě)。不幸的是,類(lèi)的撰寫(xiě)者可能不會(huì)預(yù)期到需要定義一個(gè)特殊的子類(lèi),也可能不會(huì)知道那些操作將要在子類(lèi)中被重寫(xiě)。這意味著當(dāng)子類(lèi)被定義時(shí),我們經(jīng)常需要回過(guò)頭去修改我們的父類(lèi),并且使得對(duì)于通過(guò)創(chuàng)建子類(lèi)來(lái)重用已有的庫(kù)的限制極為嚴(yán)格,尤其是當(dāng)這個(gè)庫(kù)的源代碼不能被獲得是更是如此。(當(dāng)然,你也可以將所有的操作都定義為virtual,并愿意為此付出一些小小的內(nèi)存花費(fèi)用于函數(shù)調(diào)用)【RBPEL91】
然而,讓程序員來(lái)處理virtual是一個(gè)錯(cuò)誤的機(jī)制。編譯器應(yīng)該能夠檢測(cè)到多態(tài),并為此產(chǎn)生所必須的、潛在的實(shí)現(xiàn)virtual的代碼。讓程序員來(lái)決定
virtual與否對(duì)于程序員來(lái)說(shuō)是增加了一個(gè)簿記工作的負(fù)擔(dān)。這也就是為什么C++只能算是一種弱的面向?qū)ο笳Z(yǔ)言(weak object-oriented
language):因?yàn)槌绦騿T必須時(shí)刻注意著一些底層的細(xì)節(jié)(low level details),而這些本來(lái)可以由編譯器自動(dòng)處理的。
在C++中的另一個(gè)問(wèn)題是錯(cuò)誤的重寫(xiě)(mistaken
overriding),父類(lèi)中的函數(shù)可以在毫不知情的情況下被重寫(xiě)。編譯器應(yīng)該對(duì)于同一個(gè)名字空間中的重定義報(bào)錯(cuò),除非編寫(xiě)子類(lèi)的程序員指出他是有意這么做的(即對(duì)于虛函數(shù)的重寫(xiě))。我們可以使用同一個(gè)名字,但是程序員必須清楚自己在干什么,并且顯式地聲明它,尤其是在將自己的程序與已經(jīng)存在的程序組件組裝成新的系統(tǒng)的情況下更要如此。除非程序員顯式地重寫(xiě)已有的虛函數(shù),否則編譯器必須要給我們報(bào)告出現(xiàn)了名字被聲明多處(duplicate
declaration)的錯(cuò)誤。然而,C++卻采用了Simula最初的做法,而這種方法到現(xiàn)在已經(jīng)得到了改良。其他的一些程序語(yǔ)言通過(guò)采用了更好的、更加顯式的方法,避免了錯(cuò)誤重定義的出現(xiàn)。
解決方法就是virtual不應(yīng)該在父類(lèi)中就被指定好。當(dāng)我們需要運(yùn)行時(shí)的動(dòng)態(tài)綁定時(shí),我們就在子類(lèi)中指定需要對(duì)某個(gè)函數(shù)進(jìn)行重寫(xiě)。這樣做的好處在于:對(duì)于具有多態(tài)性的函數(shù),編譯器可以檢測(cè)其函數(shù)簽名(function
signature)的一致性;而對(duì)于重載的函數(shù),其函數(shù)簽名在某些方面本來(lái)就不一樣。第二個(gè)好處表現(xiàn)在,在程序的維護(hù)階段,能夠清楚地表達(dá)程序的最初意愿。而實(shí)際上后來(lái)的程序員卻經(jīng)常要猜測(cè)先前的程序員是不是犯了什么錯(cuò)誤,選擇一個(gè)相同的名字,還是他本來(lái)就想重載這個(gè)函數(shù)。
在 Java中,沒(méi)有virtual這個(gè)關(guān)鍵字,所有的方法在底層都是多態(tài)的。當(dāng)方法被定義為static,
private或是final時(shí),Java直接調(diào)用它們而不是通過(guò)動(dòng)態(tài)的查表的方式。這意味著在需要被動(dòng)態(tài)調(diào)用時(shí),它們卻是非多態(tài)性的函數(shù),Java的這種動(dòng)態(tài)特性使得編譯器難以進(jìn)行進(jìn)一步的優(yōu)化。
Eiffel和Object
Pascal迎合了這個(gè)選項(xiàng)。在它們中,編寫(xiě)子類(lèi)的程序員必須指定他們所想進(jìn)行的重定義動(dòng)作。我們可以從這種做法中得到巨大的好處:對(duì)于以后將要閱讀這些程序的人及程序的將來(lái)維護(hù)者來(lái)說(shuō),可以很容易地找出來(lái)被重寫(xiě)的函數(shù)。因而選項(xiàng)2最好是在子類(lèi)中被實(shí)現(xiàn)。
Eiffel和Object Pascal都優(yōu)化了函數(shù)調(diào)用的方式:因?yàn)樗麄冎恍枰a(chǎn)生那些真正多態(tài)的函數(shù)的調(diào)用分配表的入口項(xiàng)。對(duì)于怎樣做,我們將會(huì)在global
analysis這節(jié)中討論。
選項(xiàng)3:
純虛函數(shù)這樣的做法迎合了讓一個(gè)函數(shù)成為抽象的,從而子類(lèi)在實(shí)例化時(shí)必須為其提供一個(gè)實(shí)現(xiàn)這樣的一個(gè)條件。沒(méi)有重寫(xiě)這些函數(shù)的任何子類(lèi)同樣也是抽象類(lèi)。這個(gè)概念沒(méi)有錯(cuò),但是請(qǐng)你看一看pure
virtual functions這一節(jié),我們將在那節(jié)中對(duì)于這種術(shù)語(yǔ)及語(yǔ)法進(jìn)行批判討論。
Java也擁有純虛方法(同樣Eiffel也有),實(shí)現(xiàn)方法是為該方法加上deffered標(biāo)注。
結(jié)論:
virtual
的主要問(wèn)題在于,它強(qiáng)迫編寫(xiě)父類(lèi)的程序員必須要猜測(cè)函數(shù)在子類(lèi)中是否有多態(tài)性。如果這個(gè)需求沒(méi)有被預(yù)見(jiàn)到,或是為了優(yōu)化、避免動(dòng)態(tài)調(diào)用而沒(méi)有被包含進(jìn)去的話,那么導(dǎo)致的可能性就是極大的封閉,勝過(guò)了開(kāi)放。在C++的實(shí)現(xiàn)中,virtual提高了重寫(xiě)的耦合性,導(dǎo)致了一種容易產(chǎn)生錯(cuò)誤的聯(lián)合。
Virtual是一種難以掌握的語(yǔ)法,相關(guān)的諸如多態(tài)、動(dòng)態(tài)綁定、重定義以及重寫(xiě)等概念由于面向于問(wèn)題域本身,掌握起來(lái)就相對(duì)容易多了。虛擬函數(shù)的這種實(shí)現(xiàn)機(jī)制要求編譯器為其在class中建立起virtual
table入口,而global analysis并不是由編譯器完成的,所以一切的重?fù)?dān)都?jí)涸诹顺绦騿T的肩上了。多態(tài)是目的,虛擬機(jī)制就是手段。Smalltalk,
Objective-C, Java和Eiffel都是使用其他的一種不同的方法來(lái)實(shí)現(xiàn)多態(tài)的。
Virtual是一個(gè)例子,展示了C
++在OOP的概念上的混沌不清。程序員必須了解一些底層的概念,甚至要超過(guò)了解那些高層次的面向?qū)ο蟮母拍睢irtual把優(yōu)化留給了程序員;其他的方法則是由編譯器來(lái)優(yōu)化函數(shù)的動(dòng)態(tài)調(diào)用,這樣做可以將那些不需要被動(dòng)態(tài)調(diào)用的分配(即不需要在動(dòng)態(tài)調(diào)用表中存在入口)100%地消除掉。對(duì)于底層機(jī)制,感興趣的應(yīng)該是那些理論家及編譯器實(shí)現(xiàn)者,一般的從業(yè)者則沒(méi)有必要去理解它們,或是通過(guò)使用它們來(lái)搞清楚高層的概念。在實(shí)踐中不得不使用它們是一件單調(diào)乏味的事情,并且還容易導(dǎo)致出錯(cuò),這阻止了軟件在底層技術(shù)及運(yùn)行機(jī)制下(參見(jiàn)并發(fā)程序)的更好適應(yīng),降低了軟件的彈性及可重用性。