程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C# 2.0:使用匿名方法、迭代程序和局部類來創建優雅的代碼

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

編輯:關於C#

熱衷於 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 成員變量維持當前的迭代狀 態,此成員變量用作數組的索引。

Figure 2Implementing IEnumerable<string>

public class CityCollection : IEnumerable<string>
{
  string[] m_Cities = {"New York","Paris","London"};
  public IEnumerator<string> GetEnumerator()
  {
   return new MyEnumerator(this);
  }
  //Nested class definition
  class MyEnumerator : IEnumerator<string>
  {
    CityCollection m_Collection;
   int m_Current;
    public MyEnumerator(CityCollection collection)
   {
      m_Collection = collection;
     m_Current = -1;
    }
   public bool MoveNext()
   {
     m_Current++;
     if(m_Current < m_Collection.m_Cities.Length)
      return true;
      else
      return false;
   }
   public string Current
   {
     get
     {
       if(m_Current == -1)
        throw new InvalidOperationException();
      return m_Collection.m_Cities[m_Current];
     }
   }
    public void Dispose(){}
  }
}

圖 2

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

Figure 3Simple Iterator

CityCollection cities = new CityCollection();
//For this foreach loop:
foreach(string city in cities)
{
  Trace.WriteLine(city);
}
//The compiler generates this equivalent code:
IEnumerable<string> enumerable = cities;
IEnumerator<string> enumerator = enumerable.GetEnumerator();
using(enumerator)
{
  while(enumerator.MoveNext())
  {
   Trace.WriteLine (enumerator.Current);
  }
}

圖 3

您可以使 用新的 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> 所 用的特定類型:

Figure 4Providing Iterators on a Generic Linked List

//K is the key, T is the data item
class Node<K,T>
{
  public K Key;
  public T Item;
  public Node<K,T> NextNode;
}
public class LinkedList<K,T> : IEnumerable<T>
{
  Node<K,T> m_Head;
  public IEnumerator<T> GetEnumerator()
  {
   Node<K,T> current = m_Head;
   while(current != null)
   {
     yield return current.Item;
     current = current.NextNode;
   }
  }
  /* More methods and members */
}

圖 4

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 的實現的類。

Figure 5The Compiler- generated Iterator

public class MyCollection : IEnumerable<string>
{
  public virtual IEnumerator<string> GetEnumerator()
  {
    GetEnumerator$0003__IEnumeratorImpl impl;
   impl = new GetEnumerator$0003__IEnumeratorImpl;
   impl.<this> = this;
   return impl;
  }
  private class GetEnumerator$0003__IEnumeratorImpl :
    IEnumerator<string>
  {
   public MyCollection <this>; // Back reference to the collection
   string $_current;
   // state machine members go here
    string IEnumerator<string>.Current
   {
     get
     {
      return $_current;
     }
   }
   bool IEnumerator<string>.MoveNext()
   {
     //State machine management
   }
    IDisposable.Dispose()
   {
     //State machine cleanup if required 
   }
  }
}

圖 5

遞歸迭代

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

Figure 6Implementing a Recursive Iterator

class Node<T>
{
  public Node<T> LeftNode;
  public Node<T> RightNode;
  public T Item;
}
public class BinaryTree<T>
{
  Node<T> m_Root;
  public void Add(params T[] items)
  {
   foreach(T item in items)
     Add (item);
  }
  public void Add(T item)
  {...}
  public IEnumerable<T> InOrder
  {
   get
    {
     return ScanInOrder(m_Root);
   }
  }
  IEnumerable<T> ScanInOrder(Node<T> root)
  {
   if(root.LeftNode != null)
   {
     foreach(T item in ScanInOrder(root.LeftNode))
     {
       yield return item;
     }
   }
    yield return root.Item;
   if(root.RightNode != null)
    {
     foreach(T item in ScanInOrder(root.RightNode))
     {
      yield return item;
     }
    }
  }
}

圖 6

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 所示。

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

Figure 7Local Variable in Anonymous Method Code

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

圖 7

Figure 8Anonymous Method as Event Handler

public class MyForm : Form
{
  Button m_MyButton;
  public MyForm()
  {
   InitializeComponent();
    m_MyButton.Click += delegate(object sender,EventArgs args)
              {
               MessageBox.Show("Clicked");
              };
  }
  void InitializeComponent()
  {...}
}

匿名方法實現

編譯器為匿名方法生成的代碼很大程度上依賴於匿名方 法使用的參數或變量的類型。例如,匿名方法使用其包含方法的局部變量(也叫 做外部變量)還是使用類成員變量和方法參數?無論是哪一種情況,編譯器都會 生成不同的 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 中 定義的匿名方法生成的代碼。

Figure 9Anonymous Method Code with Outer Variables

class SomeClass
{
  string m_Space = " ";
  delegate void SomeDelegate(string str);
  private sealed class __LocalsDisplayClass$00000001
  {
   public SomeClass <this>; //Back pointer, name is valid in MSIL
   public string msg;    //Outer variable
   public void __AnonymousMethod$00000000(string name)
    {
     MessageBox.Show(msg + <this>.m_Space + name);
   }
  }
  public void InvokeMethod()
  {
   string msg = "Hello";
    __LocalsDisplayClass$00000001 locals;
   locals = new __LocalsDisplayClass$00000001();
   locals.<this> = this;
   locals.msg = msg;
   SomeDelegate del = new
     SomeDelegate(locals.__AnonymousMethod$00000000);
    del("Juval");
}

圖 9

一般匿名方法

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

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 窗體控件。

Figure 10The SafeLabel Control

public class SafeLabel : Label
{
  delegate void SetString(string text);
  delegate string GetString();
  override public string Text
  {
   set
   {
     if (InvokeRequired)
     {
      SetString setTextDel = delegate(string text)
                      {base.Text = text;};
      Invoke(setTextDel,new object[] {value});
     }
     else
      base.Text = value;
   }
   get
   {
     if (InvokeRequired)
     {
      GetString getTextDel = delegate(){return base.Text;};
      return (string) Invoke(getTextDel,null);
     }
     else
       return base.Text;
   }
  }
}

圖 10

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 指定可見性,而不能同時為它們指 定可見性。

Figure 12Public Get and Protected Set

public class MyClass
{
  public string this [int index]
  {
   get
   {
     return m_Names[index];
   }
   protected set
   {
     m_Names[index] = value;
   }
  }
  string[] m_Names;
  //Rest of the class
}

圖 12

靜態類

有些類只有靜態方法或靜態成員(靜態類),這是非常常見 的。在這種情況下,實例化這些類的對象沒有意義。例如,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 所示。

Figure 13Using the Global Namespace Qualifier

namespace MyApp
{
  class MyClass
  {
   public void MyMethod()
   {
     ::MyClass obj = new ::MyClass();
     obj.MyMethod(); // Traces "Hello" instead of recursion
   }
  }
}
public class MyClass
{
  public void MyMethod ()
  {
   Trace.WriteLine("Hello");
  }
}

圖 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 專業開發人員的優秀工具。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved