在所有的文字之前,我需要強(qiáng)調(diào)一下,我本人對structure typing持反對態(tài)度,所以就算文中的內(nèi)容“看起來很像”go的interface,讀者們也最好不要覺得我是在贊揚(yáng)go的interface。我比較喜歡的是haskell和rust的那種手法??上ust跟go一樣恨不得把所有的單詞都縮成最短,結(jié)果代碼寫出來連可讀性都沒有了,單詞都變成了符號。如果rust把那亂七八糟的指針設(shè)計(jì)和go的那種屎縮寫一起干掉的話,我一定會很喜歡rust的。同理,COM這個(gè)東西設(shè)計(jì)得真是太他媽正確了,簡直就是學(xué)習(xí)面向?qū)ο笫址ǖ淖罴逊独?,可惜COM在C++下面操作起來有點(diǎn)傻逼,于是很多人看見這個(gè)東西就呵呵呵了。
上一篇文章說這次要寫類成員函數(shù)和lambda的東西,不過我想了想,還是先把OO放前面,這樣順序才對。
我記得我在讀中學(xué)的時(shí)候經(jīng)常聽到的宣傳,是面向?qū)ο蟮淖龇ǚ浅7先祟惖乃季S習(xí)慣,所以人人喜歡,大行其道,有助于寫出魯棒性強(qiáng)的程序。如今已經(jīng)過了十幾年了,我發(fā)現(xiàn)網(wǎng)上再也沒有這樣的言論了,但是也沒有跟反C++的浪潮一樣拼命說面向?qū)ο筮@里不好那里不好要廢除——明顯人們還是覺得帶面向?qū)ο蟮恼Z言用起來還是比較爽的,不然也就沒有那么多人去研究,一個(gè)特別合用來寫functional programming的語言——javascript——是如何可以“模擬”面向?qū)ο笳Z言里面的常用操作——new、繼承和虛函數(shù)覆蓋了。
所以像面向?qū)ο筮@種定義特別簡單的東西,語法上應(yīng)該做不出什么坑的了。那今天的坑是什么呢?答案:是人。
動態(tài)類型語言里面的面向?qū)ο笳f實(shí)話我也不知道究竟好在哪里,對于這種語言那來講,只要做好functional programming的那部分,剩下的OO究竟要不要,純粹是一個(gè)語法糖的問題。在動態(tài)類型語言里面,一個(gè)類和一個(gè)lambda expression的差別其實(shí)不大。
那么靜態(tài)類型語言里面的面向?qū)ο笠趺纯创??首先我們要想到的一個(gè)是,凡是面向?qū)ο蟮恼Z言都支持interface。C++雖然沒有直接支持,但是他有多重繼承,我們只需要寫出一個(gè)純虛類出來,就可以當(dāng)interface用了。
在這里我不得不說一下C++的純虛類和interface的這個(gè)東西。假設(shè)一下我們有下面的C#代碼:
interface IButton{}
interface ISelectableButton : IButton{}
interface IDropdownButton : IButton{}
class CheckBox : ISelectableButton{}
class MyPowerfulButton : CheckBox, IDropdownButton
{
// 在這里我們只需要實(shí)現(xiàn)IDropdownButton里面比IButton多出來的那部分函數(shù)就夠了。
}
我們先不管GUI是不是真的能這么寫,我們就看看這個(gè)繼承關(guān)系就好了。這是一個(gè)簡單到不能再簡單的例子。意思就是我有兩種button的接口,我從一個(gè)實(shí)現(xiàn)里面擴(kuò)展出一個(gè)支持另一種button接口的東西。但是大家都知道,我那個(gè)完美的GacUI用的是C++,那么在C++下面會遇到什么問題呢:
#region 抱怨
一般來說在C++里面用純虛類來代替interface的時(shí)候,我們繼承一個(gè)interface用的都是virtual繼承。為什么呢?看上面那個(gè)例子,ISelectableButton繼承自IButton,IDropdownButton繼承自IButton。那么當(dāng)你寫一個(gè)MyPowerfulButton的時(shí)候,你希望那兩個(gè)接口里面各自的IButton是不一樣的東西嗎?這當(dāng)然不是。那如何讓兩個(gè)接口的IButton指向的是同一個(gè)東西呢?當(dāng)然就是用virtual繼承了。
好了,現(xiàn)在我們有CheckBox這個(gè)實(shí)現(xiàn)了ISelectableButton(帶IButton)的類了,然后我們開始寫MyPowerfulButton。會發(fā)生什么事情呢?
猜錯(cuò)了!答案是,其實(shí)我們可以寫,但是Visual C++(gcc什么的你們自己玩玩就好了)會給我們一個(gè)warning,大意就是你IDropdownButton里面的IButton被CheckBox給覆蓋了,再說抽象一點(diǎn)就是一個(gè)父類覆蓋了另一個(gè)父類的虛函數(shù)。這跟virtual繼承是沒關(guān)系的,你怎么繼承都會出這個(gè)問題。
但這其實(shí)也怪不了編譯器,本來在其他情況下,虛函數(shù)這么覆蓋自然是不好的,誰讓C++沒有interface這個(gè)概念呢。但是GUI經(jīng)常會碰到這種東西,所以我只好無可奈何地在這些地方用#pragma來supress掉這個(gè)warning,反正我知道我自己在干什么。
C++沒有interface的抱怨到這里就完了,但是virtual繼承的事情到這里還沒完。我再舉一個(gè)例子:
class A
{
private:
int i;
public:
A(int _i)i:(_i){}
};
class B : public virtual A
{
public:
B(int _i):A(_i){}
};
class C : public virtual A
{
public:
C(int _i):A(_i){}
};
class D : public B, public C
{
public:
D():B(1), C(2){}
};
大家都是知道什么是virtual繼承的,就是像上面這個(gè)例子,D里面只有一個(gè)A對象,B和C在D里面共享了A。那么,我們給B和C用了不同的參數(shù)來構(gòu)造,難道一個(gè)A對象可以用不同的參數(shù)構(gòu)造兩次嗎,還是說編譯器幫我們隨便挑了一個(gè)?
呵呵呵呵呵呵呵呵,我覺得C++的virtual繼承就是這里非常反直覺——但是它的解決方法是合理的。反正C++編譯器也不知道究竟要讓B還是C來初始化A,所以你為了讓Visual C++編譯通過,你需要做的事情是:
D()
: A(0) // 參數(shù)當(dāng)然是胡扯的,我只是想說,你在D里面需要顯式地給A構(gòu)造函數(shù)的參數(shù)
, B(1)
, C(2)
{
}
#endregion
大家估計(jì)就又開始吵了,C++干嘛要支持多重繼承和virtual繼承這兩個(gè)傻逼東西呢?我在想,對于一個(gè)沒有內(nèi)建interface機(jī)制的語言,你要是沒有多重繼承和virtual繼承,那用起來就跟傻逼一樣,根本發(fā)揮不了靜態(tài)類型語言的優(yōu)勢——讓interface當(dāng)contract。當(dāng)然,我基本上用多重繼承和virtual繼承也是用來代替interface的,不會用來做羞恥play的。
當(dāng)我們在程序里面拿到一個(gè)interface也好,拿到一個(gè)class也好,究竟這代表了一種什么樣的精神呢?interface和class的功能其實(shí)是很相似的
interface IA:只要你拿到了一個(gè)IA,你就可以對她做很多很多的事情了,當(dāng)然僅限大括號里面的!
class C : IA, IB:只要你拿到了一個(gè)C——哦不,你只能拿到interface不能拿到class的——反正意思就是,你可以對她做對IA和IB都可以做的事情了!
所以contract這個(gè)概念是很容易理解的,就是只要你跟她達(dá)成了contract,你就可以對她做這樣那樣的事情了。所以當(dāng)一個(gè)函數(shù)返回給你一個(gè)interface的時(shí)候,他告訴你的是,函數(shù)運(yùn)行完了你就可以做這樣那樣的事情。當(dāng)一個(gè)函數(shù)需要一個(gè)interface的時(shí)候,他告訴你的是,你得想辦法讓我(函數(shù))干這樣那樣的事情,我才會干活。
那class呢?class使用來實(shí)現(xiàn)interface的,不是給你直接用的。當(dāng)然這是一個(gè)很理想的情況,可惜現(xiàn)在的語言糖不夠甜,堅(jiān)持這么做的話實(shí)在是太麻煩了,所以只好把某些class也直接拿來用了,GUI的控件也只好叫Control而不是IControl了。
其實(shí)說到底class和interface有什么區(qū)別呢?我們知道面向?qū)ο蟮囊淮筇卣骶褪欠庋b,封裝的意思就是封裝狀態(tài)。什么是狀態(tài)呢?反正云風(fēng)一直在說的“類里面的數(shù)據(jù)”就不是狀態(tài)。我們先來看什么是數(shù)據(jù):
struct Point
{
int x;
int y;
};
這就是典型的數(shù)據(jù),你往x和y里面隨便寫什么東西都是沒問題的,反正那只是一個(gè)點(diǎn)。那什么是狀態(tài)呢:
struct String
{
wchar_t* buffer;
int length;
};
String和Point有什么不一樣呢?區(qū)別只有一個(gè):String的成員變量之間是滿足一個(gè)不變量的:wcslen(buffer) == length;
如果我們真的決定要給String加上這么個(gè)不變量的話,那這里面包含了兩點(diǎn):
1:buffer永遠(yuǎn)不是nullptr,所以他總是可以被wcslen(buffer)
2:length的值和buffer有直接的關(guān)系
如果你要表達(dá)一個(gè)空字符串,你總是可以寫buffer=L””,不過這就要你給String再加上一些數(shù)據(jù)來指明這個(gè)buffer需要如何被釋放了,不過這是題外話了。我們可以假設(shè)buffer永遠(yuǎn)是new[]出來的——反正這里不關(guān)心它怎么釋放。
這個(gè)不變量代表什么呢?意思就是說,無論你怎么折騰String,無論你怎么創(chuàng)建釋放String,這個(gè)等式是一定要滿足的。也就是說,作為String外部的“操作人員”,你應(yīng)當(dāng)沒機(jī)會“觀測”到這個(gè)String處于一個(gè)不滿足不變量的狀態(tài)。
所以這兩個(gè)成員變量都不應(yīng)該是public的。因?yàn)槟呐履鉷ublic了他們其中的一個(gè),你也會因?yàn)橥獠靠梢噪S意修改它而使他進(jìn)入一個(gè)不滿足不變量的狀態(tài)。
這代表了,為了操作這些成員變量,我們需要public一些函數(shù)來給大家用。其實(shí)這也是contract,String的成員函數(shù)告訴我們,你可以對我(String)做很多很多的事情哦!
這同時(shí)也代表了,我們需要一個(gè)構(gòu)造函數(shù)。因?yàn)槿绻覀冊趧?chuàng)建一個(gè)String之后,實(shí)例沒有被正確初始化,那么他就處于了一個(gè)不滿足不變量的狀態(tài),這就不滿足上面說的東西了。有些人喜歡帶一個(gè)Init函數(shù)和一個(gè)基本不干什么事情的構(gòu)造函數(shù),我想說的是,反正你構(gòu)造完了不Init都不能用,你為什么非要我每次創(chuàng)建它的時(shí)候都立刻調(diào)用Init這么多次一舉呢?而且你這樣會使得我無法對于一個(gè)這樣的函數(shù)f(shared_ptr<ClassThatNeedsInit> x)直接寫f(make_shared(new ClassThatNeedInit))因?yàn)槟愕臉?gòu)造函數(shù)是殘廢的!
有些人會說,init沒有返回值,我不知道他犯了錯(cuò)誤啊——你可以用Exception!
還有些人會說,exception safe的構(gòu)造函數(shù)好難寫啊——學(xué)啊我艸!
但是這樣仍然有些人會負(fù)隅頑抗,都這么麻煩了反正我可以用對Init和返回值就好了——你連exception safe的構(gòu)造函數(shù)都不知道怎么寫你怎么知道你可以“用對”它們?
#region 題外話展開
但是有些人就喜歡返回error,怎么辦呢?其實(shí)我們都很討厭Java那個(gè)checked exception的對吧,要拋什么exception都得在函數(shù)簽名里面寫,多麻煩啊。其實(shí)這跟error是一樣的。一個(gè)exception是可以帶有很多豐富的信息的——譬如說他的callstack什么的,還可以根據(jù)需要有很多其他的信息,總之不是一個(gè)int可以表達(dá)的。這就是為什么exception【通?!慷际且粋€(gè)類。那如果我們不能用exception,但是也要返回一樣多的信息怎么辦?你只好把函數(shù)的返回值寫得相當(dāng)?shù)膹?fù)雜,譬如說:
struct ErrorInfoForThisFunction
{
xxxxxxxx
};
template<typename R, typename E>
struct ReturnValue // C++沒有好用的tuple就是臥槽
{
bool hasError;
R returnValue;
E errorInfo;
};
ReturnValue<ReturnType, ErrorInfoForThisFunction> ThisFunction( ... ); //我知道因?yàn)樾畔?shí)在太多你們又要糾結(jié)返回struct還是它的指針還是ReturnValue里面的東西用指針還是用引用參數(shù)等等各種亂七八糟的事情了哈哈哈哈哈哈
于是現(xiàn)在出問題了,我有一個(gè)ThatFunction調(diào)用ThisFunction,當(dāng)錯(cuò)誤是一種原因的時(shí)候我可以處理,當(dāng)錯(cuò)誤是另一種原因的時(shí)候我無法處理,所以在這種情況下我有兩個(gè)選擇:
1:把錯(cuò)誤信息原封不斷的返回
2:把ThisFunction的錯(cuò)誤信息包裝成ThatFunction的錯(cuò)誤信息
不過我們知道其實(shí)這兩種方法都一樣,所以我們采用第一種:
struct ErrorInfoForThatFunction
{
yyyyyyyy
};
ReturnValue<ReturnType2, tuple<ErrorInfoForThisFunction, ErrorForThatFunctio, bool /*用來代表哪個(gè)是有效的*/> ThatFunction( ... ); //數(shù)據(jù)越來越多我知道你們會對返回值糾結(jié)的越厲害
你可能會說,為什么不把tuple包裝成另一個(gè)struct?其實(shí)都一樣,懶得寫了。
我們知道,通常一個(gè)常見的幾百人一起寫的小軟件都會有幾百上千萬行甚至幾十G代碼的,函數(shù)的調(diào)用層次只有幾十層都已經(jīng)很不錯(cuò)了。就算調(diào)用鏈里面只有10%的函數(shù)添加了自己的錯(cuò)誤信息,那累積到最后肯定會很壯觀的。而且只要底層的一個(gè)函數(shù)修改了錯(cuò)誤信息,所有直接間接調(diào)用它的函數(shù)都會受到影響。
這簡直就跟Java的checked exception一樣嘛!
有些人會說,我們有error code就夠了!我知道你們根本沒有好好想“怎么做error recovery”這件事情。
有些人還會說(就在我微博上看見的),用error code就是代表可以不處理,我干嘛要費(fèi)那么多心思搞你這些返回值的東西?我對這種人只能呵呵呵了,轉(zhuǎn)行吧……
這個(gè)時(shí)候我就會想,C++多好啊,我只要把ReturnValue<ReturnType, ErrorInfoForThisFunction>給改成ReturnType,然后在函數(shù)里面發(fā)生了錯(cuò)誤還是構(gòu)造一個(gè)ErrorInfoForThisFunction,然后直接給throw出來就好了。throw一個(gè)值我們還不用關(guān)心怎么釋放它,多省事。對于ErrorInfoForThatFunction,我還可以讓這兩個(gè)struct都繼承自同一個(gè)基struct(就是你們經(jīng)常在別的語言里面看見的Exception類了),這樣我在外面還可以直接catch(const 基struct& ex)。
有些人會說,為什么不強(qiáng)制所有繼承都繼承自Exception?我知道你們就只是想catch了之后不理罷了,反正C++也有catch(…)你們偷著樂就行了。
用Exception有性能問題?反正在不發(fā)生錯(cuò)誤的情況下,寫了幾句try也就只是操作了寫在FS : [ 0 ]里面的一個(gè)鏈表而已,復(fù)制幾個(gè)指針根本就不算什么影響。
C++的catch不能抓到Access Violation(也就是segmant fault?)?現(xiàn)在連最新的.net你來寫catch(Exception ex)也抓不到AccessViolationException了。都AV了你的內(nèi)存都搞得一團(tuán)糟了,如果你這個(gè)時(shí)候還不備份數(shù)據(jù)dump自己然后退出重啟(如果需要的話),那你接著執(zhí)行代碼,天知道會發(fā)生什么事情啊!連C#都覺得這么做危險(xiǎn)了,C++只能更危險(xiǎn)——所以用SEH抓下來dump自己然后進(jìn)程自殺吧。Java還區(qū)分Error和Exception,雖然我不知道他具體代表什么,反正一般來說Exception有兩種
1:可以預(yù)見的錯(cuò)誤,譬如說Socket斷開了所以Write就是敗了給個(gè)Exception之類的
2:必須修改代碼的錯(cuò)誤,譬如說數(shù)組下標(biāo)越界——這除了你寫錯(cuò)以外根本沒有別的原因,就應(yīng)該掛掉,這時(shí)候你debug的時(shí)候才能立刻知道,然后改代碼。
所以有三個(gè)基類然后最嚴(yán)重的那種不能catch我覺得也是一種好的選擇。你可能會問,那C#在AV之后你又抓不到那怎么知道呢?答案:Application類有一個(gè)事件就是在發(fā)生這類事情的時(shí)候被調(diào)用的,在里面dump就好了。如果你非要抓AV,那也可以抓得到,就是有點(diǎn)麻煩……
#endregion
說了這么多,無非就是因?yàn)橐粋€(gè)類的構(gòu)造函數(shù)——其實(shí)他真的是一個(gè)函數(shù),只是函數(shù)名和類名一樣了,這種事情在js里面反正經(jīng)常出現(xiàn)——強(qiáng)制了你只能返回正確的時(shí)候的結(jié)果,于是有些人沒辦法加入error code了,又不知道怎么正確使用exception,只好搞出個(gè)C語言的封建社會殘留思想Init函數(shù)來。其實(shí)我們知道,一旦有了Exception,函數(shù)簽名里面的返回值就是他正確執(zhí)行的時(shí)候返回的東西,這根構(gòu)造函數(shù)一樣。
C++的exception在構(gòu)造函數(shù)里面不好,其實(shí)是因?yàn)橐坏?gòu)造函數(shù)發(fā)生了異常,那代表這個(gè)類沒構(gòu)造完,所以析構(gòu)函數(shù)是不會執(zhí)行的。這在一定程度上給你寫一個(gè)正確的構(gòu)造函數(shù)(這也是“如何寫一個(gè)正確的類”的其中一個(gè)方面)帶來了麻煩,所以很多人到這里就呵呵呵了。
這就跟很多人學(xué)習(xí)SQL語言結(jié)果到group by這里就怎樣也跨不過去了一樣——人和人之間說沒有差距這個(gè)不符合客觀現(xiàn)實(shí)啊……
不過我不否認(rèn),想寫一個(gè)正確的C++程序是一件非常困難的事情,以至于連我在【只有我自己用的那部分library】里面都不是總是遵守各種各樣的規(guī)則,反正我寫的代碼,我知道怎么用。不過公司的代碼都是一大堆人一起寫的,就像sqlserver一個(gè)組有一千多人一樣(oracle是我們的十幾倍,我們還能活下來真是太不容易了)——你能每個(gè)人都溝通到嗎?撐死了就幾十個(gè)吧,才不到10%。天知道別人會在代碼里面干什么。所以寫代碼是不能太隨便的。同理,招人也不能太隨便,特別是你們這些連code review都不做的公司,你平時(shí)都不能阻止他checkin垃圾代碼,你還敢招不合格的人嗎?
現(xiàn)在我們回到面向?qū)ο蟮臇|西。Exception其實(shí)也應(yīng)該算在contract里面,所以其實(shí)interface里面的函數(shù)會拋什么異常是需要明確的表達(dá)出來的。但是checked exception這個(gè)東西實(shí)在是太蠢了,因?yàn)檫@個(gè)規(guī)則是不能組合的,會導(dǎo)致上面說的error返回值一樣的“接口信息大爆炸”。
所有不能組合的東西都是很難用的,譬如checked exception,譬如鎖,譬如第一篇文章說的C語言那個(gè)碉堡了的函數(shù)指針數(shù)組作為參數(shù)的一個(gè)成員函數(shù)指針類型的聲明什么的。
如果你不直接寫出這個(gè)函數(shù)會拋exception,那要怎么辦呢?方法有兩個(gè):
1:你給我把文檔寫好了,而且你,還有你,用這個(gè)library之前,給我RTFM!
2:就跟VisualStudio一樣支持xml注釋,這樣VS就可以在你調(diào)用這個(gè)函數(shù)的時(shí)候用tooltip的形式提示你,你需要注意這些那些事情……
什么?你不用IDE?給我RTFM!你連文檔都不看?滾!明天不要來上班了!
突然發(fā)現(xiàn)本來要寫面向?qū)ο蟮模Y(jié)果Exception也寫了相當(dāng)長的一段。這件故事告訴我們,就算你不知道interface as contract是什么意思,你還能湊合寫點(diǎn)能維護(hù)的代碼。但是你Exception用得不好,程序就寫成了渣,這個(gè)問題比較嚴(yán)重,所以寫的也就比較多了。所以下面我們真正來談contract的事情。需要注意的是,C++對這種東西是用你們很討厭的東西來支持的——模板和偏特化。
contract的概念是很廣泛的。對于面向?qū)ο笳Z言來說,int這種東西其實(shí)也可以是一個(gè)類。你們不要老是想著編譯后生成什么代碼的事情,語法這種東西只是障眼法而已,編譯出來的東西跟你們看到的可以是完全不同的。一個(gè)典型的例子就是尾遞歸優(yōu)化了。還有C#的int雖然繼承自object但是你直接用他的話生成出來的代碼跟C++是沒什么區(qū)別的——因?yàn)榫幾g器什么后門都可以開!
那我們就拿int來說吧。int有一個(gè)很重要的特征,就是可以用來比較。C++怎么表達(dá)這個(gè)事情的呢?
struct int
{
......
bool operator<(int i);
......
};
如果你想寫一個(gè)排序函數(shù),內(nèi)部想用<來排序的話,你不需要在接口上寫任何東西,你只需要假設(shè)那個(gè)typename T的T可以被<就好了。所有帶有<的類型都可以被這個(gè)函數(shù)使用。這特別的structure typing,而且C++沒有concept mapping,導(dǎo)致了你無法在接口上表達(dá)“這個(gè)類必須帶<”的這件事情,所以一旦你用錯(cuò)了,這錯(cuò)誤信息只能跟煙霧一般繚繞了……
concept mapping其實(shí)也是一個(gè)面向?qū)ο蟮奶貏e好用特征不過這太高級了估計(jì)很多人都沒用過——你們又不喜歡haskell和rust——那對于我們熟悉的面向?qū)ο蟮奶匦詠碇v,這樣的事情要怎么表達(dá)呢?
于是我們偉大的先知Anders Hejlsberg菊苣就做了這么個(gè)決定(不是他干的也是他手下干的?。?
interface IComparable // .net 1.0沒有模板,只好搞了這么個(gè)傻逼東西出來
{
int CompareTo(object o); //具體的忘了,這是我根據(jù)記憶隨便寫的
}
interface IComparable<T>
{
int CompareTo(T o);
}
struct Int32 : IComparable, IComarable<T> ...
{
......
}
所以你的排序函數(shù)只需要寫成:
void Sort<T>(T[] data)
where T : IComparable<T>
{ ...... }
看看IComparable<int>這個(gè)傻逼。我為什么要?jiǎng)?chuàng)建一個(gè)對象(IComparable<int>),他的職責(zé)就是跟另一個(gè)int作比較?這實(shí)在是太蠢了,無論怎么想都不能想出這種對象到底有什么存在的意義。不過因?yàn)镃#沒有concept mapping,于是看在interface as contract的份上,讓interface來干這種事情其實(shí)也是很合理的。
所以contract這個(gè)東西又賦予了一個(gè)更加清晰的意義了。我們其實(shí)想要表達(dá)的事情是“我們可以對這個(gè)參數(shù)做什么事情”,而不是“這個(gè)參數(shù)是什么類型”。所以在這個(gè)Sort函數(shù)里面,這個(gè)T其實(shí)不代表任何事情,真正起到聲明的作用的是where T : IComparable<T>這一句,指明了data數(shù)組里面的所有東西都是可以被排序的。那你可能會問,為什么不把IComparable<T>改成IComparable然后干脆把參數(shù)改成IComparable[] data呢?雖然說這樣做的確更加“面向?qū)ο?#8221;,但是實(shí)在是太不現(xiàn)實(shí)了……
本來面向?qū)ο筮@種概念就不是特別的可以表達(dá)客觀現(xiàn)實(shí),所以出現(xiàn)一些這種狀況,也是正常的。
#region 題外話
看看這兩個(gè)函數(shù):
void Sort<T>(T[] data) where T:IComparable<T>;
void Sort(IComparable[] data);
他們互相之間存在了一個(gè)特別優(yōu)美的(數(shù)學(xué)意義上的)變換,發(fā)現(xiàn)沒有,發(fā)現(xiàn)沒有!所以對于動態(tài)類型語言(interface可以從代碼里面得到)做一些“靜態(tài)化”的優(yōu)化的時(shí)候,就可以利用類似的技巧——咳咳,說太遠(yuǎn)了,趕緊打住。誰說懂點(diǎn)代數(shù)對編程沒用的?哼!
#endregion
在這里我們終于看到了contract在帶泛型的純潔的面向?qū)ο笳Z言里面的兩種表達(dá)方法。你可能會想,我想在java里面干這種事情怎么辦?還是換C#吧。那我們拿到一個(gè)class的時(shí)候,這代表什么呢?其實(shí)我們應(yīng)該看成,其實(shí)我們拿到的是一個(gè)interface,只是他恰好只有一個(gè)實(shí)現(xiàn)。所以在這種時(shí)候,你最好不要依賴于“這個(gè)interface恰好只有一種實(shí)現(xiàn)而且我知道他是什么”的這個(gè)事情,否則程序?qū)懘罅耍銜l(fā)現(xiàn)你越來越不滿足“面向interface編程”的這個(gè)原則,代碼越來越難處理了。
我們可能會想到另一件事情,先知們跟我們說,當(dāng)你設(shè)計(jì)函數(shù)參數(shù)的類型的時(shí)候,這個(gè)類型越基,哦不,越是在繼承鏈里面距離object靠得越近越好,這是為什么呢?這其實(shí)也是一個(gè)interface as contract的問題。舉個(gè)例子,我們需要對一個(gè)數(shù)組求和:
T Sum<T>(T[] data, Func<T, T, T> 加法函數(shù)); // C#真的可以用中文變量的哦!
費(fèi)盡心思終于寫好了,然后我們第二天發(fā)現(xiàn),我們對List<T>也要做一樣的事情。那怎么辦呢?
在這里,Sum究竟對data有什么需求呢?其實(shí)研究一下就會發(fā)現(xiàn),我們需要的只是想遍歷data里面的所有內(nèi)容而已。那data是不是一個(gè)數(shù)組,還是列表,還是一顆二叉樹,還是你垃圾ipad里面的一些莫名其妙的數(shù)據(jù)也好,其實(shí)都一樣。那C#里面什么interface代表“遍歷”這個(gè)contract呢?當(dāng)然是IEnumerable<T>了:
T Sum<T>(IEnumerable<T> data, Func<T, T, T> 加法函數(shù)); // C#真的可以用中文變量的哦!
這樣你的容器只要能夠被foreach,也就是繼承自IEnumearble<T>,就可以用這個(gè)函數(shù)了。這也是為什么”linq to 容器”基本上都是在IEnumerable上做的,因?yàn)樗麄冎恍枰闅v。
哦,我們還說到了foreach。foreach是一個(gè)語句,用來遍歷一個(gè)容器。那你如何表達(dá)一個(gè)容器可以被foreach拿來遍歷的這個(gè)contract呢?還是IEnumerable<T>。interface拿來做contract的確是最佳匹配呀。
其實(shí)說了這么多,我只想表達(dá)一個(gè)東西。不要太在意“這個(gè)變量的類型是什么”,你要關(guān)心的是“我可以對這個(gè)變量做這樣那樣的事情”。這就是為什么我們會推薦“面向接口編程”,因?yàn)槿丝偸菓猩⒌?,需要一些約束。interface不能用來new,不包含成員變量,剛好是contract的一種很好地表達(dá)方法。C++表達(dá)contract其實(shí)還可以用模板,不過這個(gè)就下次再說了。如果你們非得現(xiàn)在就知道到底怎么用的話,我就告訴你們,只要把C#的(i as IComparable<int>).CompareTo(j)換成Comparable<int>::Compare(i, j)就好了。
所以在可能的情況下,我們設(shè)計(jì)函數(shù)的參數(shù)和返回值的類型,也盡量用更基的那些類型。因?yàn)槿绻愀厦娴腟um一樣,只關(guān)心遍歷,那么你根本不應(yīng)該要求你的參數(shù)可以被隨機(jī)訪問(數(shù)組就是這個(gè)意思)。
希望大家看完這篇文章之后可以明白很多我們在面向?qū)ο缶幊痰臅r(shí)候,先知們建議的那些條款。當(dāng)然這里還不涉及設(shè)計(jì)模式的東西。其實(shí)設(shè)計(jì)模式說白了是語法的補(bǔ)丁。設(shè)計(jì)模式這種東西用的最多,C#略少,你們會發(fā)現(xiàn)像listener模式這種東西C#就不常用,因?yàn)樗懈玫臇|西——event。
posted on 2013-05-04 19:29
陳梓瀚(vczh) 閱讀(15577)
評論(16) 編輯 收藏 引用 所屬分類:
啟示