冒號和他的學(xué)生們(連載9)——泛型范式
冒號和他的學(xué)生們
——程序員提高班紀(jì)事
- 泛型范式
算法是脊,數(shù)據(jù)是肉;思想是雞,結(jié)論是蛋 ——題記
冒號重新開講:“你們會不會經(jīng)常遇到這種情景:一遍又一遍地寫著相似的代碼,有心將其歸并,卻因種種原因無法踐行。”
逗號心有戚戚焉道:“是啊,有時明明兩個函數(shù)的實現(xiàn)幾乎一模一樣的,就因為某些參數(shù)不匹配,無法合而為一。”
“有一種編程范式可以解決這個問題,它打破了不同數(shù)據(jù)結(jié)構(gòu)之間的壁壘,讓你的代碼不再臃腫,這——就是泛型編程。”冒號的語調(diào)和說辭不免令人聯(lián)想到電視上的減肥廣告,“Generic Programming,簡稱GP,其基本思想是:將算法與其作用的數(shù)據(jù)結(jié)構(gòu)分離,并將后者盡可能泛化,最大限度地實現(xiàn)算法重用。這種泛化是基于模板的參數(shù)多態(tài)(parametric polymorphism),相比OOP基于繼承的子類型多態(tài)(subtype polymorphism),不僅普適性更強,而且效率也更高。這不能不說是一種異數(shù)——我們知道,普適性往往是以效率為代價的。GP最著名的代表是C++中的STL,其后亦為Java,C#等所吸納。此外,一些函數(shù)式語言如Ocaml、Standard ML、Generic Haskell等也支持GP。”
冒號寫下兩段代碼——
C++(泛型編程):
template <typename T>
T max(T a, T b) // 求出兩個數(shù)中的較大者
{
return (a > b) ? a : b;
}
C(宏定義):
#define max(a,b) ((a) > (b) ?
(a) : (b))
“求兩個數(shù)中的較大值是經(jīng)常遇到的問題。”冒號解說著,“對于靜態(tài)類型語言來說,若參數(shù)類型不同,即使函數(shù)體相同也不能合為一體。如果語言不支持重載(overload),還可能出現(xiàn)maxInt、maxLong、maxFloat、 maxDouble之類的函數(shù)名,冗贅而丑陋。盡管在C中可用宏定義來實現(xiàn),但無法保證類型安全,而C++模板則兼顧類型安全和代碼重用,并且由于是在編譯期間展開的,效率上也不損失。不止于此,C++支持運算符重載,除數(shù)值類型外,一切定義了‘>’ 運算的數(shù)據(jù)類型均可調(diào)用max函數(shù),真是一舉N得,N趨向無窮大啊!”
冒號邊說邊比劃,夸張的語氣和手勢逗得大家都笑了。
引號提出疑問:“Java的一切對象都是Object,將所有參數(shù)都換成Object類型,豈不也是一種泛化?”
冒號答道:“首先,基本類型如int,float等不是Object的子類,雖然Java
新增了自動裝拆箱(autoboxing/unboxing)的功能,但要付出性能的代價。更重要的是,這將不可避免地需要類型強制轉(zhuǎn)換,喪失了靜態(tài)類型語言的優(yōu)勢,為Bug大開方便之門。這也是Java最終引入模板的原因,雖然有些姍姍來遲。類似地,C/C++中的通用指針void *也有類型安全問題。”
句號發(fā)表他的看法:“泛型雖好,似乎只是某些局部才用到的技術(shù),不具有前面幾種范式的滲透性。”
冒號聽罷不語,返身在黑板上寫下幾道題——
1.從一個整數(shù)數(shù)組中隨機抽取十個數(shù),對其中的素數(shù)求和
2.將一個無序整數(shù)集中所有的完全平方數(shù)換成其平方根
3.從學(xué)生成績表中,列出門門都及格且平均分在70分以上的學(xué)生名單
4.在一個著色二元樹中,將所有的紅色結(jié)點涂成藍(lán)色
5.將一個字符串從倒數(shù)第三個字符開始反向拷貝到另一個字符串中
6.每從標(biāo)準(zhǔn)輸入讀取一個非數(shù)字的字符,于標(biāo)準(zhǔn)輸出打印‘請輸入數(shù)字’
句號暗忖,不過是些常規(guī)題嘛。不料冒號的問題卻出人意表:“請問它們之間有何共同之處?能否共享一段代碼?”
見眾人緘默已久,冒號接著投影出一段代碼——
template <class Iterator, class
Act, class Test>
void process(Iterator begin,
Iterator end, Act act, Test test)
// 對容器中在給定范圍內(nèi)(即起于begin止于end)所有滿足給定條件的元
//素(即test(元素)==true)進行處理(即act(元素))
{
for ( ; begin != end;
++begin) // 從頭至尾遍歷容器內(nèi)元素
if (test(*begin)) act(*begin); // 若當(dāng)前元素滿足條件,則對其采取行動
}
“STL有三要素:算法(algorithm)、容器(container)和迭代器(iterator)。容器是數(shù)據(jù)的集合,可理解為抽象的數(shù)組;迭代器是算法與容器之間的接口,可理解為抽象的指針或者游標(biāo)。”冒號講述道,“算法串聯(lián)數(shù)據(jù),如脊貫肉;數(shù)據(jù)實化算法,如肉附脊。只有抽象出表面的數(shù)據(jù),算法的脊梁才能顯現(xiàn)。以上幾題看似風(fēng)馬牛不相及,若運用泛型思維,便可發(fā)現(xiàn)它們的共性:對指定集合中滿足指定條件的元素進行指定處理。用模板語言,寥寥數(shù)行即勾勒完畢。”
問號詫異道:“相比前面的max模板,這里連元素的數(shù)據(jù)類型T都不見了?”
冒號回答:“元素被容器封裝了。”
問號追問:“可這里連容器也看不到啊?”
冒號料有此問:“容器通過它的迭代器參與算法。”
句號豁然開朗:“通過模板,泛化了容器——可以是數(shù)組、列表、集合、映射、隊列、棧、字符串等等;泛化了元素——可以是任何數(shù)據(jù)類型;泛化了處理方法和限定條件——可以是任何函數(shù)。”
冒號提醒道:“補充兩點:處理方法和限定條件不限于函數(shù),還可以是函子(Functor)——自帶狀態(tài)的函數(shù)對象;另外還有一個隱蔽的泛化:迭代器——可以從前往后移動,可以從后往前移動,可以隨機移動,也可以按任意預(yù)先定義的規(guī)律移動。”
嘆號由衷感嘆:“果然強悍啊!”
逗號倒也心細(xì):“最后一題中標(biāo)準(zhǔn)輸入也算容器嗎?”
“為什么不呢?只要一個對象配備了迭代器,它就算容器。I/O流上就有現(xiàn)成的迭代器,當(dāng)然你也可以自行定制。”冒號目光轉(zhuǎn)向句號,“現(xiàn)在還有人說泛型編程滲透性不強嗎?”
句號腆然一笑。
“這只是泛型編程的冰山一角。重要的是,我們不是在玩弄花哨的技巧,而是在用一種新的視角去審視問題。”冒號總結(jié)道,“泛型編程是算法導(dǎo)向(Algorithm-Oriented)的,即以算法為起點和中心點,逐漸將其所涉及的數(shù)據(jù)結(jié)構(gòu)內(nèi)涵模糊化、外延擴大化,從而擴展算法的適用范圍。這非常類似數(shù)學(xué)思維——當(dāng)數(shù)學(xué)家證明完一個定理后,總會試圖在保持核心思想的前提下,盡可能地放寬題設(shè),增強結(jié)論,從而推廣定理。外行人以為數(shù)學(xué)定理最重要,其實數(shù)學(xué)思想才是數(shù)學(xué)的精髓,在數(shù)學(xué)家眼里,思想是雞,結(jié)論是蛋。這也無怪乎STL會出自一位學(xué)數(shù)學(xué)的人之手了。”
新版請見:冒號課堂§3.1:泛型范式
posted on 2008-05-09 00:09 鄭暉 閱讀(4695) 評論(4) 編輯 收藏 所屬分類: 冒號和他的學(xué)生們