在新的平台上編程
----微軟 .NET平台系列文章之一
譯文/趙湘寧
一年多來,我將注意力一直放在微軟的.NET CLR(公共語言運行時:Common Language Runtime)平台。在我看來,今後大多數新的開發都將面向這個平台,因為它使應用程序的開發變得更容易、更簡單。同時,我還期望現有的應用開發能迅速移到.NET平台上來。
為了幫助開發人員掌握這個新的平台,本文以及以後的系列文章將專門針對.NET討論各種編程問題。我將假設你已經熟悉面向對象的編程概念。每一篇文章的內容都聚焦在非選定的特定公共語言運行時編程主題上。所有.NET開發人員必須知道這些主題。
當展示代碼例子時,我必須在支持.NET CLR 的多種語言中選擇一種。我決定使用C#。它是微軟設計的一種新語言。
我的目的是介紹不同的編程主題並就如何實現它們為你提供一些想法。所以,我不想完整的描述每一個主題以及所有與之相關的細微差別。有關主題完整詳細的介紹請參考公共語言運行時或者語言文檔。
真正的面向對象設計
對於使用Win32 SDK的編程人員來說,對大多數操作系統特性的訪問時通過一組從動態鏈接庫輸出的獨立函數實現的。這些獨立的函數從諸如C這樣的非面向對象語言中非常容易調用。但對於一個新的開發人員來說,要面對上千個表面上看來毫無關系的獨立的函數是相當讓人畏懼的。更為困難的是許多函數名是以單詞“Get”開始的(如GetCurrentProcess和GetStockObject)。此外,Win32 API已經歷數年並且微軟添加了新的函數,這些新函數依舊的函數相比。有相似的語義,但提供的特性有些差異。你常常能認出較新的函數,因為它們的名字原來的函數名相似(象CreateWindow/CreateWindowEx,CreateTypeLib/CreateTypeLib2以及我最喜歡的CreatePen/CreatePenIndirect/ExtCreatePen
所有這些問題都使程序員覺得Windows開發很難。隨著.NET平台的出現,微軟終於為叫苦不迭的開發人員提供了一個完全面向對象的開發平台。平台服務現在被分成為單獨的名字空間(如:System.Collections,System.Data,System.IO,SystemSecurity,System.Web等等)並且每一個名字空間包含一組允許訪問平台服務的相關類。
因為類方法可以重載,行為差別不大的方法具有相同的名字,並且只有從原型中才能看出差別來。例如,一個類可能提供三個不同版本的CreatePen方法。所有方法都做相同的事情:即創建一支筆。但是,每一個方法都有不同的參數集並且行為不太一樣。將來微軟還要創建第四個CreatePen方法並且與前面的類方法配合默契。
因為所有的平台服務都通過這種面向對象的方式來實現,所以軟件開發者應該對面向對象的編程有所理解。面向對象的方法還帶來了其它的一些特點,如使用繼承和多態性很容易創建專門版本的基類庫類型。我再次強烈建議要熟練掌握這些概念,這對於使用微軟的.NET框架很重要。
System.Object
在.NET中,每一個對象都是從System.Object派生而來。也就是說下面的兩種類型定義(使用C#)是相同的:
class Jeff {
...
}
和
class Jeff : System.Object {
...
}
因為所有對象都是從System.Object派生出來的,從而可以保證每一個對象具有最小的功能集。表一是System.Object中的公共方法。
公共語言運行時需要所有的對象都要用new操作符創建(調用newobj IL指令)。下列代碼示范了如何創建Jeff類型(已在前面聲明)的對象實例:
Jeff j = new Jeff("ConstructorParam1");
new操作符根據指定的類型需要從堆中分配字節數來創建對象。它初始化對象的開銷成員。每一個對象都會有一些公共語言運行時用來管理對象的附加字節,如對象的許表指針以及對同步快的引用。
調用類的構造函數時,傳遞的參數在new語句中指定(例子中是串"ConstructorParam1")。注意大多數語言會編譯構造函數以便它們調用基類構造函數,但這在公共語言運行時中是不需要的。
在new實現了所有我所提到的操作後,它返回新創建對象的引用。在例子代碼中,這個引用被存儲在變量j中,它的類型是Jeff。
另外,new操作符沒有配對操作(delete)。即沒有方法顯式地釋放或銷毀對象。公共語言運行時提供自動地探測的垃圾回收環境,當對象不再被使用或不再被訪問時自動地釋放和銷毀對象,有關這個主題將在下次的討論中提出。
數據類型的強制轉換
在編程過程當中,對象從一個數據類型到另一個數據類型的強制類型轉換是十分常見的。在這一部分,我將討論對象的強制數據類型轉換規則。為此,先看下列代碼:
System.Object o = new Jeff("ConstructorParam1");
先前的代碼編譯通過並正確執行是因為有一個隱含的強制類型轉換。new操作符返回Jeff的一個引用類型,但o是一個System.Object的引用類型。因為所有的類型(包括Jeff類型)都能被強制轉換為System.Object,隱含的強制類型轉換是成功的。但是,如果執行下面的代碼,就會有編譯器錯誤,因為編譯器不提供基類型到派生類型的強制類型轉換。
Jeff j = o;
為了能通過編譯,必須插入如下的顯式強制類型轉換:
Jeff j = (Jeff) o;
現在就可以編譯通過並成功執行。
再來看另外一個例子:
System.Object o = new System.Object();
Jeff j = (Jeff) o;
第一行創建了一個System.Object類型對象。第二行代碼試圖將System.Object引用類型轉換為Jeff引用類型。兩行代碼都能編譯通過。但是在執行的時候,第二行代碼產生一個InvalidCastException異常,如果捕獲不到這個異常,將強制應用程序終止。
當第二行代碼執行時,公共語言運行時查證o所指的對象就是Jeff類型對象(或任何Jeff派生類型)。如果是,則公共語言運行時允許強制類型轉換。否則,如果o所指的對象與Jeff類型無關,或是一個Jeff的基類,則公共語言運行時會預防這種不安全的強制類型轉換並產生InvalidCastException異常。
[1] [2] [3] 下一頁
C# 使用as操作符提供另一種方法來實現強制類型轉換:
Jeff j = new Jeff(); // 創建一個新的Jeff 對象
System.Object o = j as System.Object; // 強制轉換 j 為一個System.Object對象
// 現在o 指Jeff 對象
as操作符試圖強制轉換一個對象為指定的類型。但與通常的強制轉換不一樣,如果對象的類型強制轉換不成功,結果會是null,as操作符決不會擲出異常。當引用有毛病的強制類型轉換發生時,將產生NullReferenceException異常。下列代碼示范了這種情況。
System.Object o = new System.Object(); //創建一個新的Object 對象
Jeff j = o as Jeff; //強制轉換 o 為一個Jeff對象
// 上面的強制轉換失敗:不會有異常擲出,而j會被置為null
j.ToString(); // 訪問j時產生一個NullReferenceException 異常
除了as操作符以外,C#還提供一個is操作符。它檢查是否一個對象實例與給定的類型兼容並判斷結果是True或是False。Is操作符不會產生異常。
System.Object o = new System.Object();
System.Boolean b1 = (o is System.Object); // b1 是 True
System.Boolean b2 = (o is Jeff); // b2 是 False
注意,如果對象引用是null,is操作符總是返回False,因為得不到對象來檢查其類型。
為了肯定你理解了剛才所說的內容,假設下列兩各類定義存在。
class B {
int x;
}
class D : B {
int x;
}
現在,參見圖二看看哪一行代碼通過編譯並執行成功(ES),哪一行代碼導致編譯器錯誤(CE),哪一行代碼導致公共語言運行時錯誤(RE)。
集合與名字空間
類型集可以被分組成集合(一個或多個文件集)並且被展開。在一個集合中可以只存在單獨的名字空間。對應用程序開發人員來說,名字空間就像有關聯的類型的邏輯分組。例如,基本類庫集合包含許多名字空間。System名字空間包括Object基類型、Byte、Int32、Exception、Math和Delegate之類的核心低級類型,而System.Collections名字空間包括的類型如AarryList、BitAarry、Queue和Stack。
對於編譯器來說,名字空間只不過是名字較長的的類型名,以及其唯一性是用句點分隔某些符號名來保證的。對於編譯器而言,System名字空間中的Object類型只不過是用一個叫做System.Object的類型來表示。同樣,System.Collections名字空間中的Queue類型簡單地用標示符System.Collections.Queue來表示。
運行時引擎不知道關於名字空間的任何信息。當你訪問一個類型時,公共語言運行時只需要知道完整的類型名字以及哪一個集合包含這個類型的定義,以便公共語言運行時能正確加載集合,從而找到要訪問的類型並處理之。
編程人員通常都想用最簡練的方法來表達算法,但用完全限定名引用每一個類類型的話極其麻煩。因此,許多編程語言提供一條語句來指示編譯器添加各種前綴到類型名,直到實現一個匹配。當用C#編程時,我經常在源代碼的最前面是用下面的語句:
using System;
當我在代碼中引用一個類型時,編譯器需要保證這個類型被定義過並且我的代碼要以正確的方式訪問這個類型。如果編譯器不能找到指定的類型,它試圖將“System.”添加到類型名並檢查產生的類型名字是否與現存的類型名匹配。前面的代碼行允許我在代碼中使用Object,並且編譯器將自動將名字展開為System.Object。我肯定你能輕松想象這樣省去了多少鍵盤輸入。
當進行類型定義的檢查時,編譯器必須知道哪一個集合包含了這個類型,以便這個集合的信息和類型信息能被送到結果文件中。為了獲得集合信息,你必須將定義了任何引用類型的集合傳給編譯器。
正如你所設想的一樣,這種設計存在一些潛在的問題。為了編程方便,你應該避免創建名字沖突的類型。但是在某些情況中,它完全不可能。.NET鼓勵組件重用。你的應用程序可以利用Microsoft所創建的組件,同時,你也可以用Richter創建的另一個組件。這些公司的組件可能都提供了一個叫做FooBar的類型,-Microsoft的FooBar所做的事情與Richter的FooBar所做的事情完全不同。在這種情況下,你無法控制類類型的命名。為了引用Microsoft的FooBar,你使用Microsoft.FooBar,為了引用Richter的FooBar,你使用Richter.FooBar。
在下列的代碼中,對FooBar的引用是不明確的。編譯器報告一個錯誤也就罷了,但是實際上C#編譯器挑選FooBar類型的一種可能的情況;直到運行時你才能發現問題:
using Microsoft;
using Richter;
class MyApp {
method void Hi() {
FooBar f = new FooBar(); // Ambiguous, compiler picks
}
}
為了排除這種不明確的引用,你必須顯式地告訴編譯器,你想創建哪一個FooBar。
using Microsoft;
using Richter;
class MyApp {
method void Hi() {
Richter.FooBar f = new Richter.FooBar(); // 明確引用
}
}
另一種語句形式是允許你為單獨類型創建別名,如果你只使用名字空間中的幾種類型並且不想用所有的名字空間類型污染整個名字空間的話,這種方法很方便。下列代碼示范了另一種解決類型不明確問題的方法。
// 定義RichterFooBar
上一頁 [1] [2] [3] 下一頁
為Richter.FooBar的別名
using RichterFooBar = Richter.FooBar;
class MyApp {
method void Hi() {
RichterFooBar f = new RichterFooBar(); // 不會出錯
}
}
這種方法對於消除類型歧義有用,但不盡人意的地方仍然存在。假設澳大利亞的飛镖(Boomerang)公司(簡稱ABC)和阿拉斯加的船舶(Boat)公司(也簡稱ABC)兩家公司各自創建了一個類型。可能兩家公司都創建了叫做ABC的名字空間,在名字空間中包含一個叫做BuyProduct的類型。任何試圖開發需購買飛镖和船舶應用程序的人將會陷入麻煩,除非編程語言提供編程方法來區分兩家公司的集合-而不僅僅是兩家公司的名字空間。
不幸的是C#語言只支持名字空間,並不提供任何方式來詳細說明集合。但實際上碰到這個問題的時候並不多,屬於罕見問題。如果是設計希望第三方使用的組件類型。推薦在一個名字空間中定義類型,以便編譯器輕松排除類型問題。事實上,應該使用公司全名(不是只取首字母)作為最高級名字空間名來降低沖突的可能性。你能看到Microsoft使用“Microsoft”作為名字空間。
在代碼中寫一個名字空間聲明來創建名字空間是一件很簡單的事情。就像下面這樣:
namespace CompanyName { // CompanyName
class A { // CompanyName.A
class B { ... } // CompanyName.A.B
}
namespace X { // CompanyName.X
class C { ... } // CompanyName.X.C
}
}
注意名字空間是隱含的公共類型(public)。不能通過任何訪問修飾符改變這一點。但可以在內部的名字空間中定義類型(不能在集合外面使用)或者在公共的名字空間中定義類型(能被任何集合訪問)。名字空間只表示邏輯上的限制策略,可訪問性和包裝是通過將名字空間放入一個集合來完成的。
下一次的討論中,我將闡述所有.NET編程人員必須掌握的簡單數據類型、引用類型和數值類型。對於每一個.NET程序員來說,透徹理解數值類型是非常重要的。
上一頁 [1] [2] [3]