C# 2.0:使用匿名方法、迭代程序和局部類來創建優雅的代碼

發佈日期: 11/10/2004 | 更新日期: 11/10/2004

Juval Lowy

本文基於 Microsoft Visual Studio 2005 的預發佈版本,它以前的代碼名稱爲“Whidbey”。此處所包含的任何信息都可能會改變。

本文討論:

遍歷集合

跨文件類定義

與委託一起使用的匿名方法

Visual Studio 2005 中的其他 C# 新功能

本文使用下列技術:

C# 和 Visual Studio

可以在此下載代碼:

C20.exe (164KB)

*
本頁內容
迭代程序迭代程序
迭代程序實現迭代程序實現
遞歸迭代遞歸迭代
局部類型局部類型
匿名方法匿名方法
將參數傳遞到匿名方法將參數傳遞到匿名方法
匿名方法實現匿名方法實現
一般匿名方法一般匿名方法
匿名方法示例匿名方法示例
委託推理委託推理
屬性和索引可見性屬性和索引可見性
靜態類靜態類
全局命名空間限定符全局命名空間限定符
內聯警告內聯警告
小結小結

熱衷於 C# 語言的人會喜歡上 Visual C# 2005。Visual Studio 2005 爲 Visual C# 2005 帶來了大量令人興奮的新功能,例如泛型、迭代程序、局部類和匿名方法等。雖然泛型是人們最常談到的也是預期的功能,尤其是在熟悉模板的 C++ 開發人員中間,但是其他的新功能同樣是對Microsoft .NET開發寶庫的重要補充。與 C# 的第一個版本相比,增加這些功能和語言將會提高您的整體生產效率,從而使您能夠以更快的速度寫出更加簡潔的代碼。有關泛型的一些背景知識,您應該看一看提要欄“什麼是泛型?”。

迭代程序

在 C# 1.1 中,您可以使用 foreach 循環來遍歷諸如數組、集合這樣的數據結構:

string[] cities = {"New York","Paris","London"};
foreach(string city in cities)
{
   Console.WriteLine(city);
}

實際上,您可以在 foreach 循環中使用任何自定義數據集合,只要該集合類型實現了返回 IEnumerator 接口的 GetEnumerator 方法即可。通常,您需要通過實現 IEnumerable 接口來完成這些工作:

public interface IEnumerable 
{
   IEnumerator GetEnumerator();
}
public interface IEnumerator 
{
   object Current{get;}
   bool MoveNext();
   void Reset();
}

在通常情況下,用於通過實現 IEnumerable 來遍歷集合的類是作爲要遍歷的集合類型的嵌套類提供的。此迭代程序類型維持了迭代的狀態。將嵌套類作爲枚舉器往往較好,因爲它可以訪問其包含類的所有私有成員。當然,這是迭代程序設計模式,它對迭代客戶端隱藏了底層數據結構的實際實現細節,使得能夠在多種數據結構上使用相同的客戶端迭代邏輯,如 1 所示。


1 迭代程序設計模式

此外,由於每個迭代程序都保持單獨的迭代狀態,所以多個客戶端可以執行單獨的併發迭代。通過實現 IEnumerable,諸如數組和隊列這樣的數據結構可以支持這種超常規的迭代。在 foreach 循環中生成的代碼調用類的 GetEnumerator 方法簡單地獲得一個 IEnumerator 對象,然後將其用於 while 循環,從而通過連續調用它的 MoveNext 方法和當前屬性遍歷集合。如果您需要顯式地遍歷集合,您可以直接使用 IEnumerator(不用求助於 foreach 語句)。

但是使用這種方法有一些問題。首先,如果集合包含值類型,則需要對它們進行裝箱和拆箱才能獲得項,因爲 IEnumerator.Current 返回一個對象。這將導致潛在的性能退化和託管堆上的壓力增大。即使集合包含引用類型,仍然會產生從對象向下強制類型轉換的不利結果。雖然大多數開發人員不熟悉這一特性,但是在 C# 1.0 中,實際上不必實現 IEnumerator 或 IEnumerable 就可以爲每個循環實現迭代程序模式。編譯器將選擇調用強類型化版本,以避免強制類型轉換和裝箱。結果是,即使在 1.0 版本中,也可能沒有導致性能損失。

爲了更好地闡明這個解決方案並使其易於實現,Microsoft .NET 框架 2.0 在 System.Collections.Generics 命名空間中定義了一般的類型安全的 IEnumerable <ItemType> 和 IEnumerator <ItemType> 接口:

public interface IEnumerable<ItemType>
{   
   IEnumerator<ItemType> GetEnumerator();
} 
public interface IEnumerator<ItemType> : IDisposable
{
   ItemType Current{get;}
   bool MoveNext();
}     

除了利用泛型之外,新的接口與其前身還略有差別。與 IEnumerator 不同,IEnumerator <ItemType> 是從 IDisposable 派生而來的,並且沒有 Reset 方法。圖 2 中的代碼顯示了實現 IEnumerable <string> 的簡單 city 集合,而圖 3 顯示了編譯器在跨越 foreach 循環的代碼時如何使用該接口。圖 2 中的實現使用了名爲 MyEnumerator 的嵌套類,它將一個引用作爲構造參數返回給要枚舉的集合。MyEnumerator 清楚地知道 city 集合(本例中的一個數組)的實現細節。MyEnumerator 類使用 m_Current 成員變量維持當前的迭代狀態,此成員變量用作數組的索引。

第二個問題也是更難以解決的問題,就是迭代程序的實現。雖然對於簡單的例子(如圖 3所示),實現是相當簡單的,但是對於更高級的數據結構,實現將非常複雜,例如二叉樹,它需要遞歸遍歷,並需在遞歸時維持迭代狀態。另外,如果需要各種迭代選項,例如需要在一個鏈接表中從頭到尾和從尾到頭選項,則此鏈接表的代碼就會因不同的迭代程序實現而變得臃腫。這正是設計 C# 2.0 迭代程序所要解決的問題。通過使用迭代程序,您可以讓 C# 編譯器爲您生成 IEnumerator 的實現。C# 編譯器能夠自動生成一個嵌套類來維持迭代狀態。您可以在一般集合或特定於類型的集合中使用迭代程序。您需要做的只是告訴編譯器在每個迭代中產生的是什麼。如同手動提供迭代程序一樣,您需要公開 GetEnumerator 方法,此方法通常是通過實現 IEnumerable 或 IEnumerable <ItemType> 來公開的。

您可以使用新的 C# 的 yield return 語句告訴編譯器產生什麼。例如,下面的代碼顯示瞭如何在 city 集合中使用 C# 迭代程序來代替圖 2 中的手動實現:

public class CityCollection : IEnumerable<string>
{
   string[] m_Cities = {"New York","Paris","London"};
   public IEnumerator<string> GetEnumerator()
   {
      for(int i = 0; i<m_Cities.Length; i++)
         yield return m_Cities[i];
   }
}

您還可以在非一般集合中使用 C# 迭代程序:

public class CityCollection : IEnumerable
{
   string[] m_Cities = {"New York","Paris","London"};
   public IEnumerator GetEnumerator()
   {
      for(int i = 0; i<m_Cities.Length; i++)
         yield return m_Cities[i];
   }
}

此外,您還可以在完全一般的集合中使用 C# 迭代程序,如圖 4 所示。當使用一般集合和迭代程序時,編譯器從聲明集合(本例中的 string)所用的類中型知道 foreach 循環內 IEnumerable <ItemType> 所用的特定類型:

LinkedList<int,string> list = new LinkedList<int,string>();
/* Some initialization of list, then  */
foreach(string item in list)
{   
   Trace.WriteLine(item);
}

這與任何其他從一般接口進行的派生相似。如果出於某些原因想中途停止迭代,請使用 yield break 語句。例如,下面的迭代程序將僅僅產生數值 1、2 和 3:

public IEnumerator<int> GetEnumerator()
{
   for(int i = 1;i< 5;i++)
   {
      yield return i;
      if(i > 2)
         yield break;
   }
}

您的集合可以很容易地公開多個迭代程序,每個迭代程序都用於以不同的方式遍歷集合。例如,要以倒序遍歷 CityCollection 類,提供了名爲 Reverse 的 IEnumerable <string> 類型的屬性:

public class CityCollection 
{   
   string[] m_Cities = {"New York","Paris","London"};
   public IEnumerable<string> Reverse
   {
      get
      {
         for(int i=m_Cities.Length-1; i>= 0; i--)
            yield return m_Cities[i];         
      }
   }
}

這樣就可以在 foreach 循環中使用 Reverse 屬性:

CityCollection collection = new CityCollection();
foreach(string city in collection.Reverse)
{   
   Trace.WriteLine(city);
}

對於在何處以及如何使用 yield return 語句是有一些限制的。包含 yield return 語句的方法或屬性不能再包含其他 return 語句,因爲這樣會錯誤地中斷迭代。不能在匿名方法中使用 yield return 語句,也不能將 yield return 語句放到帶有 catch 塊的 try 語句中(也不能放在 catch 塊或 finally 塊中)。

返回頁首

迭代程序實現

編譯器生成的嵌套類維持迭代狀態。當在 foreach 循環中(或在直接迭代代碼中)首次調用迭代程序時,編譯器爲 GetEnumerator 生成的代碼將創建一個帶有 reset 狀態的新的迭代程序對象(嵌套類的一個實例)。在 foreach 每次循環調用迭代程序的 MoveNext 方法時,它都從前一次 yield return 語句停止的地方開始執行。只要 foreach 循環執行,迭代程序就會維持它的狀態。然而,迭代程序對象(以及它的狀態)在多個 foreach 循環之間並不保持一致。因此,再次調用 foreach 是安全的,因爲您將使新的迭代程序對象開始新的迭代。這就是爲什麼 IEnumerable <ItemType> 沒有定義 Reset 方法的原因。

但是嵌套迭代程序類是如何實現的呢?並且如何管理它的狀態呢?編譯器將一個標準方法轉換成一個可以被多次調用的方法,此方法使用一個簡單的狀態機在前一個 yield return 語句之後恢復執行。您需要做的只是使用 yield return 語句指示編譯器產生什麼以及何時產生。編譯器具有足夠的智能,它甚至能夠將多個 yield return 語句按照它們出現的順序連接起來:

public class CityCollection : IEnumerable<string>
{
   public IEnumerator<string> GetEnumerator()
   {
      yield return "New York";
      yield return "Paris";
      yield return "London";
   }
}

讓我們看一看在下面幾行代碼中顯示的該類的 GetEnumerator 方法:

public class MyCollection : IEnumerable<string>
{
   public IEnumerator<string> GetEnumerator()
   {
      //Some iteration code that uses yield return 
   }
}

當編譯器遇到這種帶有 yield return 語句的類成員時,它會插入一個名爲 GetEnumerator$<random unique number>__IEnumeratorImpl 的嵌套類的定義,如圖 5 中 C# 僞代碼所示。(記住,本文所討論的所有特徵 — 編譯器生成的類和字段的名稱 — 是會改變的,在某些情況下甚至會發生徹底的變化。您不應該試圖使用反射來獲得這些實現細節並期望得到一致的結果。)嵌套類實現了從類成員返回的相同 IEnumerable 接口。編譯器使用一個實例化的嵌套類型來代替類成員中的代碼,將一個指向集合的引用賦給嵌套類的 <this> 成員變量,類似於圖 2 中所示的手動實現。實際上,該嵌套類是一個提供了 IEnumerator 的實現的類。

返回頁首

遞歸迭代

當在像二叉樹或其他任何包含相互連接的節點的複雜圖形這樣的數據結構上進行遞歸迭代時,迭代程序才真正顯示出了它的優勢。通過遞歸迭代手動實現一個迭代程序是相當困難的,但是如果使用 C# 迭代程序,這將變得很容易。請考慮圖 6 中的二叉樹。這個二叉樹的完整實現是本文所提供的源代碼的一部分。這個二叉樹在節點中存儲了一些項。每個節點均擁有一個一般類型 T(名爲Item)的值。每個節點均含有指向左邊節點的引用和指向右邊節點的引用。比 Item 小的值存儲在左邊的子樹中,比 Item 大的值存儲在右邊的子樹中。這個樹還提供了 Add 方法,通過使用參數限定符添加一組開放式的 T 類型的值:

public void Add(params T[] items);

這棵樹提供了一個 IEnumerable <T> 類型的名爲 InOrder 的公共屬性。InOrder 調用遞歸的私有幫助器方法 ScanInOrder,把樹的根節點傳遞給 ScanInOrder。ScanInOrder 定義如下:

IEnumerable<T> ScanInOrder(Node<T> root);

它返回 IEnumerable <T> 類型的迭代程序的實現,此實現按順序遍歷二叉樹。對於 ScanInOrder 需要注意的一件事情是,它通過遞歸遍歷這個二叉樹的方式,即使用 foreach 循環來訪問從遞歸調用返回的 IEnumerable <T>。在順序 (in-order) 迭代中,每個節點都首先遍歷它左邊的子樹,接着遍歷該節點本身的值,然後遍歷右邊的子樹。對於這種情況,需要三個 yield return 語句。爲了遍歷左邊的子樹,ScanInOrder 在遞歸調用(它以參數的形式傳遞左邊的節點)返回的 IEnumerable <T>上使用 foreach 循環。一旦 foreach 循環返回,就已經遍歷併產生了左邊子樹的所有節點。然後,ScanInOrder 產生作爲迭代的根傳遞給其節點的值,並在 foreach 循環中執行另一個遞歸調用,這次是在右邊的子樹上。通過使用屬性 InOrder,可以編寫下面的 foreach 循環來遍歷整個樹:

BinaryTree<int> tree = new BinaryTree<int>();
tree.Add(4,6,2,7,5,3,1);
foreach(int num in tree.InOrder)
{
   Trace.WriteLine(num);
}
// Traces 1,2,3,4,5,6,7

可以通過添加其他的屬性用相似的方式實現前序 (pre-order) 和後序 (post-order) 迭代。雖然以遞歸方式使用迭代程序的能力顯然是一個強大的功能,但是在使用時應該保持謹慎,因爲可能會出現嚴重的性能問題。每次調用 ScanInOrder 都需要實例化編譯器生成的迭代程序,因此,遞歸遍歷一個很深的樹可能會導致在幕後生成大量的對象。在對稱二叉樹中,大約有 n 個迭代程序實例,其中 n 爲樹中節點的數目。在任一特定的時刻,這些對象中大約有 log(n) 個是活的。在具有適當大小的樹中,許多這樣的對象會使樹通過 0 代 (Generation 0) 垃圾回收。也就是說,通過使用棧或隊列維護一列將要被檢查的節點,迭代程序仍然能夠方便地遍歷遞歸數據結構(例如樹)。

返回頁首

局部類型

C# 1.1 要求將類的全部代碼放在一個文件中。而 C# 2.0 允許將類或結構的定義和實現分開放在多個文件中。通過使用 new partial 關鍵字來標註分割,可以將類的一部分放在一個文件中,而將另一個部分放在一個不同的文件中。例如,可以將下面的代碼放到文件 MyClass1.cs 中:

public partial class MyClass
{
   public void Method1()
   {...}
}

在文件 MyClass2.cs 中,可以插入下面的代碼:

public partial class MyClass
{
   public void Method2()
   {...}
   public int Number;
}

實際上,可以將任一特定的類分割成任意多的部分。局部類型支持可以用於類、結構和接口,但是不能包含局部枚舉定義。

局部類型是一個非常有用的功能。有時,我們需要修改機器生成的文件,例如 Web 服務客戶端包裝類。然而,當重新生成此包裝類時,對該文件的修改將會被丟棄。通過使用局部類,可以將這些改變分開放在單獨的文件中。ASP.NET 2.0 將局部類用於 code-beside 類(從 code-behind 演變而來),單獨存儲頁面中機器生成的部分。Windows 窗體使用局部類來存儲 InitializeComponent 方法的可視化設計器輸出以及成員控件。通過使用局部類型,兩個或者更多的開發人員可以工作在同一個類型上,同時都可以從源控制中籤出其文件而不互相影響。

您可以問自己,如果多個不同的部分對同一個類做出了相互矛盾的定義會出現什麼樣的後果?答案很簡單。一個類(或一個結構)可能具有兩個不同的方面或性質:累積性的 (accumulative) 和非累積性的 (non-accumulative)。累積性的方面是指類可以選擇添加它的各個部分,比如接口派生、屬性、索引器、方法和成員變量。

例如,下面的代碼顯示了一個部分是如何添加接口派生和實現的:

public partial class MyClass
{}
public partial class MyClass : IMyInterface
{
   public void Method1()
   {...}
   public void Method2()
   {...}
}

非累積性的方面是指一個類型的所有部分都必須一致。無論這個類型是一個類還是一個結構,類型可見性(公共或內部)和基類都是非累積性的方面。例如,下面的代碼不能編譯,因爲並非 MyClass 的所有部分都出現在基類中:

public class MyBase
{}
public class SomeOtherClass
{}
public partial class MyClass : MyBase
{}
public partial class MyClass : MyBase
{}
//Does not compile
public partial class MyClass : SomeOtherClass
{}

除了所有的部分都必須定義相同的非累積性部分以外,只有一個部分能夠重寫虛方法或抽象方法,並且只有一個部分能夠實現接口成員。

C# 2.0 是這樣來支持局部類型的:當編譯器構建程序集時,它將來自多個文件的同一類型的各個部分組合起來,並用 Microsoft 中間語言 (Microsoft intermediate language, MSIL) 將這些部分編譯成單一類型。生成的 MSIL 中不含有哪一部分來自哪個文件的記錄。正如在 C# 1.1 中一樣,MSIL 不含有哪個文件用於定義哪個類型的記錄。另外值得注意的是,局部類型不能跨越程序集,並且通過忽略其定義中的 partial 限定符,一個類型可以拒絕包含其他部分。

因爲編譯器所做的只是將各個部分累積,所以一個單獨的文件可以包含多個部分,甚至是包含同一類型的多個部分(儘管這樣做的意義值得懷疑)。

在 C# 中,開發人員通常根據文件所包含的類來爲文件命名,這樣可以避免將多個類放在同一個文件中。在使用局部類型時,我建議在文件名中指示此文件包含哪個類型的哪些部分(例如 MyClassP1.cs、MyClassP2.cs),或者採用其他一致的方式從外形上指示源文件的內容。例如,Windows 窗體設計人員將用於該窗體的局部類的一部分存放在 Form1.cs 中,並將此文件命名爲 Form1.Designer.cs。

局部類的另一個不利之處是,當開始接觸一個不熟悉的代碼基時,您所維護類型的各個部分可能遍佈在整個項目的文件中。在這種情況下,建議您使用 Visual Studio Class View,因爲它可以將一個類型的所有部分積累起來展示給您,並允許您通過單擊它的成員來導航各個不同的部分。導航欄也提供了這個功能。

返回頁首

匿名方法

C# 支持用於調用一個或多個方法的委託 (delegate)。委託提供運算符和方法來添加或刪除目標方法,它也可以在整個 .NET 框架中廣泛地用於事件、回調、異步調用、多線程等。然而,僅僅爲了使用一個委託,有時您不得不創建一個類或方法。在這種情況下,不需要多個目標,並且調用的代碼通常相對較短而且簡單。在 C# 2.0 中,匿名方法是一個新功能,它允許定義一個由委託調用的匿名(也就是沒有名稱的)方法。

例如,下面是一個常規 SomeMethod 方法的定義和委託調用:

class SomeClass
{
   delegate void SomeDelegate();
   public void InvokeMethod()
   {
      SomeDelegate del = new SomeDelegate(SomeMethod);      
      del(); 
   }
   void SomeMethod()   
   {      
      MessageBox.Show("Hello");
   }
}

可以用一個匿名方法來定義和實現這個方法:

class SomeClass
{
   delegate void SomeDelegate();
   public void InvokeMethod()
   {
      SomeDelegate del = delegate() 
                         {
                             MessageBox.Show("Hello");
                         };
      del();   
   }
}

匿名方法被定義爲內嵌 (in-line) 方法,而不是作爲任何類的成員方法。此外,無法將方法屬性應用到一個匿名方法,並且匿名方法也不能定義一般類型或添加一般約束。

您應該注意關於匿名方法的兩件值得關注的事情:委託保留關鍵字的重載使用和委託指派。稍後,您將看到編譯器如何實現一個匿名方法,而通過查看代碼,您就會相當清楚地瞭解編譯器必須推理所使用的委託的類型,實例化推理類型的新委託對象,將新的委託包裝到匿名方法中,並將其指派給匿名方法定義中使用的委託(前面的示例中的 del)。

匿名方法可以用在任何需要使用委託類型的地方。您可以將匿名方法傳遞給任何方法,只要該方法接受適當的委託類型作爲參數即可:

class SomeClass
{
   delegate void SomeDelegate();
   public void SomeMethod()   
   {      
      InvokeDelegate(delegate(){MessageBox.Show("Hello");});   
   }   
   void InvokeDelegate(SomeDelegate del)   
   {      
      del();   
   }
}

如果需要將一個匿名方法傳遞給一個接受抽象 Delegate 參數的方法,例如:

void InvokeDelegate(Delegate del);

則首先需要將匿名方法強制轉換爲特定的委託類型。

下面是一個將匿名方法作爲參數傳遞的具體而實用的示例,它在沒有顯式定義 ThreadStart 委託或線程方法的情況下啓動一個新的線程:

public class MyClass
{   
   public void LauchThread()
   {
      Thread workerThread = new Thread(delegate()
                            {
                                MessageBox.Show("Hello");
                            });      
      workerThread.Start();
   }
}

在前面的示例中,匿名方法被當作線程方法來使用,這會導致消息框從新線程中顯示出來。

返回頁首

將參數傳遞到匿名方法

當定義帶有參數的匿名方法時,應該在 delegate 關鍵字後面定義參數類型和名稱,就好像它是一個常規方法一樣。方法簽名必須與它指派的委託的定義相匹配。當調用委託時,可以傳遞參數的值,與正常的委託調用完全一樣:

class SomeClass
{
   delegate void SomeDelegate(string str);
   public void InvokeMethod()
   {
      SomeDelegate del = delegate(string str)
                         {
                             MessageBox.Show(str);
                         };
      del("Hello");   
   }
}

如果匿名方法沒有參數,則可以在 delegate 關鍵字後面使用一對空括號:

class SomeClass
{
   delegate void SomeDelegate();
   public void InvokeMethod()
   {
      SomeDelegate del = delegate()
                         {
                             MessageBox.Show("Hello");
                         };
      del();   
   }
}

然而,如果您將 delegate 關鍵字與後面的空括號一起忽略,則您將定義一種特殊的匿名方法,它可以指派給具有任何簽名的任何委託:

class SomeClass
{
   delegate void SomeDelegate(string str);
   public void InvokeMethod()
   {
      SomeDelegate del = delegate                         
                         {
                             MessageBox.Show("Hello");
                         };
      del("Parameter is ignored");   
   }
}

明顯地,如果匿名方法並不依賴於任何參數,而且您想要使用這種與委託簽名無關的方法代碼,則您只能使用這樣的語法。注意,當調用委託時,您仍然需要提供參數,因爲編譯器爲從委託簽名中推理的匿名方法生成無名參數,就好像您曾經編寫了下面的代碼(在 C# 僞碼中)一樣:

SomeDelegate del = delegate(string) 
                   {
                       MessageBox.Show("Hello");
                   };

此外,不帶參數列表的匿名方法不能與指出參數的委託一起使用。

匿名方法可以使用任何類成員變量,並且它還可以使用定義在其包含方法範圍之內的任何局部變量,就好像它是自己的局部變量一樣。圖 7 對此進行了展示。一旦知道如何爲一個匿名方法傳遞參數,也就可以很容易地定義匿名事件處理,如圖 8 所示。

因爲 += 運算符僅僅將一個委託的內部調用列表與另一個委託的內部調用列表連接起來,所以可以使用 += 來添加一個匿名方法。注意,在匿名事件處理的情況下,不能使用 -= 運算符來刪除事件處理方法,除非將匿名方法作爲處理程序加入,要這樣做,可以首先將匿名方法存儲爲一個委託,然後通過事件註冊該委託。在這種情況下,可以將 -= 運算符與相同的委託一起使用,來取消將匿名方法作爲處理程序進行註冊。

返回頁首

匿名方法實現

編譯器爲匿名方法生成的代碼很大程度上依賴於匿名方法使用的參數或變量的類型。例如,匿名方法使用其包含方法的局部變量(也叫做外部變量)還是使用類成員變量和方法參數?無論是哪一種情況,編譯器都會生成不同的 MSIL。如果匿名方法不使用外部變量(也就是說,它只使用自己的參數或者類成員),則編譯器會將一個私有方法添加到該類中,以便賦予方法一個唯一的名稱。該方法的名稱具有以下格式:

<return type> __AnonymousMethod$<random unique number>(<params>)

和其他編譯器生成的成員一樣,這都是會改變的,並且最有可能在最終版本發佈之前改變。方法簽名將成爲它指派的委託的簽名。

編譯器只是簡單地將匿名方法定義和賦值轉換成推理委託類型的標準實例,以包裝機器生成的私有方法:

SomeDelegate del = new SomeDelegate(__AnonymousMethod$00000000);

非常有趣的是,機器產生的私有方法並不顯示在 IntelliSense 中,也不能顯式地調用它,因爲其名稱中的美元符號對於 C# 方法來說是一個非法標記(但它是一個有效的 MSIL 標記)。

當匿名方法使用外部變量時,情況會更加困難。如果這樣,編譯器將用下面的格式添加具有唯一名稱的私有嵌套類:

__LocalsDisplayClass$<random unique number>

嵌套類有一個名爲 <this> 的指向包含類的引用,它是一個有效的 MSIL 成員變量名。嵌套類包含與匿名方法使用的每個外部變量對應的公共成員變量。編譯器向嵌套類定義中添加一個具有唯一名稱的公共方法,格式如下:

<return type> __AnonymousMethod$<random unique number>(<params>)

方法簽名將成爲被指派的委託的簽名。編譯器用代碼替代匿名方法定義,此代碼創建一個嵌套類的實例,並進行必要的從外部變量到該實例的成員變量的賦值。最後,編譯器創建一個新的委託對象,以便包裝嵌套類實例的公共方法,然後調用該委託來調用此方法。圖 9 用 C# 僞代碼展示了編譯器爲圖 7 中定義的匿名方法生成的代碼。

返回頁首

一般匿名方法

匿名方法可以使用一般參數類型,就像其他方法一樣。它可以使用在類範圍內定義的一般類型,例如:

class SomeClass<T>
{   
   delegate void SomeDelegate(T t);   
   public void InvokeMethod(T t)   
   {      
      SomeDelegate del = delegate(T item){...}    
      del(t);   
   }
}

因爲委託可以定義一般參數,所以匿名方法可以使用在委託層定義的一般類型。可以指定用於方法簽名的類型,在這種情況下,方法簽名必須與其所指派的委託的特定類型相匹配:

class SomeClass
{   
   delegate void SomeDelegate<T>(T t);   
   public void InvokeMethod()   
   {      
      SomeDelegate<int> del = delegate(int number) 
                              { 
                                 MessageBox.Show(number.ToString()); 
                              };      
      del(3);   
   }
}
返回頁首

匿名方法示例

雖然乍一看匿名方法的使用可能像一種另類的編程技術,但是我發現它是相當有用的,因爲在只要一個委託就足夠的情況下,使用它就可以不必再創建一個簡單方法。圖 10 展示了一個有用的匿名方法的實際例子 — SafeLabel Windows 窗體控件。

Windows 窗體依賴於基本的 Win32 消息。因此,它繼承了典型的 Windows 編程要求:只有創建窗口的線程可以處理它的消息。在 .NET 框架 2.0 中,調用錯誤的線程總會觸發一個 Windows 窗體方面的異常。因此,當在另一個線程中調用窗體或控件時,必須將該調用封送到正確的所屬線程中。Windows 窗體有內置的支持,可以用來擺脫這個困境,方法是用 Control 基類實現 ISynchronizeInvoke 接口,其定義如下:

public interface ISynchronizeInvoke 
{
   bool InvokeRequired {get;}
   IAsyncResult BeginInvoke(Delegate method,object[] args);
   object EndInvoke(IAsyncResult result);
   object Invoke(Delegate method,object[] args);
}

Invoke 方法接受針對所屬線程中的方法的委託,並且將調用從正在調用的線程封送到該線程。因爲您可能並不總是知道自己是否真的在正確的線程中執行,所以通過使用 InvokeRequired 屬性,您可以進行查詢,從而弄清楚是否需要調用 Invoke 方法。問題是,使用 ISynchronizeInvoke 將會大大增加編程模型的複雜性,因此較好的方法常常是將帶有 ISynchronizeInvoke 接口的交互封裝在控件或窗體中,它們會自動地按需使用 ISynchronizeInvoke。

例如,爲了替代公開 Text 屬性的 Label 控件,您可以定義從 Label 派生的 SafeLabel 控件,如圖 10 所示。SafeLabel 重寫了其基類的 Text 屬性。在其 get 和 set 中,它檢查 Invoke 是否是必需的。如果是這樣,則它需要使用一個委託來訪問此屬性。該實現僅僅調用了基類屬性的實現,不過是在正確的線程上。因爲 SafeLabel 只定義這些方法,所以它們可以通過委託進行調用,它們是匿名方法很好的候選者。SafeLabel 傳遞這樣的委託,以便將匿名方法作爲其 Text 屬性的安全實現包裝到 Invoke 方法中。

返回頁首

委託推理

C# 編譯器從匿名方法指派推理哪個委託類型將要實例化的能力是一個非常重要的功能。實際上,它還提供了另一個叫做委託推理的 C# 2.0 功能。委託推理允許直接給委託變量指派方法名,而不需要先使用委託對象包裝它。例如下面的 C# 1.1 代碼:

class SomeClass
{
   delegate void SomeDelegate();
   public void InvokeMethod()
   {
      SomeDelegate del = new SomeDelegate(SomeMethod);
      del();
   }
   void SomeMethod()
   {...}
}

現在,您可以編寫下面的代碼來代替前面的代碼片斷:

class SomeClass
{
   delegate void SomeDelegate();
   public void InvokeMethod()
   {
      SomeDelegate del = SomeMethod;
      del();
   }
   void SomeMethod()
   {...}
}

當將一個方法名指派給委託時,編譯器首先推理該委託的類型。然後,編譯器根據此名稱檢驗是否存在一個方法,並且它的簽名是否與推理的委託類型相匹配。最後,編譯器創建一個推理委託類型的新對象,以便包裝此方法,並將其指派給該委託。如果該類型是一個具體的委託類型(即除了抽象類型 Delegate 之外的其他類型),則編譯器只能推理委託類型。委託推理的確是一個非常有用的功能,它可以使代碼變得簡練而優雅。

我相信,作爲 C# 2.0 中的慣例,您會使用委託推理,而不是以前的委託實例化方法。例如,下面的代碼說明了如何在不顯式地創建一個 ThreadStart 委託的情況下啓動一個新的線程:

public class MyClass
{   
   void ThreadMethod()   
   {...}
   public void LauchThread()
   {
      Thread workerThread = new Thread(ThreadMethod);      
      workerThread.Start();
   }
}

當啓動一個異步調用並提供一個完整的回調方法時,可以使用一對委託推理,如圖 11 所示。首先,指定異步調用的方法名來異步調用一個匹配的委託。然後調用 BeginInvoke,提供完整的回調方法名而不是 AsyncCallback 類型的委託。

返回頁首

屬性和索引可見性

C# 2.0 允許爲屬性或索引器的 get 和 set 訪問器指定不同的可見性。例如,在通常情況下,可能想將 get 訪問器公開爲 public,而把 set 訪問器公開爲 protected。爲此,可以爲 set 關鍵字添加 protected 可見性限定符。類似地,可以將索引器的 set 方法定義爲 protected(請參見圖 12)。

當使用屬性可見性時有幾項規定。首先,應用在 set 或 get 上的可見性限定詞只能是此屬性本身可見性的嚴格子集。換句話說,如果此屬性是 public,那麼您就可以指定 internal、protected、protected internal、private。如果此屬性可見性是 protected,就不能將 get 或 set 公開爲 public。此外,只能分別爲 get 或 set 指定可見性,而不能同時爲它們指定可見性。

返回頁首

靜態類

有些類只有靜態方法或靜態成員(靜態類),這是非常常見的。在這種情況下,實例化這些類的對象沒有意義。例如,Monitor 類或類工廠(例如 .NET 框架 1.1 中的 Activator 類)都是靜態類。在 C# 1.1 中,如果想要阻止開發人員實例化類的對象,您可以只提供一個私有的默認構造函數。如果沒有任何公共的構造函數,就不可以實例化類的對象:

public class MyClassFactory 
{
   private MyClassFactory()   
   {}
   static public object CreateObject()
   {...}
}

然而,因爲 C# 編譯器仍然允許您添加實例成員(儘管可能從來都不使用它們),所以是否在類中只定義靜態成員完全由您決定。C# 2.0 通過允許將類限定爲 static 來支持靜態類:

public static class MyClassFactory
{  
   static public T CreateObject<T>()
   {...}
}

C# 2.0 編譯器不允許您將一個非靜態成員添加到一個靜態類中,也不允許您創建此靜態類的實例,就好像它是一個抽象類一樣。此外,您不能從一個靜態類派生子類。這就如同編譯器在靜態類定義中加入了 abstract 和 sealed 一樣。注意,可以定義靜態類而不能定義靜態結構,並且可以添加靜態構造函數。

返回頁首

全局命名空間限定符

很可能有這樣一個嵌套的命名空間,它的名稱與一些其他的全局命名空間相匹配。在這種情況下,C# 1.1 編譯器在解析命名空間引用時會出現問題。請考慮下例:

namespace MyApp
{
   namespace System
   {
      class MyClass
      {
         public void MyMethod()
         {
            System.Diagnostics.Trace.WriteLine("It Works!");
         }
      }
   }
}

在 C# 1.1 中,調用 Trace 類會產生編譯錯誤(沒有全局命名空間限定符 ::)。出現這種錯誤的原因在於,當編譯器嘗試解析對 System 命名空間的引用時,它使用直接包含範圍,此範圍包含 System 命名空間但不包含 Diagnostics 命名空間。C# 2.0 允許您使用全局命名空間限定符 :: 來表示編譯器應該在全局範圍內進行搜索。可以將 :: 限定符應用於命名空間和類型,如圖 13 所示。

返回頁首

內聯警告

C# 1.1 允許使用項目設置或者通過向編譯器發佈命令行參數來禁止特殊的編譯器警告。其中的問題在於,這是一個全局取消,因此這樣做會取消一些您仍然需要的警告。C# 2.0 允許使用 #pragma 警告指令顯式地取消和恢復編譯器警告:

// Disable 'field never used' warning
#pragma warning disable 169
public class MyClass 
{
   int m_Number;
}
#pragma warning restore 169

在生產代碼中通常並不鼓勵禁止警告。禁止警告只是爲了進行某些分析,比如,當您嘗試隔離一個問題時,或者當您設計代碼並且想要得到代碼合適的初始結構而不必先行對其加以完善時。而在所有其他的情況下,都要避免取消編譯器警告。注意,您不能通過編程的方式來重寫項目設置,這意味着您不能使用 pragma 警告指令來恢復全局取消的警告。

返回頁首

小結

本文所提到的 C# 2.0 中的一些新功能是專門的解決方案,旨在處理特定的問題,同時可以簡化整體編程模型。如果您關注工作效率和質量,您就需要讓編譯器生成儘可能多的實現,減少重複性的編程任務,使最後得到的代碼簡潔易讀。新的功能帶給您的正是這些,並且我相信,它們象徵着 C# 時代的到來,它會使自己成爲服務於 .NET 專業開發人員的優秀工具。

Juval Lowy 是一名軟件架構師,他提供 .NET 設計和移植方面的諮詢和培訓。他還是硅谷的 Microsoft 地區總裁 (Microsoft Regional Director)。他最新出版的一本書是 Programming .NET Components (O'Reilly, 2003)。可以在 http://www.idesign.net 上與 Juval 聯繫。

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