C#、Java以及C++的泛型

Copyright © 1996-2005 Artima Software, Inc. All rights reserved

Generics in C#, Java, and C++

A Conversation with Anders Hejlsberg, Part VII
by Bill Venners with Bruce Eckel. January 26, 2004

翻譯:http://blog.csdn.net/lxwde

摘要

Anders HejlsbergC#的主架構師,與Bruce EckelBill Venners 談論了C#Java的泛型、C++模板、C#constraints特性以及弱類型化和強類型化的問題。

Anders Hejlsberg,微軟的一位傑出工程師,他領導了C#(發音是C Sharp)編程語言的設計團隊。Hejlsberg首次躍上軟件業界舞臺是源於他在80年代早期爲MS-DOSCP/M寫的一個Pascal編譯器。不久一個叫做Borland的非常年輕的公司僱傭了他並且買下了他的編譯器,從那以後這個編譯器就作爲Turbo Pascal在市場上推廣。在BorlandHejlsberg繼續開發Turbo Pacal並且在後來領導一個團隊設計Turbo Pascal的替代品:Delphi1996年,在Borland工作13年以後,Hejlsberg加入了微軟,在那裏一開始作爲Visual J++windows基礎類庫(WFC)的架構師。隨後,Hejlsberg擔任了C#的主要設計者和.NET框架創建過程中的一個主要參與者。現在,Anders Hejlsberg領導C#編程語言的後續開發。

2003730號,Bruce Eckel(《Thinking in C++》以及《Thinking in Java》的作者)和Bill VennersArtima.com的主編)與Anders Hejlsberg在他位於華盛頓州Redmond的微軟辦公室進行了一次面談。這次訪談的內容將分多次發佈在Artima.com以及Bruce Eckel將於今年秋天發佈的一張音頻光碟上。在這次訪談中,Anders Hejlsberg談論了C#語言和.NET框架設計上的一些取捨。

·        第一部分:C#的設計過程中, Hejlsberg談論了C#設計團隊所採用的流程,以及在語言設計中可用性研究(usability studies)和好的品味(good taste)相對而言的優點。

·        第二部分:Checked Exceptions的問題中, Hejlsberg談論了已檢測異常(checked exceptions)的版本(versionability)問題和規模擴展(scalability)問題。

·        第三部分: 委託、組件以及表面上的簡單性裏,Hejlsberg 談論了委託(delegates)以及C#對於組件的概念給予的頭等待遇。

·        第四部分:版本,虛函數和覆寫裏,Hejlsberg解釋了談論了爲什麼C#的方法默認是非虛函數,以及爲什麼程序員必須顯式指定覆寫(override)。

  • 在第七部分, Hejlsberg比較了C#Java的泛型以及C++模板的實現方法,並且介紹了C#constraints特性以及弱類型化和強類型化的問題。

泛型概述

Bruce Eckel: 能否就泛型做一個簡短的介紹?

Anders Hejlsberg: 泛型的本質就是讓你的類型能夠擁有類型參數。它們也被稱爲參數化類型(parameterized types)或者參數的多態(parametric polymorphism)。經典的例子就是一個List集合類。List是一個方便易用的、可增長的數組。它有一個排序方法,你可以通過索引來引用它的元素,等等。現今,如果沒有參數化類型,在使用數組或者Lists之間就會有些彆扭的地方。如果使用數組,你得到了強類型保證,因爲你可以定義一個關於Customer的數組,但是你沒有可增長性和那些方便易用的方法。如果你用的是List,雖然你得到了所有這些方便,但是卻喪失了強類型保證。你不能指定一個List是關於什麼的List。它只是一個關於ObjectList。這會給你帶來一些問題。類型檢測必須在運行時刻做,也就意味着沒有在編譯時刻對類型進行檢測。即便是你塞給List一個Customer對象然後試圖取出一個String,編譯器也不會有絲毫的抱怨。直到運行時刻你纔會發現他會出問題。另外,當把基元類型(primitive type)放入List的時候,還必須對它們進行裝箱(box)。基於上述所有這些問題,ListsArrays之間的這種不和諧的地方總是存在的。到底選擇哪個,會讓你一直猶豫不決。

泛型的最大好處就是它讓你有了一個兩全其美的辦法(you can have your cake and eat it too),因爲你可以定義一個List<T>[讀作:List of T]。當使用一個List的時候,你可以實實在在地知道這個List是關於什麼類型的List,並且讓編譯器爲你做強類型檢測。這只是它最直接的好處。接下來還有其它各種各樣的好處。當然,你不會僅僅想讓List擁有泛型。哈希表(Hashtable)或者字典(Dictionary)——隨便你怎麼叫它——把鍵(keys)映射到值(values)。你可能會想要把Strings映射到Customrs,或者intsOrders,而且是以強類型化的方式。

C#的泛型

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

Anders Hejlsberg: 沒有泛型的C#,基本上你只能寫class List {...}。有了泛型,你可以寫成class List<T> {...},這裏T是類型參數。在List<T>範圍內你可以把T當作類型來使用,當真正需要創建一個List對象的時候,寫成List<int>或者List<Customer>。新類型是通過List<T>構建的,實際上就像是你的類型參數替換掉了原本的類型參數。所有的T都變成了ints或者Customers,你不需要做類型轉換,因爲到處都會做強類型檢驗。

CLRCommon Language Runtime)環境下,當編譯List<T>或者其它任何generic類型的時候,會像其它普通類型一樣,先編譯成中間語言ILIntermediate Language)以及元數據。理所當然,IL以及元數據包含了額外的信息,從而可以知道有一個類型參數,但是從原則上來說,generic類型的編譯與其它類型並沒有什麼不同。在運行時刻,當應用程序第一次引用到List<int>的時候,系統會查找看是否有人已經請求過List<int>。如果沒有,它會把List<T>IL和元數據以及類型參數int傳遞給JIT。而JITer在即時編譯IL的過程中,也會替換掉類型參數。

Bruce Eckel: 也就是說它是在運行時刻實例化的。

Anders Hejlsberg: 的確如此,它是在運行時刻實例化的。它在需要的時候產生出針對特定類型的原生代碼(native code)。從字面上看,當你說List<int>的時候,你會得到一個關於intList。如果generic類型的代碼使用了一個關於Tarray,你得到的就是一個關於intarray

Bruce Eckel: 垃圾回收機制會在某個時候來回收它麼?

Anders Hejlsberg: 可以說會,也可以說不會,這是一個正交的問題。這個類在應用程序範圍內被創建,然後在這個應用程序範圍內就一直存在下去。如果你殺掉這個應用程序,那麼這個類也就消失了,這點跟其它類一樣。

Bruce Eckel: 如果我有一個應用程序用到了List<int>List<Cat>,但是它從來沒有走到使用List<Cat>的那個分支。。。。。。

Anders Hejlsberg:。。。。。。那麼系統就不會實例化一個List<Cat>。現在讓我說說一些例外的情況。如果你是使用NGEN在創建一個影像(image),也就是說你在直接產生一個native的映像,你可以提早產生這些實例。但是如果你是在通常的情況下運行程序,是否實例化是完全根據需要來確定的,而且推遲到越晚越好。

這之後,我們針對所有值類型(比如List<int>List<long>List<Double> List<float>)的實例化做進一步的處理,創建可執行的原生代碼的唯一拷貝。這樣List<int>就有它自己的代碼。List<long>也有它自己的代碼。List<float>也是如此。對於所有引用類型(reference types),我們共享這些代碼,因爲它們所代表的東西是相同的。它們只是一些指針罷了。

Bruce Eckel: 你需要進行類型轉換吧。

Anders Hejlsberg: 不,實際上並不需要。我們可以共享native image,但實際上它們有各自單獨的虛函數表(VTables)。我只是想指出,當共享代碼有意義的時候,我們會不遺餘力的去做這件事情,但是當你非常需要運行效率的時候,我們對於共享代碼會非常謹慎。通常對於值類型,你確實會關心List<int>元素的類型就是int。你不想把它們裝箱(box)成Objects。對值類型進行裝箱/拆箱,是可以用來進行代碼共享的一種方法,但是這種方法代價過於昂貴。

Bill Venners: 對於引用類型,實際上也是完全不同的類。List<Elephant>List<Orangutan>是不同的,但是它們確實共享所有的類方法的代碼。

Anders Hejlsberg: 是的。作爲實現上的細節來說,它們確實共享了相同的原生代碼(native code)。

C#泛型與Java泛型的比較

Bruce Eckel: C#泛型相比Java泛型有什麼特點?

Anders Hejlsberg: Java的泛型實現是基於一個最初叫做Pizza的項目,這個項目是由Martin Odersky和其他一些人完成的。Pizza被重新命名爲GJ,然後他成了一個JSR,並且最後被採納進了Java語言。這個特定的泛型proposal有一個關鍵的設計目標,就是它應該能夠跑在不必經過改動的虛擬機上。不用改動虛擬機當然很棒,但是它也帶來了一系列奇奇怪怪的限制。這些限制並不都是顯而易見的,但是很快你就會說,“Hmm,這可有點怪。”

比如說,使用Java泛型,實際上你就得不到任何剛纔我所說得程序執行上的效率,因爲當你在Java裏編譯一個泛型類的時候,編譯器拿掉了類型參數併到處代之以ObjectList<T>編譯好的影像文件(image)就像是一個到處使用Object(作爲類型參數)的List。當然,如果你試圖創建一個List<int>,那就的對所有用到的int對象進行裝箱(boxing)。這就產生了很大的負擔。此外,爲了與老的虛擬機兼容,編譯器實際上會插入各種各樣的轉換代碼,而這些轉換代碼並不是由你來寫的。如果是一個關於ObjectList,而你試圖把這些Objects當作Customers來對待,這些Objects必須在某些地方被轉換成Customers,以便讓verifier的驗證能夠通過。實際上它們的實現所做的就是自動爲你插入那些類型轉換。也就是說你得到了語法上的甜頭,或者至少是一部分語法上的甜頭,但是你並沒有得到任何程序執行上的效率。這是我認爲Java泛型解決方案的第一個問題。

第二個問題是,我認爲這可能是更大的一個問題,因爲Java的泛型實現依賴於去處掉類型參數,當到了運行時刻,你實際上並沒有一個相對於運行時刻的可靠的泛型表示。當你在Java裏針對一個泛型List使用反射(reflection)的時候,你並不知道這個List到底是關於什麼的List。它只是一個List。因爲你已經丟失了類型信息,對於任何動態代碼生成(dynamic code-generation)的應用或者基於反射的應用,就沒法工作了。這種趨勢對我來說已經很明瞭了,(丟失類型信息的)情況越來越多。它根本沒辦法工作,因爲你丟失了類型信息。而在我們的實現裏,所有這些信息都是可獲得的。你可以通過反射得到List<T>對象的System.Type表示。但這時候你還不能創建它的實例,因爲你還不知道T是什麼。但是你可以使用反射得到intSystem.Type表示。然後你可以請求反射機制把這兩個東西放在一起創建一個List<int>,這樣你就得到了另外一個用以表示List<int>System.Type。也就是說,從表示方法來說,任何你可以在編譯時刻做到的事情,你也可以在運行時刻做到。

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

Bruce Eckel: C#泛型相比C++模板有哪些特點?

Anders Hejlsberg: 在我看來,理解C#泛型與C++模板之間的差異最重要的一點就是:C#泛型實際上就像是類,除了它們有類型參數。而C++模板實際上就像是宏(macros),除了它們看起來像是類。

C#泛型與C++模板最大的不同之處在於類型檢驗發生的時間以及實例化的方式。首先,C#是在運行時刻實例化的,而C++ 是在編譯時刻或者可能是在link的時候。但是不管怎樣,C++模板實例化發生在程序運行之前。這是第一個不同之處。第二個不同之處在於,當你編譯generic類型的時候,C#對它進行強類型檢驗。對於像List<T>這樣未加限制的類型參數(unconstrained type parameter),類型T的值所能使用的方法僅限於Object類型所包含的方法,因爲只有這些方法纔是通常我們保證能夠存在的方法。也就是說,在C#泛型裏,我們保證你所實施於類型參數的任何操作都會成功。

C++正好與此相反。在C++裏,你可以對一個類型參數做任何你想做的事情。但是當你對它進行實例化的時候,它有可能通不過,而你會得到一些非常難懂的錯誤信息。比如,你有一個類型參數T以及兩個T類型的變量,xy,如果你寫成x+y,那你最好事先定義了用於兩個T型變量相加的+運算符,否則你會得到一些古怪的錯誤信息。所以從某種意義上說,C++模板實際上是非類型化的,或者說是弱類型化的。而C#泛型則是強類型化的。

C#泛型的constraints特性

Bruce Eckel: constraintsC#泛型裏是如何工作的?

Anders Hejlsberg: C#泛型裏,我們可以針對類型參數加一些限制條件(constraints)。還以List<T>爲例,你可以寫成,class List<T> where T: IComparable。意思是T必須實現IComparable接口。

Bruce Eckel: 有意思的是在C++裏限制條件是隱含的。

Anders Hejlsberg: 是的。在C#裏,你也可以讓限制條件是隱含的。比如說我們有一個Dictionary<K,V>,它有一個add方法,以K爲鍵(keyV爲值(value)。Add方法的實現很可能需要把傳入的鍵與Dictionary已有的鍵進行比較,而且它可能通過一個叫做IComparable的接口來做這個比較。一種方法是把key參數轉換成IComparable,然後調用compareTo方法。當然,當你這麼做的時候,你就已經針對K類型和key參數創建了一個隱式的限制條件。如果傳入的key沒有實現IComparable接口,你就會得到一個運行時錯誤。但是實際上你並沒有在你的哪個方法裏或者約定裏明確表明key必須實現IComparable。而且你當然還得付出運行時刻類型檢測的代價,因爲實際上你所做的是運行時刻的動態類型檢驗。

使用constraint,你可以把代碼裏的動態檢驗提前,在編譯時刻或者加載的時候對它進行驗證。當你指定K必須實現IComparable,這就隱含了一系列的東西。對於任何K類型的值,你都可以直接訪問接口方法,而不需要進行轉換,因爲從語義上來說,在整個程序裏K類型要實現這個接口,這一點是得到保證的。無論什麼時候你想要創建該類型的一個實例,編譯器都會針對你給出的任何作爲K參數的類型進行檢驗,看它是否實現了IComparable。如果沒有實現,你會得到一個編譯時錯誤。或者如果你是利用反射來做的話,會得到一個異常。

Bruce Eckel: 你說到了編譯器以及運行時刻。

Anders Hejlsberg: 編譯器會做檢驗,但是你也可能是在運行時刻通過反射來做的,這時候就由系統來做檢驗。如前所述,任何你在編譯時刻可以做的事情,你都可以在運行時刻通過反射來做。

Bruce Eckel: 我是否可以寫一個模板函數,或者換句話說,一個參數類型未知的函數?你們是在所做的是給容器加上更強的類型檢驗,但是我是否可以像在C++模板裏那樣得到弱類型化的東西呢?比如說,我是否可以寫一個函數,它以A aB b作爲參數,然後我在代碼裏就可以寫a+b?我是否可以不關心AB是什麼,只要它們有一個“+”運算符就可以了,因爲我想要的是弱類型化。

Anders Hejlsberg: 你實際上問的是,通過constraints你到底能做到什麼程度?與其它特性類似,如果把constraints發揮到極致,他可以變得異常複雜。仔細想想,其實constraints是一種模式匹配(pattern matching)的機制。你想要能指定,“該類型參數必須有一個接受兩個參數的構造函數,並且實現了+運算符,要有某個靜態方法,以及其它兩個非靜態方法,等等。”問題是,你想要這種模式匹配的機制複雜到哪種程度?

從什麼也不做到功能全面的模式匹配,這是很大的一個範圍。我們認爲什麼也不做太說不過去了,而全面的模式匹配又會變得非常複雜,所以我們選擇了折衷的方式。我們允許你指定一個constraint,它可以是一個類、零個或者多個接口、以及叫做constructor constraint的東西。比如說,你可以指定“該類型必須實現IFooIBar接口,”或者“該類型必須繼承自基類X。”一旦你這麼做了,我們會在所有地方做類型檢驗以確認該constraint是否爲真,包括編譯時刻和運行時刻。任何由這個constraint所暗含的方法都可以通過類型參數的實例直接訪問。

另外,在C#裏,運算符都是靜態成員函數。也就是說,一個運算符永遠不可能成爲一個接口的成員函數,因此一個接口限制條件(interface constraint)永遠不可能讓你指定一個“+”運算符。要指定一個“+”運算符,唯一的方法就是通過一個類限制條件(class constraint),這個類限制條件指定說必須繼承自某個類,比如說Number類,因爲Number有一個“+”運算符。但是你不可能把它抽象成:“必須有一個+運算符”,然後由我們來以多態的方式解析它的實際含義。

Bill Venners: 你是通過類型,而不是簽名(signature)來實現限制條件的。

Anders Hejlsberg: 是的。

Bill Venners: 也就是說指定類型必須擴展某個類或者實現某些接口。

Anders Hejlsberg: 是的。本來我們可以走得更遠。我們確實考慮過走得更遠一些,但是那會非常複雜。並且我們不知道添加這些複雜性相對於你所獲得的微不足道的好處,是否值得。如果你想做的事情沒有被constraint系統直接支持,你可以藉助於工廠模式(factory pattern)來完成。比如說,你有一個矩陣類Matrix<T>,在這個Matrix裏你想定義一個標量積(dot product)方法。這當然意味着你最終需要理解如何把兩個T相乘,但你不能把它表達成一個constraint,至少如果Tintdouble或者float的時候這樣做不行。但是你可以這麼做:讓Matrix接受一個Calculator<T>這樣的參數,然後在Calculator<T>裏聲明一個叫做multiply的方法。你實現這個方法並把它傳給Matrix

Bruce Eckel: Calculator也是個參數化類型。

Anders Hejlsberg: 是的,它有點像factory模式。總之,是有辦法來做這些事情的。可能不如你想要的那麼棒,但是任何事情都是有代價的。

Bruce Eckel: 嗯,我感覺C++模板像是一種弱類型化(weak typing)的機制。當你開始在它上面添加constraints的時候,你是在從弱類型化轉向強類型化(strong typing)。通常加入強類型化都會讓事情更加複雜。這像是一個頻譜。

Anders Hejlsberg: 你所意識到的類型化(typing)的問題,其實是一個撥盤(dial)。你把它撥的越高,程序員越覺得難受,但同時代碼更安全了。但是在兩個方向上你都有可能把它撥過頭。

反饋

對本文所描述的設計原則有自己的觀點麼?那麼請到News&Ideas論壇討論這篇文章, Generics in C#, Java, and C++.

資源

Deep Inside C#: An Interview with Microsoft Chief Architect Anders Hejlsberg:
http://windows.oreilly.com/news/hejlsberg_0800.html

A Comparative Overview of C#:
http://genamics.com/developer/csharp_comparative.htm

Microsoft Visual C#:
http://msdn.microsoft.com/vcsharp/

Dan Fernandez's Weblog:
http://blogs.gotdotnet.com/danielfe/

Eric Gunnerson's Weblog:
http://blogs.gotdotnet.com/ericgu/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章