匿名方法基礎
匿名方法是C#2.0的一個新的語言特性。本文的主要內容是提供給讀者關於匿名方法的內部實現和工作方式的一個更好的理解。本文無意於成為匿名方法的完全語言特性參考。
匿名方法允許我們定義委托對象可以接受的代碼塊。這個功能省去我們創建委托時想要傳遞給一個委托的小型代碼塊的一個額外的步驟。它也消除了類代碼中小型方法的混亂。讓我們看看:比方說,我們有一個字符串集合命名為MyCollection。這個類有一個方法:獲得集合中滿足用戶提供的過濾准則的所有項,調用者決定在集合中的一個特殊項是否符合條件而被檢索到,作為從此方法返回數組的一部分。
public class MyCollection
{
public delegate bool SelectItem(string sItem);
public string[] GetFilteredItemArray(SelectItem itemFilter)
{
List<string> sList = new List<string>();
foreach(string sItem in m_sList)
{
if (itemFilter(sItem) == true) sList.Add(sItem);
}
return sList.ToArray();
}
public List<string> ItemList
{
get
{
return m_sList;
}
}
private List<string> m_sList = new List<string>();
}
我們可以用上面定義的類寫如下所示的代碼:
public class Program
{
public static void Main(string[] args)
{
MyCollection objMyCol = new MyCollection();
objMyCol.ItemList.Add("Aditya");
objMyCol.ItemList.Add("Tanu");
objMyCol.ItemList.Add("Manoj");
objMyCol.ItemList.Add("Ahan");
objMyCol.ItemList.Add("Hasi");
//獲得集合中以字母’A‘開頭的字符項數組
string[] AStrings = objMyCol.GetFilteredItemArray(FilterStringWithA);
Console.WriteLine("----- Strings starting with letter ''A'' -----");
foreach(string s in AStrings)
{
Console.WriteLine(s);
}
//獲得集合中以字母’T‘開頭的字符項數組
string[] TStrings = objMyCol.GetFilteredItemArray(FilterStringWithT);
Console.WriteLine("----- Strings starting with letter ''T'' -----");
foreach(string s in TStrings)
{
Console.WriteLine(s);
}
}
public static bool FilterStringWithA(string sItem)
{
if (sItem[0] == ''A'')
return true;
else
return false;
}
public static bool FilterStringWithT(string sItem)
{
if (sItem[0] == ''T'')
return true;
else
return false;
}
}
可以看出對於每個我們想要提供的簡單過濾准則,我們應該定義一個方法(靜態或實例的)。這很快就搞亂了類的代碼。而用匿名方法,代碼變得相當自然。下面是這個Program類用匿名方法重寫後的:
public class Program
{
public delegate void MyDelegate();
public static void Main(string[] args)
{
MyCollection objMyCol = new MyCollection();
objMyCol.ItemList.Add("Aditya");
objMyCol.ItemList.Add("Tanu");
objMyCol.ItemList.Add("Manoj");
objMyCol.ItemList.Add("Ahan");
objMyCol.ItemList.Add("Hasi");
//獲得集合中以字母’A‘開頭的字符項數組
string[] AStrings = objMyCol.GetFilteredItemArray(delegate(string sItem)
{
if (sItem[0] == ''A'')
return true;
else
return false;
});
Console.WriteLine("----- Strings starting with letter ''A'' -----");
foreach (string s in AStrings)
{
Console.WriteLine(s);
} //獲得集合中以字母’ T ‘開頭的字符項數組
string[] TStrings = objMyCol.GetFilteredItemArray(delegate(string sItem)
{
if (sItem[0] == ''T'')
return true;
else
return false;
});
Console.WriteLine("----- Strings starting with letter ''T'' -----");
foreach (string s in TStrings)
{
Console.WriteLine(s);
}
}
}
正如上面示例中的所示,我們已能用內聯代碼塊定義的過濾准則替代定義一個新的方法來代表每個過濾准則。老實說,用這種內聯代碼可能看起來自然並且避免了定義新方法,但是如果這個技術被用於更大的內聯代碼塊,這時代碼很快變得難於管理並可能導致代碼重復。因此,使用方法與內聯匿名方法都是委托/事件處理器的可選方案。
好了,這些就是匿名方法的基礎。本文的余下部分將討論在不同的場景下匿名方法內部如何工作的。理解匿名方法如何被實現和內部如何工作對於正確地使用它們是重要的。否則,你使用匿名方法的代碼的結果看起來將不可預知。
匿名方法的靜態數據成員的用法
匿名方法總是以一個delegate關鍵字開始,後面跟著用在方法和方法體(the method body)本身中的參數。正如從上面示例中所見,用戶不需要確定匿名方法的返回類型。它(譯注:指返回類型)由方法體中的return語句推斷而來。.NET CLR不能執行像匿名方法一樣的自由流(free flowing)代碼塊。CLR要求:它執行的每個方法是一個類型的一部分,並且應該是一個靜態(static)方法或實例(instance)方法(譯注:若一個方法聲明中含有 static 修飾符,則稱該方法為靜態方法。若其中沒有 static 修飾符時,則稱該方法為實例方法。靜態方法不對特定實例進行操作,在靜態方法中引用 this 是編譯時錯誤。實例方法對類的某個給定的實例進行操作,而且可以用 this來訪問該實例)。因此當你在一個類的代碼中寫匿名方法並編譯這個代碼時,C#編譯器默默地在你定義匿名方法的相同的類中創建了一個靜態或實例方法。所以匿名方法只是一個在類中定義你自己方法以傳遞到委托(委托處理器/事件處理器)的方便的語法。
當你編譯上面的示例時,C#編譯器在類''Program''內部即我們定義匿名方法的地方創建了兩個private static方法。它此時用這些static方法的地址取代了匿名方法。編譯器決定如何創建靜態方法或實例方法取決於匿名方法被定義的類中的靜態或實例數據成員的用法。在我們的示例中,我們沒有用到任何類''Program''的數據成員,因為調用一個靜態方法而不是一個實例方法將是高效的,因此C#編譯器創建一個static方法來封裝我們的匿名方法的代碼。下面是這個示例程序集''Program'' 類的ILDASM視圖。高亮部分顯示了由C#編譯器默默添加到''Program''類的新的靜態方法。
如果我們已經使用了用匿名方法的''Program'' 類的任何靜態數據,C#編譯器將仍然在''Program'' 類裡創建一個靜態方法來包裝匿名方法。
匿名方法的實例數據成員用法
讓我們在我們的示例中的''Program''類中定義一個新的實例方法,並使用示例類(譯注:即''Program''類)一個實例數據成員。下面的代碼顯示了修改後的示例:
public class Program
{
public delegate void MyDelegate();
public static void Main(string[] args)
{
//實例數據成員測試
Program p = new Program();
for(int i=1;i<=5;i++)
p.TestInstanceDataMembers();
}
public void TestInstanceDataMembers()
{
MyDelegate d = delegate
{
Console.WriteLine("Count: {0}",++m_iCount);
};
d();
}
public int m_iCount = 0;
}
我們定義了一個新的實例方法:TestInstanceDataMembers,在''Program''類中這個方法定義了一個匿名方法,匿名方法使用了實例數據成員:隸屬''Program''類的m_iCount。當這個示例編譯時,C#編譯器將創建一個private實例方法來包裝這個在TestInstanceDataMembers中定義的匿名方法。C#編譯器必須創建一個實例方法因為該方法需要訪問''Program''類的實例數據成員。下面是這個示例程序集''Program''類的ILDASM視圖。在圖的下部選中部分顯示了由C#編譯器默默添加到''Program''類的新的private實例方法。
匿名方法的局部變量用法
到現在為止,我們對匿名方法如何工作以及內部如何實現有了一點基本的理解。從根本上說,C#創建了private方法來包裝匿名方法。同時這些方法的簽名與它們被分配到的委托相匹配。現在,讓我們看看下面的代碼:
public class Program
{
public delegate void MyDelegate();
public static void Main(string[] args)
{
int iTemp = 100;
MyDelegate dlg = delegate
{
Console.WriteLine(iTemp);
};
dlg();
}
}
對於我們到現在為止對匿名方法已了解的內容來說,這段代碼不應該編譯。因為我們沒有使用如何實例數據成員,C#編譯器應該在''Program''類中創建一個private靜態方法來包裝這個匿名方法。但是新的方法如何訪問局部變量呢?這讓我們相信該代碼將不能被編譯。但是令人驚訝的是,C#編譯器成功編譯了這個代碼而沒有任何錯誤或報警。而且,當你執行這個示例時,在控制台屏幕上輸出打印出iTemp變量的正確的值。現在讓我們進入匿名方法的高級話題。一個匿名方法有封裝在其方法體中使用了的環境變量的值的能力。這個封裝應用於匿名方法被定義的方法中的所有局部變量。當C#編譯器在一個匿名方法的方法體中識別出用到一個局部變量,它就會做如下事情:
1.創建一個新的private類作為匿名方法被定義的類的一個內部類。
2.在新類(譯注:即內部類)中創建一個公共數據成員,使用與用在匿名方法體中的局部變量相同的類型和名稱。
3.在包裝匿名方法的新類中創建一個public實例方法。
4.用新類中的聲明替代局部變量的聲明。創建該新類的一個實例代替局部變量的聲明。
5.用新類實例的數據成員替代在匿名方法體內部和外部使用的局部變量。
6.用在新類中定義的實例方法的地址取代匿名方法的定義。
因此在編譯時,上面的代碼將被C#編譯器翻譯為如下代碼:
public class Program
{
private class InnerClass
{
private void InstanceMethod()
{
Console.WriteLine(iTemp);
}
public int iTemp;
}
public delegate void MyDelegate();
public static void Main(string[] args)
{
InnerClass localObject = new InnerClass();
localObject.iTemp = 100;
MyDelegate dlg = new MyDelegate(localObject.InstanceMethod);
dlg();
}
}
正如上面的偽代碼所示,C#編譯器為''Program''類生成了一個private內部類。在匿名方法中使用的局部變量作為新的已創建的內部類的一個實例數據成員而捕獲。並且匿名方法本身被包裝在內部類的實例方法中。最後,該實例方法在Main方法中作為一個委托處理器而使用。這樣,當委托被調用時,對於在被封裝入匿名方法中的局部變量將會有一個正確的值。下面圖中選定的部分顯示了由C#編譯器默默添加到''Program'' 類的新的private內部類。
被用在匿名方法中的局部變量有著超出用到它們的外部常規方法的生命周期。這個技術,在其它語言中,就是大家都知道的closures。除去匿名方法提供的簡單語法,closures是匿名方法提供給開發者的一個功能強大的技術。該技術允許委托處理器代碼(匿名方法)訪問在常規方法內部被定義的局部變量。這就允許out-of-band數據,除了委托參數之外還有數據將被傳遞到委托,以供在其方法執行時使用。沒有這個技術,每個委托和其相應的處理器方法就不得不聲明表示局部上下文數據的參數,隨著時間的過去這(譯注:指不斷聲明表示局部上下文數據的參數)將變得難於管理。 匿名方法的作用域和局部變量用法
我們討論了在方法的主作用域(the main scope)中的匿名方法的實現。當一個匿名方法在一個嵌套作用域中被定義時,並且匿名方法中用到獨立作用域級的局部變量,C#為每個作用域創建一個private內部類。比如,假設scope 1有局部變量iTemp,而scope 2,是scope 1的嵌套作用域,有一個局部變量jTemp。讓在使用來自scope 1 和 scope 2局部變量iTemp 和 jTemp的 scope 2中,我們定義一個匿名方法。下面的代碼顯示了上面描述的示例:
public class Program
{
public delegate void MyDelegate();
public static void Main(string[] args)
{
MyDelegate dlg = null;
int iTemp = 100;
if (iTemp > 50)
{
int jTemp = 200;
dlg = delegate
{
Console.WriteLine("iTemp: {0}, jTemp: {1}",iTemp,jTemp);
};
}
dlg();
}
}
當上面的代碼被編譯時,C#編譯器在''Program''類中創建兩個內部類。一個內部類包裝局部變量iTemp作為一個public數據成員。第二個內部類包裝在嵌套作用域中的局部變量,jTemp,作為一個public數據成員,同時在相同的嵌套作用域中包裝匿名方法作為public實例方法。C#編譯器為上面的代碼生成下面的偽代碼:
public class Program
{
//包裝來自外部作用域的局部變量''iTemp''的類
private class InnerClassScope1
{
public int iTemp;
}
//包裝來自內部作用域和匿名方法的局部變量的類
private class InnerClassScope2
{
public void InstanceMethod()
{
Console.WriteLine("iTemp: {0}, jTemp: {1}", localObjectScope1.iTemp, jTemp);
}
public InnerClassScope1 localObjectScope1;
public int jTemp;
}
public delegate void MyDelegate();
public static void Main(string[] args)
{
MyDelegate dlg = null;
InnerClassScope1 localObject1 = new InnerClassScope1();
localObject1.iTemp = 100;
if (localObject1.iTemp > 50)
{
InnerClassScope2 localObject2 = new InnerClassScope2();
localObject2.localObjectScope1 = localObject1;
localObject2.jTemp = 200;
dlg = new MyDelegate(localObject2.InstanceMethod);
}
dlg();
}
}
正如上面的代碼所示,包裝匿名方法的內部類將擁有所有代表外部作用域局部變量的對象,這些變量被用在匿名方法中,像public數據成員。下圖顯示了C#默默創建的內部類的ILDASM視圖:
在循環控制結構內使用匿名方法的局部變量的用法
當處理循環控制結構時將局部變量封裝入類的數據成員有著有趣但危險的一面,讓我們看看下面代碼:
public class Program
{
public delegate void MyDelegate();
public static void Main(string[] args)
{
MyDelegate d = null;
for (int i = 1; i <= 5; i++)
{
MyDelegate tempD = delegate
{
Console.WriteLine(i);
};
d += tempD;
}
d();
}
}
上面的代碼運行時將會有什麼輸出呢?我們的意圖是捕獲在我們的匿名方法中的循環計數變量''i''並顯示之。我們預期的輸出應該如下所示:
1
2
3
4
5
但是如果你運行上面的代碼,輸出將是如下所示:
6
6
6
6
6
如果我們仔細回憶我們關於匿名方法的內部工作機制的知識,我提到:在匿名方法中被捕獲的任何局部變量將會被該作用域的一個新的已創建內部類的實例數據成員替代。對於循環控制變量,作用域是包含了for循環的作用域,這就是上面的簡單代碼所示的main方法體。因此當該代碼編譯時,C#編譯器生成創建了內部類的實例的代碼,包裝了匿名方法和循環計數變量,在for循環的外部。並且該內部類的實例的數據成員,代表了循環計數變量,將被用來替代用於for循環而且也在匿名方法中使用的原始循環計數變量。因此來自內部類的相同實例的數據成員被用於for循環並且也用在包裝匿名方法的實例方法中。作為循環完成時的結果,實例數據成員會增加六次。這裡有一個需要注意的重要地方:盡管這個循環在五次迭代後結束,在它跳出循環控制結構時循環計數變量被增加了六次。既然該循環控制變量是一個實例數據成員,第六次增加觸發了已由循環計數變量提供的循環結束條件。既然相同實例的一個方法被用做匿名方法的委托處理器,在委托結束時被調用,所有委托的實例將被指向相同實例,同時將為數據成員顯示相同值,就是6。這就是我在本節開始已提到過的有危險影響的一面。
為了克服這個問題並獲得預期的結果,匿名方法應該在for循環的作用域中捕獲一個局部變量,它將有與循環計數變量的相同的值。這可以通過如下修改示例代碼獲得:
public class Program
{
public delegate void MyDelegate();
public static void Main(string[] args)
{
MyDelegate d = null;
for (int i = 1; i <= 5; i++)
{
int k = i;
MyDelegate tempD = delegate
{
Console.WriteLine(k);
};
d += tempD;
}
d();
}
}
在你運行上面的代碼示例時,將會獲得預期的輸出,也就是:
1
2
3
4
5
原因就是,C#編譯器將為for循環的每次迭代而包裝局部變量''k''的內部類創建 實例。同時包裝了每個循環迭代的實例上的匿名方法的這個方法被用做一個委托處理器。
總結
匿名方法是C#2.0語言增加的一個非常有用和強大的功能。除了介紹的一些對委托聲明和用法上的語法改進,Microsoft已在使匿名方法代碼自然融入所包含的方法體方面獲得很大進展,包括訪問在包含(匿名方法)的方法定義的作用域中的局部變量。最後,我希望本文提供給C#開發人員正確而聰明地利用匿名方法的必備知識。