C# 2.0對現有文法的改進
作者:lover_P
[自序]
儘管Microsoft Visual Studio .NET 2005(過去好像叫Visual Studio .NET 2004)一再延遲其發布日期,但廣大開發人員對其的猜測以及各種媒體對其各方面的“曝光”也似乎已經充斥了網路。但與C#有關的文章似乎無外乎兩個方面:VS.NET 2005 IDE特性、介紹C# 2.0中引入的“四大特性(泛型、匿名方法、迭代器和不完整類型)”。對IDE的研究我不想談了,微軟就是微軟,他的視窗應用程式總是沒得說的。而就語言本身的改進來說,在我聽完了Anders Hejlsberg在Microsoft Professional Developers Conference 2003(2003.10, Los Angeles, CA)上的演講後,發現除了這四大特性之外,還有一些鮮為人知的特性,甚至在微軟官方的文檔《C# Language Specification Version 2.0》中都沒有提到。而這些特性卻更加提高了語言的邏輯性。於是我編寫了大量實際程式研究了這些特性,終於著成本文。本打算髮表在《CSDN開發高手》雜誌上的,可無奈水平有限,只能留在這個角落裡貽笑大方了。希望能夠對那些對C#語言有著濃厚興趣的朋友有些協助。
——lover_P 於北京工業大學1號樓221寢室
[修訂說明]
- 2004-08-24 下午
第二次修訂。感謝不知名的 fans1 朋友為我指出:原來using ns = namespace的文法在C#1.x中就支援。現在已經刪除了。希望自己以後能夠小心些…… '-_-
- 2004-08-24
第一次修訂。修改了大量的錯別字和文法錯誤。添加了對global限定符的介紹。
[本文]
微軟在其即將推出的C#2.0(Visual C# Whidbey)中,添加了許多令程式員感到振奮的新特性。除了泛型(Generic)、迭代器(Iterator)、匿名方法(Anonmynous)和不完整類型(Partial Type)等重大的改進,還對現有的文法細節進行了很大的改進,極大地方便了.NET架構程式設計的工作,並且進一步加強了C#語言專屬的高邏輯性。在本文中,我將向大家介紹一下這些改進。(文中C#指代的是C#1.2及以前的版本,而C#2.0指代的是微軟尚未正式推出的C# Whidbey;文章中的所有代碼均在版本號碼為8.00.30703.4的C#編譯器下進行了測試,標有*的錯誤訊息得自版本號碼為7.10.3052.4的C#編譯器。)
[內容]
- 靜態類
- 屬性的可訪問性限定
- global限定符
- 編譯器指令
- 固定大小緩衝區
- 參考文獻
靜態類
使用C#進行.NET架構程式設計的人應該都知道,無法將一個類聲明為靜態。例如,下面的類聲明:
public static class A {
static int i;
}
在C#中是無效的,當我們嘗試編譯這段代碼時會得到下面的編譯錯誤*:
error CS0106: 修飾符“static”對該項無效
由於無法用static修飾符修飾一個類,我們在類中總是能夠既聲明靜態成員又聲明執行個體成員。這無疑會帶來很大的靈活性。但是,如果我們希望一個類是靜態,也就是希望強制要求這個類中的所有成員都應該為靜態,就無能為力了,唯一能做的就是自己注意將所有的成員聲明為static。當我們忘記對一個本應是靜態成員使用static修飾符(儘管這是一個“低級錯誤”,但仍有可能發生)時,將會產生難以預料的錯誤。最重要的是,對於一個邏輯上的靜態類(所有成員均使用static修飾符進行聲明的類),我們甚至可以聲明該類的一個變數並使用new操作符產生該類的執行個體!這顯然不是我們所期望的。
而在C#2.0中,則提供了靜態類這一概念,允許static修飾符對類進行修飾,上面的代碼得以通過編譯。如果一個類聲明中包含了static修飾符,那麼這個類中的所有成員將被強制要求聲明為靜態。這時,如果我們故意在類中聲明執行個體成員或是不小心忘記了成員聲明中的static修飾符,如下面代碼所示:
public static class A {
int i;
}
則編譯器會報告錯誤:
error CS0708: 'A.i': cannot declare instance members in a static class
同時,如果我們聲明該類的變數或是試圖建立該類的一個執行個體時,如下面的代碼:
public class Test {
A a; // error CS0723
void Foo() {
a = new A(); // error CS0712
}
}
則會得到下面的兩個編譯錯誤:
error CS0723: Cannot declare variable of static type 'A'
error CS0712: Cannot create an instance of the static class 'A'
很顯然,C#2.0中對靜態類的支援極大程度地避免了我們在書寫程式中的意外失誤,尤其是加強了靜態類的邏輯性,提高了代碼的可讀性。
屬性的可訪問性限定
C#為我們提供了相當方便的屬性定義,使得我們可以像訪問類的公有變數成員那樣訪問類的屬性,但還可以同時得到像訪問函數那樣的安全性。然而,C#只允許屬性的設定動作(set{...})和擷取動作(get{...})具有相同的可訪問性(由屬性聲明中的public、internal和private等修飾符指定)。那麼,當我們希望允許從任何程式集中的類擷取一個特定類的屬性,但只允許該類所在的程式集或該類的私人成員才能設定該屬性時,我們只能將這個屬性聲明為公有且唯讀(即使用public修飾符聲明但只有get{}域),而內部的或私人的成員只能通過設定與該屬性相關的內部或私人的變數成員的值來完成屬性的設定工作:
public class A {
int _intValue; // 與屬性相關的一個int類型的成員變數
// 公有且唯讀屬性,允許任何類擷取該屬性的值:
public int Value {
get { return _intValue; }
}
// 下面的方法需要設定上面的屬性,
// 但只能通過訪問私人成員變數來完成,
// 並且要另外進行錯誤處理
private void SomeMethod() {
int i;
// ......
// 下面的if-else語句僅用來設定屬性值:
if(0 < i && i < 10) {
_intValue = i;
}
else {
// 錯誤處理
}
}
}
很明顯,這種做法是非常麻煩的。如果在多個地方改變了成員變數的值會使代碼變得冗長不可讀,還很有可能會產生錯誤,譬如該類有另外一個方法:
private void AnotherMethod() {
int i;
// ......
// 下面的if-else語句僅用於設定屬性值,
// 但其對i的區間檢測發生了錯誤
if(0 < i && i <= 10) { // 注意這裡的 <= 運算子
_intValue = i;
}
// 並且沒有進行錯誤處理
// ......
}
上面的方法對將要賦給私人變數成員的值的檢查區間是錯誤的,這種錯誤是很有可能發生的。一旦調用了這個方法,_intValue很有可能具有錯誤的值,而訪問了Value屬性的外部程式集將會出現邏輯錯誤。這種錯誤的解決是相當困難的。並且,如果一個小組中的其他成員負責設計同一程式集中其他的類,要求他們在方法中書寫如此大量的代碼並要進行錯誤檢查是不人道的。
當然,我們可能會想到將這種設定屬性值的工作放到一個內部方法中集中進行:
// 程式集內部的類或該類的私人成員通過
// 下面的內部方法對上面的屬性進行設定工作
internal void _setIntValue(int newValue) {
if(0 < newValue && newValue < 10) {
_intValue = newValue;
}
else {
throw new System.InvalidArgumentException (
“The new value must greater than 0 and less than 10”
);
}
}
// 下面的方法需要對上述屬性進行設定
private void SomeMethod() {
int i;
// ......
_setIntValue(i); // 通過調用內部方法進行
}
這樣做雖然避免了邏輯錯誤的出現(至少使出現了錯誤時的解決工作變得容易),但其可讀性仍然不理想,尤其是邏輯性很差,與“屬性”本身的意義相去甚遠。
然而C#2.0允許我們對屬性的get{}和set{}域分別設定可訪問性,我們能夠將上面的代碼簡單地寫作:
public class A {
int _intValue; // 與屬性相關的一個int類型的成員變數
// 公有的屬性,
// 允許任何類擷取該屬性的值,
// 但只有程式集內部的類和該類中的私人成員
// 能夠設定屬性的值
public int Value {
get {
return _intValue;
}
internal set {
if(0 < value && value < 10) {
_intValue = value;
}
else {
throw new System.InvalidArgumentException (
“The new value must greater than 0 and less than 10”
);
}
}
} // property
// 下面的方法需要對上述屬性進行設定
private void SomeMethod() {
int i;
// ......
Value = i;
}
}
尤其在程式集中的其他類的成員中訪問該屬性時相當方便:
// 這是同一個程式集中另外的一個類:
public class B {
public A SomeMethod() {
A a = new A();
a.Value = 8; // 這裡對屬性進行設定,方便!
return a;
}
}
可以看出,能夠對屬性的擷取和設定作業分別設定可訪問性限定極大地增強了C#程式的可讀性和語言邏輯性,寫出的程式也具有更強的可維護性。
global限定符
在C#2.0以前,在使用命名空間時還有一個非常細微的問題。這就是C#命名空間的尋找方式。考慮下面這個例子:
using System;
namespace MyNamespace {
namespace System {
public class MyConsole {
public void WriteLine(String str) {
System.Console.WriteLine(str); // 注意這一行!
}
}
}
}
這裡我在自己的命名空間內聲明了一個System命名空間,並在其中類比了控制台類。我希望它通過調用System.Console類的方法來類比控制台的行為。這個程式片斷是沒有文法問題的,當仍然不能通過編譯。其主要原因是C#的命名空間範圍和普通變數的範圍規則類似,總是尋找最近的聲明,並且內部聲明可以覆蓋外部聲明。因此,這段代碼中標有注釋的一行在編譯的時候編譯器會提示找不到類MyNamespace.System.Console——它試圖在我自己的命名空間裡找System.Console類!
在C#2.0以前,這個問題對於類庫的設計者來說是非常頭疼的。唯一的解決方案就是盡量在自己的命名空間內不使用全域命名空間中的名字。但是,由於類庫開發人員眾多,難免會出現類似的情況;而且,這樣做還會導致既是在自己的命名空間中也不能使用可讀性高而又簡潔的名字,這無疑傷害了語言的邏輯性和簡潔性。
然而,C#2.0引入了global關鍵字,允許我們從全域選取命名空間。下面這個圖示從.net命名空間的布局說明了global關鍵字的地位:
圖示1:global關鍵字的地位
在C#1.x中:
+++ Who's the root? +++
|
+- System (namespace)
| |
| +- Console (class)
| +WriteLine (method)
| +- Int32 (struct)
| +- ...
|
+- MyNameSpace (namespace)
|
+- System ([sub]namespace)
+ MyConsole (class)
在C#2.0中
global (!!!ROOT!!!)
|
+- System (namespace)
| |
| +- Console (class)
| +WriteLine (method)
| +- Int32 (struct)
| +- ...
|
+- MyNameSpace (namespace)
|
+- System ([sub]namespace)
+ MyConsole (class)
這樣一來,我們就能通過使用global關鍵字輕易地解決命名空間的衝突問題。上面的例子也就能夠重寫為:
using System;
namespace MyNamespace {
namespace System {
public class MyConsole {
public void WriteLine(String str) {
global.System.Console.WriteLine(str); // 注意這一行!
}
}
}
}
編譯器指令
在我們調試C#程式時,經常會聲明一些臨時變數用來監測程式狀態,並在調試完成後將這些聲明刪除。而當我們聲明了這樣的臨時變數,在調試過程中卻沒有用到的時候,我們通常會得到大量的如:
warning CS0168: The variable 'exp' is declared but never used
的警告。然而,我們很清楚這樣的警告是無害的。同樣,很多其他時候我們也會得到一些警告,但我們不得不從大量的無害的警告中尋找我們需要的錯誤訊息。
然而,C#2.0為我們提供了一條新的編譯器指令:pragma warning,使得我們能夠在一段代碼中禁止一些我們確認無害的警告(通過指定警告的編號)。以前,這種工作只能由特定的編譯器選項(譬如Microsoft Visual C#編譯器的/nowarn)或相應的IDE選項(如Microsoft Visual Studio .NET 2003中的項目屬性頁面中的相應選項)來完成。而且,通過編譯環境來隱藏警告將導致在編譯整個項目或整個源檔案的過程中所有相應的警告都會被隱藏。如果我們僅僅知道在某一個代碼塊中一個警告是無害的,但對於代碼的其它部分,我們還是希望看到這個警告訊息時,這種做法就無能為力了。這個時候,我們只有通過pragma warning指令來命令編譯器僅僅隱藏某一個代碼塊中相應的警告。我們可以用下列代碼來禁止產生上面的例子中所述的“未使用參數”的警告:
public class Test {
public void SomeMethod() {
// 下面的編譯器指令禁止了“未使用參數”的警告:
#pragma warning disable 0168
int tempStatus;
// ......
// 下面的編譯器指令重新允許產生“未使用參數”的警告:
#pragma warning restore 0168
}
}
這樣,當編譯器編譯SomeMethod()方法時,將不會產生上述的“未使用參數”的警告,但在編譯其它程式碼片段時,仍然會產生該警告,因為我們用#pragma warning restore指令重新開啟了該警告。
固定大小緩衝區
最後,除了上述的一些特性外,C#2.0還提供了“固定大小緩衝區(Fixed Size Buffers)”的新特性。即像C語言那樣可以在結構中聲明一個固定大小的數組,這通過System.Runtime.CompilerServices.FixedBufferAttribute屬性和Fixed關鍵字實現(參見參考文獻第26頁):
[System.Runtime.CompilerServices.FexedBuffer]
public struct Buffer {
public fixed char Buffer[128];
}
但由於我所使用的編譯器尚未支援這一特性,手頭又沒有相應的資料,在此就不做介紹了。
以上是我對C#2.0中除了泛型、迭代器、匿名方法和部分型別等重大改進之外的一些對現有特性進行的改進的簡要介紹。這些改進看起來很細微,卻極大程度地增強了C#語言的邏輯性,使得我們能夠寫出更加漂亮且可維護性更強的代碼。我的介紹是非常簡略的,甚至可能有錯誤,希望大家指教。(連絡方式:lyb_dotNET@hotmail.com)
參考文獻
[1]《Visual C# Whidbey: Language Ehancements》,Anders Hejlsberg在Microsoft Professional Developers Conference 2003(2003.10, Los Angeles, CA)上的演講及其PowerPoint文檔。
[回頂端]