(收藏)Anders Hejlsberg談C#、Java和C++中的泛型

來源:互聯網
上載者:User
Anders Hejlsberg談C#、Java和C++中的泛型

原著:Bill Venners、Bruce Eckel  2004.2.26
原文:http://www.artima.com/intv/generics.html翻譯:lover_P
出處:http://www.cstc.net.cn/docs/docs.php?id=258

[人物介紹]

    Anders Hejlsberg,微軟著名工程師,帶領他的小組設計了C#(讀作:C-Sharp)程式設計語言。Hejlsberg第一次登上軟體界曆史舞台是在80年代早期,因為他為MS-DOS和CP/M設計了Pascal編譯器。當時,還是一個小公司的Borland很快僱用了他,並買下了他的編譯器,改稱Turbo Pascal。在Borland,Hejlsberg繼續開發Turbo Pascal,並最終帶領他的小組設計了Turbo Pascal的替代品:Delphi。1996年,在進入Borland 13年後,Hejlsberg加入了微軟。最初,他做Visual J++和Windows Fundatioin Classes(WFC)的架構師。隨後,Hejlsberg成為C#的首席設計師和.NET Framework的關鍵參與者。目前,Anders Hejlsberg還在領導著C#程式設計語言的繼續開發。

    Bruce Eckel,Think in C++(C++編程思想)和Think in Java(Java編程思想)的作者。

    Bill Venners,Artima.com的主編。

[內容]

  • 泛型概述
  • C#中的泛型
  • C#泛型和java泛型的比較
  • C#泛型和C++模板的比較
  • C#泛型中的約束
泛型概述

    Bruce Eckel:您能對泛型做一個快速的介紹嗎?

    Anders Hejlsberg:泛型其實就是能夠向你的類型中加入型別參數的一種能力,也稱作參數化類別型或多重參數變形性。最著名的例子就是List集合類。一個List是一個易於增長的數組。它有一個排序方法,你可以為 它做索引,等等。現在,如果沒有參數化類別型,那麼不論使用數組還是使用List都不是很好。如果你使用數組,你能獲得強型別,因為你可以聲明一個Customer類型的數組,但你失去了可增長性和那些方便的方法;如果你使用一個List,你能夠得到所有的便利,但你失去了強型別。你難以說出一個List是什麼(類型的)List,它只是一個Object的List【譯註:“什麼類型的List”指的是List存放的元素是什麼類型的】。這會給你帶來麻煩 ,因為類型只能在運行形時進行檢查,也就是說在編譯時間不會進行類型檢查。就算你硬要把一個Customer放進一個List並試圖從中得到一個String,編譯器也不會不高興。在運行之前你根本無法發現它不能工作。同時,當你將簡單類型【譯註:指實值型別】放入List時,還必須對它們進行裝箱。正是由於這些問題,你不得不在List和數組之間徘徊,你經常要很痛苦地決定應該使用哪一個。

    泛型的偉大之處在於你現在可以盡情地享受你的蛋糕了,因為你能夠定義一個List<T>(讀作:List of T)【譯註:中文可以說成“T類型的List”】。當你使用List時,你居然能夠說出它是什麼類型的List,並且你將獲得強型別,編譯器會為你檢查它的類型。這些只是直覺上的好處,它還有其它許多優點。當然,你並不是只能將它用於List,Hastable、Dictionary(將鍵影射到值上的資料結構)——所有你想調用的都行。你可能想將String影射到Customer、將int影射到Order,在這些情況下你都能獲得強型別。

C#中的泛型

    Bill Venners:泛型在C#中是如何工作的呢?

    Anders Hejlsberg:在沒有泛型的C#中,你只能寫class List {...};而在帶有泛型的C#中,你可以寫class List<T> {...},這裡的T是一個型別參數。在List<T>中,你可以把T就當作一個類型來用。當它實際用來建立一個List對象時,你要寫List<int>或List<Customer>。這樣你就從List<T>構造了一個新的類型,看起來就好像你用你的類型變數替換了所有的型別參數。所有的T都變成了int或Customer,你無須進行向下轉換,它們是強型別的,任何時候都會被檢查。

    在CLR(Common Language Runtime,通用語言執行平台)中,當你編譯List<T>或其它泛型型別時,它們和普通類型一樣被轉換為IL(Intermediate Language,中繼語言)和中繼資料。IL和中繼資料帶有附加資訊,可以知道這是一個型別參數,當然,原則上泛型型別的編譯和其它類型一樣。在運行時,當你的應用程式第一次引用List<T>時,系統會看看你是否已經使用過List<int>。如果沒有,它會調用JIT將帶有int類型變數的List<T>編譯為IL和中繼資料。當JIT即時編譯IL時,同樣會替換型別參數。

    Bruce Eckel:所以它是在運行時被執行個體化的。

    Anders Hejlsberg:它確實是在運行時執行個體化。它在需要的時候才產生特定的原生代碼(native code)。字面上,當你說List<T>時,你會得到一個int類型的List。如果泛型型別中使用的是T類型的數組,它會變成int類型的數組。

    Bruce Eckel:這個類會在某一時刻被垃圾收集器收集嗎?

    Anders Hejlsberg:是也不是,這是一個正交的問題。它會在該程式集中建立一個類,這個類在程式集中會一直存在。如果你終止了程式集,這個類會消失,和其它類一樣。

    Bruce Eckel:但如果我的程式中聲明了一個List<int>和一個List<Cat>,但我從未使用過List<Cat>……

    Anders Hejlsberg:……那麼系統不會執行個體化List<Cat>。當然,下面的情況除外。如果你使用NGEN產生一個鏡像,也就是說如果你預先產生了一個原生代碼的鏡像,會預先執行個體化。但是如果你在一般的環境下運行,則這個執行個體化是純需求驅動(demand driven)的,會儘可能地延遲【譯註:正如上面所說,直到使用時才進行執行個體化】。

    實際上,我們所要進行執行個體化的所有類型都是實值型別——如List<int>、List<long>、List<double>、List<float>——我們為每一個都建立一份唯一的可執行原生代碼的拷貝。因此,List<int>有它自己的代碼,List<long>有它自己的代碼,List<float>有它自己的代碼。對於所有的參考型別我們共用它們的代碼,因為它們在表現上是一樣的,它們只是一些指標。

    Bruce Eckel:因此你只需要轉換。

    Anders Hejlsberg:不,實際上是不需要的。我們可以共用原生鏡像,但他們實際上具有獨立的VTable。我要指出的是,我們只是盡量對代碼進行有意義的共用,但我們很清楚,為了效率,有很多代碼是不能共用的。典型的就是實值型別,你會很關心List<int>中到底是不是int。你肯定不希望將它們被裝箱為Object。對實值型別進行裝箱是一種共用的方法,但對它們進行裝箱開銷會很大。

    Bill Venners:對於參考型別,所不同的只是類。List<Elephant>不同於List<Orangutan>,但他們實際上共用了所有方法的代碼。

    Anders Hejlsberg:是的。作為實現的細節,它們實際上共用了相同的原生代碼。

C#泛型和java泛型的比較

    Bruce Eckel:如何比較C#中的泛型和java中的泛型呢?

    Adners hejlsberg:Java的泛型最初是基於Martin Odersky和其它人一起做的稱作Pizza的一個項目的。Pizza後改名為GJ,然後成為JSR,最後以被Java語言收容而告終。這種泛型以能夠在原有的VM(Virtual Machine,虛擬機器)上運行為關鍵設計目標。也就是說,你不必修改你的VM,但它會帶來很多限制。這些限制並不會很快出現,但很快你就會說:“嗯,這有點陌生。”

    例如,使用Java泛型,我覺得你實際上不會獲得任何的執行效率,因為當你編譯一個Java泛型類時,編譯器會將所有的型別參數替換為Object。當然,如果你嘗試建立一個List<int>,你就需要對所有的int進行裝箱。因此,這會有很大的開銷。另外,為了讓VM高興,編譯器必須為所有的類型插入類型轉換。如果一個List是Object的,而你想將這些Object視為Customer,就必須將Object轉換為Customer,以讓類型檢查器滿意。而它在實現這些的時候,真的只是為你插入所有這些類型轉換。因此,你只是嘗到了文法上的甜頭,卻沒有獲得任何執行效率。所以我覺得這是(泛型的)Java實現的頭號問題。

    第二號問題,我覺得也是一個很嚴重的問題,這就是由於Java泛型是依靠消除所有的型別參數來實現的,你就無法在運行時獲得一個和編譯時間同樣可靠的表現。當你在Java中反射一個泛型的List的時候,你無法得知這是個List什麼類型的List。它只是一個List。因為你失去了類型資訊,任何由代碼產生方案或基於反射的方案所產生的動態類型都將無法工作。唯一讓我認為清晰的趨勢就是,越來越多的東西將不能運行,就是因為你丟掉了類型資訊。但在我們的實現中,所有這些資訊都是可用的。你可以使用反射來獲得List<T>對象的System.Type。但你還不能建立它的一個執行個體,因為你並不知道T是什麼。但是接下來你可以使用反射來獲得int的Sytem.Type。然後你就可以請求反射將這兩個System.Type結合起來並建立一個List<int>,然後你還能獲得List<int>的另一個System.Type。因此,所有你在編譯期間能做的在運行時同樣可以。

C#泛型和C++模板的比較

    Bruce Eckel:如何比較C#泛型和C++模板呢?

    Anders Hejlsberg:我認為對C#泛型和C++模板之間的區別最好的理解是:C#泛型更像類,只不過它帶有型別參數;C++模板接近宏,只不過它看起來像類。

    C#泛型和C++模板之間最大的區別在於類型檢查發生的時機和如何進行執行個體化。首先,C#在運行時進行執行個體化。而C++在編譯時間,或者可能是串連時進行執行個體化。不管怎麼說,C++是在程式運行前進行執行個體化。這是第一點不同。第二點不同是當你編譯泛型型別時,C#會進行強型別檢查。對於一個非約束的型別參數,如List<T>,能夠在類型為T的值上執行的方法僅僅是那些能夠在Object類型中找到的方法,因為只有這些方法是我們能夠保證存在的。在C#中,我們要保證在一個型別參數上執行的所有操作都能成功。

    C++正相反。在C++中,你可以在型別參數所指定的類型的變數上執行你想做的任何操作。但是一旦你對它進行了執行個體化,它就有可能無法工作,你將會得到一些含義模糊的錯誤資訊。例如,如果你有一個型別參數T,而x和y是T類型的變數,然後你執行x+y,如果你對兩個T定義了一個operator+還好說,否則你就只能得到一些沒意義的錯誤訊息。因此,從某種意義上說,C++模板實際上是無類型的,或者說是弱類型的。而C#泛型是強型別的。

C#泛型中的約束

    Bruce Eckel:約束是如何在C#泛型中工作的呢?

    Anders Hejlsberg:在C#泛型中,我們能夠為型別參數施加約束。以我們的List<T>為例,你可以說class List<T> where T : IComparable。這意味著T必須實現IComparable介面。

    Bruce Eckel:有意思。在C++中,約束是隱式的。

    Anders Hejlsberg:是的。在C#中我們也可以這樣做。譬如我們有一個Dictionary<K, V>,它有一個Add()方法,這個方法帶有K key和V value參數。Add()方法的實現將希望能夠將傳遞進來的key和Dictionary中已經存在的key進行比較,而且它希望使用一個稱作IComparable的介面。唯一的途徑就是將key參數轉換為IComparable介面,然後調用CompareTo方法。當然,當你這麼做的時候,你就為K類型和key參數建立了一個隱式的約束。如果傳遞進來的key沒有實現IComparable介面,你會得到一個執行階段錯誤。這在你的所有方法中都有可能出現,因為你的約定沒有要求key必須實現IComparable介面。當然,你還得為運行時類型檢查付出代價,因為你實際上進行了動態類型轉換。

    使用約束,你可以消除代碼中的動態檢查,而在編譯時間或裝載時進行。當你要求K必須實現IComparable介面時,會發生很多事情。對於K類型的值,你現在可以直接存取介面方法而無需類型轉換。因為程式在語義上可以保證它實現了這個介面。無論什麼時候你嘗試建立這個類型的一個執行個體時,編譯器都會檢查這些類型是否實現了這個介面,如果沒有實現,會給你一個編譯錯誤。如果你使用的是反射,你會得到一個異常。

    Bruce Eckel:你是說編譯器和運行時(都會進行檢查)?

    Anders Hejlsberg:編譯器會檢查它,但你仍有可能在運行時通過反射來做這些,因此系統還會檢查它。正像我前面說的,編譯時間可以做的任何事都可以在運行是通過反射來做。

    Bruce Eckel:我可以做一個函數模板,換句話說,一個帶有不知道類型的參數的函數?你為約束添加了強型別檢查,但我是不是能像C++模板那樣得到一個弱類型模板? 例如,我能否寫一個函數,它帶有兩個參數A a和B b,並在代碼中寫a+b?我能不能說我不在乎對於A和B是否有operator+,因為它們是弱類型的?

    Anders Hejlsberg:你真正要問的問題應該是這在約束中如何說吧?約束,和其他特性一樣,最終將可以是任意複雜的。當你考慮它的時候,約束只是一個模式比對機制。你可能希望能夠說“這個型別參數必須有一個帶有兩個參數的構造器、實現了operator+、有這個靜態方法、有那兩個執行個體方法、等等”。問題是,你希望這種模式比對機制有多複雜?

    從沒有任何東西到完全模式比對是一個整個的連續體。沒有任何東西(的模式比對)太小了,不能說明問題;而完全模式比對又太複雜了,因此我們需要在中間找一個平衡點。我們允許你將約束指定為一個類、一個或多個介面,以及一些構造器約束。譬如,你可以說:“這個類型必須實現IFoo和IBar”或“這個類型必須繼承基類X”。一旦你這麼做了,在編譯時間和運行時都會檢查這個約束是否為真。這個約束所隱含的任何方法對於型別參數所指定的類型的值都是直接有效。

    現在,在C#中,運算子是靜態成員。因此,運算子不能是介面的成員,因此介面約束不能帶給你operator+。你只能通過類約束獲得operator+,你可以說這個型別參數必須繼承自比如說Number類,並且Number類對於兩個Nubmer有operator+。但你不能抽象地說“必須有一個operator+”,我們無法知道這句話的具體含義。

    Bill Venners:你通過類型進行約束,而不是簽名。

    Anders Hejlsberg:是的。

    Bill Venners:因此這個類型必須擴充一個類或實現一個介面。

    Anders Hejlsberg:是的。而且我們還能夠走得更遠。實際上我們也想過再走遠一些,但這會變得相當複雜。而且增加的複雜性與所得到的相比很不值得。如果你想做的事情在約束系統中不直接支援,你可以使用一個原廠模式。例如你有一個Martix<T>,而在這個Martix(矩陣)中,你可能想定義一個“點乘”【譯註:矩陣上的一種乘法運算,另一種稱為“叉乘”】方法。這意味著你最終將要考慮如何將兩個T相乘,但你不能將這說成是一個約束,至少當T不是int、double或float時你不能這麼說。但你可以讓你的Martix帶有一個Calculator<T>作為參數,而在Calculator<T>中,有一個稱為Multiply的方法。你可以在其中進行實現,並將結果傳遞給Martix。

    Bruce Eckel:而且Calculator也是一個參數化類別型。

    Anders Hejlsberg:是的。這有些像原廠模式,還有很多方法可以做到,這也許不是你最喜歡的方法,但做任何事情都要付出代價。

    Bruce Eckel: 是呀,我開始認為C++模板是一種弱類型機制。而當你想其中添加了約束後,你從弱類型走向了強型別。但這一定會帶來更多的複雜性。這就是代價吧。

    Anders Hejlsberg: 關於類型你可以認為它是一個尺規。這個尺規定得越高,程式員的日子就會越不好過,但更高的安全性隨之而來。但你可以把這個尺規向任何一個方向調節。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.