關(guān)鍵字struct
是C++
繼承自C
語言的一項遺產(chǎn)。作為更加貼切的詞匯,class
被引入C++
,用來表現(xiàn)類。這個決策造成的結(jié)果是:一種語言提供了兩個關(guān)鍵字來表示完全一致的概念。在什么情況下應(yīng)該使用誰,社區(qū)內(nèi)并無定論,甚至C++
的發(fā)明者Bjarne Stroustrup
也無法給出毫不含糊的建議。
一種流行的看法
如果只是名字的差別,那么毫無疑問,在任何時候都應(yīng)該使用class
,畢竟它更直觀、準確。
很多C++
開發(fā)者可能并不同意這一點。從他們的邏輯和經(jīng)驗出發(fā),struct
仍然只應(yīng)該用來定義那些只有數(shù)據(jù),沒有行為的“類”—— 它們事實上是C
語言中的結(jié)構(gòu)體;而class
則應(yīng)該用來定義真正的類——那些有行為的家伙。甚至有團隊規(guī)定: 一旦你使用struct
來定義一個類型,則其就不應(yīng)該有任何行為;否則,就需要使用class
。
如果使用C++
開發(fā)一套庫,卻要提供一套可供C
語言調(diào)用的接口,這樣的規(guī)定是合理的。畢竟,C
語言是不認識超出自己理解范圍的任何C++
語法的。
如果不是基于此類目的,這種規(guī)定就是自找麻煩。一個類型是不是應(yīng)該有行為,是動態(tài)的。盡管最初你定義它的時候,它是以持有數(shù)據(jù)為主要目的。但你無法確保隨后你不會因為某種目的需要為其添加一個方法。
比如,最初的時候你定義了這樣一個數(shù)據(jù)結(jié)構(gòu),由于它是進程間通信的消息包,所以在你看來,它應(yīng)該是一個純粹的數(shù)據(jù)類——結(jié)構(gòu)體。于是你將其定義為:
struct PDU
{
time_t timestamp;
data_t today;
int value;
};
隨后你發(fā)現(xiàn),在使用的過程中,總是需要重復(fù)這樣的代碼:
PDU pdu;
pdu.timestamp = now();
pdu.today = today();
pdu.value = value;
其中,前兩個字段的設(shè)置方式是確定的,都是取系統(tǒng)的當前時間和日期。作為一個專業(yè)程序員,這種重復(fù)讓你無法忍受,所以你決定實現(xiàn)一個類似于C
語言的初始化函數(shù):
void init_pdu(PDU& pdu, int value)
{
pdu.timestamp = now();
pdu.today = today();
pdu.value = value;
}
然后,你就可以這樣來調(diào)用:
PDU pdu;
init_pdu(pdu, 5);
這是一種典型的C
語言處理手法。如果你是個更老練的C++
程序員,會選擇使用構(gòu)造函數(shù):
struct PDU
{
explicit PDU(int value)
: timespace(::now())
, today(::today())
, value(value)
{}
time_t timestamp;
data_t today;
int value;
};
這種做法沒有帶來任何副作用:沒有任何額外的內(nèi)存和性能開銷。甚至,嚴格來講,由于使用了初始化列表,它的性能還略微提高了。
除此之外,你還收獲了一些其它好處:
- 它的代碼和數(shù)據(jù)被毫無爭議、無法分割的放在了一起,有著更好的內(nèi)聚性和可理解性;
- 具備某種強制性:你永遠也不可能忘記調(diào)用構(gòu)造函數(shù);
- 客戶代碼更加的簡潔,直觀。比如:
PDU pdu(5);
基于這些理由,我們讓類型PDU
有了一個行為。按照之前的規(guī)定,我們需要把它從struct
改成class
。這似乎不難做到,但問題在于:為什么我們需要關(guān)注這件事情?如果從一開始它就是一個class
,哪怕它沒有任何行為,我們寶貴的時間和精力就不會被無謂的消耗。
這還不算完。當你將一個類型由struct
改為class
后,所有對其進行聲明的地方都需要做同步的修改。否則,編譯器將會發(fā)出警告。作為一個紀律嚴明的專業(yè)軟件公司的雇員,你被要求消除掉所有告警。于是,更多的精力被毫無價值的浪費了。
所以,以這種原則來區(qū)分struct
還是class
不會帶來任何好處,只會帶來一堆麻煩。于是,就不難得出這樣的結(jié)論:我們只應(yīng)該堅持使用其中一個。
問題是:哪一個?
接口定義時的差別
除了名字不同之外,class
和struct
唯一的差別是:默認可見性。這體現(xiàn)在定義時和繼承時。struct
在定義一個成員,或者繼承時,如果不指明,則默認為public
; 而class
則默認為 private
。
但這不是我要討論的重點,介紹語言的基礎(chǔ)特性并不是本文的目標,重點是這樣的差別會產(chǎn)生出不同的代碼。
比如,現(xiàn)在要定義一個純虛類,用兩個不同的關(guān)鍵字,會導(dǎo)致如下不同的結(jié)果:
class Interface
{
public:
virtual int invoke() = 0;
virtual ~Interface() {}
};
struct Interface
{
virtual int invoke() = 0;
virtual ~Interface() {}
};
兩者差別很小,你或許并不在意。但對我而言,一個純虛類,從邏輯上本來就是一個只有公開方法聲明、沒有實現(xiàn)細節(jié)的接口類。它所聲明的一切都應(yīng)該是公開的。在這樣的契約關(guān)系下,如果再通過public
指明其公開性,這屬于畫蛇添足。
懶惰的我討厭冗余,討厭重復(fù)。更何況從平衡和美感的角度看,那個橫立的public
就像潔白墻面上的一沫蚊子血,顯得格外刺眼。
繼承時的差別
而這并非故事的全部。struct
的默認公開性還體現(xiàn)在繼承時:像成員一樣,如果未指明,struct
對于父類的繼承默認為public
繼承。而class
則恰恰相反。這個規(guī)則本身沒有問題,問題在于我們?nèi)绾芜x擇。
作為一個有經(jīng)驗的C++
程序員,在至少百分之九十以上的情況下,都會使用公有繼承(對于很多C++程序員,這個比例是百分之百)。這就意味著,在絕大多數(shù)情況下(如果不是全部的話),我們都要一遍遍的書寫public
——這不是一個理性的選擇。(Typing does take time, doesn't it?
)
class Derived
: public Base1
, public Base2
, public Base3
{
// more code
};
而一旦我們換作使用struct
來定義一個類,則所有不必要的 public
聲明就自然省略:
struct Derived
: Base1
, Base2
, Base3
{
// more code
};
確實干凈多了,不是嗎?
實體類定義時的差別
實體類不同于接口類,往往存在私有數(shù)據(jù)(沒有數(shù)據(jù),只有實現(xiàn)的實體類也往往意味著壞味道,一些表現(xiàn)算法的策略類除外),而class
的默認私有性,讓這種場景成為它出彩的機會。
class Foo
{
int a;
double b;
public:
Foo(int);
void doSomething();
};
這個類把私有數(shù)據(jù)定義在前面,把公開方法定義在后面,所以可以利用 class
的默認私有性。
但這樣的定義布局并不只是順序上的差異。我們的認知習慣和閱讀順序決定了我們總是希望把更重要的、更希望人們了解的信息擺在一目了然的位置。而不是讓別人穿越重重迷霧才能找到自己的關(guān)注點。我們希望別人更容易理解我們的意圖,而不是試圖挑戰(zhàn)別人的智商和耐心。
所以,信息擺放的順序就成了一件有所謂的事情。你如果認為私有實現(xiàn)細節(jié)更為重要,那就把私有數(shù)據(jù)擺在前面。否則,就把公開方法置于前列。
對于把程序理解為“數(shù)據(jù)結(jié)構(gòu)+算法”的程序員,盡管正在使用面向?qū)ο蟮脑亍邦悺保瑓s依然會認為理解一個程序的前提是理解它的數(shù)據(jù)結(jié)構(gòu)。在這樣的價值驅(qū)動下,把私有數(shù)據(jù)擺在前面就是完全合情合理的。數(shù)年前,我就曾在一本C++
相關(guān)的書中讀到過這樣的建議。
但對于越來越確信信息隱藏對軟件之重要的我,則更傾向于認為公開接口才是了解一個模塊最關(guān)鍵的知識。當試圖去使用一個模塊時,我總是會優(yōu)先查看它的測試用例(如果有的話),然后再去看它的公開接口聲明。一般而言,這對于理解它能做什么,以及如何使用它已經(jīng)足夠;接口聲明處的私有元素反而會干擾我對一個模塊的理解。只有在好奇心的驅(qū)使下,我才會進一步去看看它的實現(xiàn)。
基于這樣的認知,當定義一個類的時候,我會把公開方法定義在前面。至于私有的內(nèi)容,我總是會竭盡所能的不希望引起人們的注意,寧愿付出一些代價,也要把它們藏到別人在類聲明里看不到的地方,更不會放在前面擾亂視聽。
所以,我仍然會選擇struct
來定義實體類。如下:
struct Foo
{
Foo(int);
void doSomething();
private:
int a;
double b;
};
保持一致
無論是接口,還是實現(xiàn)類;無論是一個類以行為為中心,還是以數(shù)據(jù)為中心,使用 struct
而不是 class
都會給你的編程帶來一定的便利。
基于某些原因——無論是遺留系統(tǒng)的阻力,還是個人偏好的牽引——在了解了這所有的一切之后,你可能仍然選擇使用class
。這沒有太大問題,畢竟你的程序你做主。
但需要強調(diào)的是,無論你喜歡class
還是struct
,都應(yīng)該堅持選擇其中一個,而不是混合使用(比如不要在定義數(shù)據(jù)類的時候使用 struct
,定義行為類的時候使用class
)。否則,在大量使用前導(dǎo)聲明的情況下,一旦某個使用struct
的類改為class
,或反過來,所有的前導(dǎo)聲明都需要做相應(yīng)修改。或許編譯器并不認為這種不一致是一種錯誤,但那些不斷騷擾你的警告亦會讓你不勝其煩。
總結(jié)
為什么要以這么大的篇幅討論struct
和class
?
首先,因為我所寫的所有C++
相關(guān)文章都會使用struct
來定義類(在實際項目中也一直如此)。如果不在這里給出說明,一些人可能會感到困惑。
其次是因為此問題在社區(qū)內(nèi)尚無定論,很多團隊在給出struct
和 class
的選擇問題時,給出的選擇理由并不成立。一次深入的討論對于社區(qū)是有價值的。
最后,通過這樣的討論,我希望闡述的并非結(jié)論,而是其背后所遵守價值觀和原則。這會有助于理解我其它文章的內(nèi)容。無論話題如何變化,我都會遵守一致的價值觀和原則。