摘要
Anders Hejlsberg,C#的主架構師,與Bruce Eckel和Bill Venners 談論了C#和Java的泛型、C++模板、C#的constraints特性以及弱類型化和強類型化的問題。
Anders Hejlsberg,微軟的一位傑出工程師,他領導了C#(發音是C Sharp)編程語言的設計團隊。Hejlsberg首次躍上軟件業界舞台是源於他在80年代早期為MS-DOS和CP/M寫的一個Pascal編譯器。不久一個叫做Borland的非常年輕的公司雇傭了他並且買下了他的編譯器,從那以後這個編譯器就作為Turbo Pascal在市場上推廣。在Borland,Hejlsberg繼續開發Turbo Pacal並且在後來領導一個團隊設計Turbo Pascal的替代品:Delphi。1996年,在Borland工作13年以後,Hejlsberg加入了微軟,在那裡一開始作為Visual J++和Windows基礎類庫(WFC)的架構師。隨後,Hejlsberg擔任了C#的主要設計者和.Net框架創建過程中的一個主要參與者。現在,Anders Hejlsberg領導C#編程語言的後續開發。
2003年7月30號,Bruce Eckel(《Thinking in C++》以及《Thinking in Java》的作者)和Bill Venners(Artima.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談論了DLL hell、接口契約、strong anmes以及互操作的重要性。
在第六部分:Inappropriate Abstractions裡, Hejlsberg以及C#團隊的其他成員談論了試圖讓網絡透明的分布式系統,以及試圖屏蔽掉數據庫的對象——關系映射。
在第七部分, Hejlsberg比較了C#和Java的泛型以及C++模板的實現方法,並且介紹了C#的constraints特性以及弱類型化和強類型化的問題。
泛型概述
Bruce Eckel: 能否就泛型做一個簡短的介紹?
Anders Hejlsberg: 泛型的本質就是讓你的類型能夠擁有類型參數。它們也被成為參數化類型(parameterized types)或者參數的多態(parametric polymorphism)。經典的例子十九一個List集合類。List是一個方便易用的、可增長的數組。它有一個排序方法,你可以通過索引來引用它的元素,等等。現今,如果沒有參數化類型,在使用數組或者Lists之間就會有些別扭的地方。如果使用數組,你得到了強類型保證,因為你可以定義一個關於Customer的數組,但是你沒有可增長性和那些方便易用的方法。如果你用的是List,雖然你得到了所有這些方便,但是卻喪失了強類型保證。你不能指定一個List是關於什麼的List。它只是一個關於Object的List。這會給你帶來一些問題。類型檢測必須在運行時刻做,也就意味著沒有在編譯時刻對類型進行檢測。即便是你塞給List一個Customer對象然後試圖取出一個String,編譯器也不會有絲毫的抱怨。直到運行時刻你才會發現他會出問題。另外,當把基元類型(primitive type)放入List的時候,還必須對它們進行裝箱(box)。基於上述所有這些問題,Lists與Arrays之間的這種不和諧的地方總是存在的。到底選擇哪個,會讓你一直猶豫不決。
泛型的最大好處就是它讓你有了一個兩全其美的辦法(you can have your cake and eat it too),因為你可以定義一個List<T>[讀作:List of T]。當使用一個List的時候,你可以實實在在地知道這個List是關於什麼類型的List,並且讓編譯器為你做強類型檢測。這只是它最直接的好處。接下來還有其它各種各樣的好處。當然,你不會僅僅想讓List擁有泛型。哈希表(Hashtable)或者字典(Dictionary)——隨便你怎麼叫它——把鍵(keys)映射到值(values)。你可能會想要把Strings映射到Customrs,或者ints到Orders,而且是以強類型化的方式。
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,你不需要做類型轉換,因為到處都會做強類型檢驗。
在CLR(Common Language Runtime)環境下,當編譯List<T>或者其它任何generic類型的時候,會像其它普通類型一樣,先編譯成中間語言IL(Intermediate Language)以及元數據。理所當然,IL以及元數據包含了額外的信息,從而可以知道有一個類型參數,但是從原則上來說,generic類型的編譯與其它類型並沒有什麼不同。在運行時刻,當應用程序第一次引用到List<int>的時候,系統會查找看是否有人已經請求過List<int>。如果沒有,它會把List<T>的IL和元數據以及類型參數int傳遞給JIT。而JITer在即時編譯IL的過程中,也會替換掉類型參數。
Bruce Eckel: 也就是說它是在運行時刻實例化的。
Anders Hejlsberg: 的確如此,它是在運行時刻實例化的。它在需要的時候產生出針對特定類型的原生代碼(native code)。從字面上看,當你說List<int>的時候,你會得到一個關於int的List。如果generic類型的代碼使用了一個關於T的array,你得到的就是一個關於int的array。
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裡編譯一個泛型類的時候,編譯器拿掉了類型參數並到處代之以Object。List<T>編譯好的影像文件(image)就像是一個到處使用Object(作為類型參數)的List。當然,如果你試圖創建一個List<int>,那就的對所有用到的int對象進行裝箱(boxing)。這就產生了很大的負擔。此外,為了與老的虛擬機兼容,編譯器實際上會插入各種各樣的轉換代碼,而這些轉換代碼並不是由你來寫的。如果是一個關於Object的List,而你試圖把這些Objects當作Customers來對待,這些Objects必須在某些地方被轉換成Customers,以便讓verifIEr的驗證能夠通過。實際上它們的實現所做的就是自動為你插入那些類型轉換。也就是說你得到了語法上的甜頭,或者至少是一部分語法上的甜頭,但是你並沒有得到任何程序執行上的效率。這是我認為Java泛型解決方案的第一個問題。
第二個問題是,我認為這可能是更大的一個問題,因為Java的泛型實現依賴於去處掉類型參數,當到了運行時刻,你實際上並沒有一個相對於運行時刻的可靠的泛型表示。當你在Java裡針對一個泛型List使用反射(reflection)的時候,你並不知道這個List到底是關於什麼的List。它只是一個List。因為你已經丟失了類型信息,對於任何動態代碼生成(dynamic code-generation)的應用或者基於反射的應用,就沒法工作了。這種趨勢對我來說已經很明了了,(丟失類型信息的)情況越來越多。它根本沒辦法工作,因為你丟失了類型信息。而在我們的實現裡,所有這些信息都是可獲得的。你可以通過反射得到List<T>對象的System.Type表示。但這時候你還不能創建它的實例,因為你還不知道T是什麼。但是你可以使用反射得到int的System.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類型的變量,x和y,如果你寫成x+y,那你最好事先定義了用於兩個T型變量相加的+運算符,否則你會得到一些古怪的錯誤信息。所以從某種意義上說,C++模板實際上是非類型化的,或者說是弱類型化的。而C#泛型則是強類型化的。
C#泛型的constraints特性
Bruce Eckel: constraints在C#泛型裡是如何工作的?
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為鍵(key)V為值(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 a和B b作為參數,然後我在代碼裡就可以寫a+b?我是否可以不關心A和B是什麼,只要它們有一個“+”運算符就可以了,因為我想要的是弱類型化。
Anders Hejlsberg: 你實際上問的是,通過constraints你到底能做到什麼程度?與其它特性類似,如果把constraints發揮到極致,他可以變得異常復雜。仔細想想,其實constraints是一種模式匹配(pattern matching)的機制。你想要能指定,“該類型參數必須有一個接受兩個參數的構造函數,並且實現了+運算符,要有某個靜態方法,以及其它兩個非靜態方法,等等。”問題是,你想要這種模式匹配的機制復雜到哪種程度?
從什麼也不做到功能全面的模式匹配,這是很大的一個范圍。我們認為什麼也不做太說不過去了,而全面的模式匹配又會變得非常復雜,所以我們選擇了折衷的方式。我們允許你指定一個constraint,它可以是一個類、零個或者多個接口、以及叫做constructor constraint的東西。比如說,你可以指定“該類型必須實現IFoo和IBar接口,”或者“該類型必須繼承自基類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,至少如果T是int、double或者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)。你把它撥的越高,程序員越覺得難受,但同時代碼更安全了。但是在兩個方向上你都有可能把它撥過頭。