匿名方法基礎(chǔ)
匿名方法是c#2.0的一個(gè)新的語言特性。本文的主要內(nèi)容是提供給讀者關(guān)于匿名方法的內(nèi)部實(shí)現(xiàn)和工作方式的一個(gè)更好的理解。本文無意于成為匿名方法的完全語言特性參考。
匿名方法允許我們定義委托對象可以接受的代碼塊。這個(gè)功能省去我們創(chuàng)建委托時(shí)想要傳遞給一個(gè)委托的小型代碼塊的一個(gè)額外的步驟。它也消除了類代碼中小型方法的混亂。讓我們看看:比方說,我們有一個(gè)字符串集合命名為mycollection。這個(gè)類有一個(gè)方法:獲得集合中滿足用戶提供的過濾準(zhǔn)則的所有項(xiàng),調(diào)用者決定在集合中的一個(gè)特殊項(xiàng)是否符合條件而被檢索到,作為從此方法返回?cái)?shù)組的一部分。
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‘開頭的字符項(xiàng)數(shù)組
string[] astrings = objmycol.getfiltereditemarray(filterstringwitha);
console.writeline("----- strings starting with letter ''a'' -----");
foreach(string s in astrings)
{
console.writeline(s);
}
//獲得集合中以字母’t‘開頭的字符項(xiàng)數(shù)組
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;
}
}
可以看出對于每個(gè)我們想要提供的簡單過濾準(zhǔn)則,我們應(yīng)該定義一個(gè)方法(靜態(tài)或?qū)嵗模_@很快就搞亂了類的代碼。而用匿名方法,代碼變得相當(dāng)自然。下面是這個(gè)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‘開頭的字符項(xiàng)數(shù)組
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 ‘開頭的字符項(xiàng)數(shù)組
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);
}
}
}
正如上面示例中的所示,我們已能用內(nèi)聯(lián)代碼塊定義的過濾準(zhǔn)則替代定義一個(gè)新的方法來代表每個(gè)過濾準(zhǔn)則。老實(shí)說,用這種內(nèi)聯(lián)代碼可能看起來自然并且避免了定義新方法,但是如果這個(gè)技術(shù)被用于更大的內(nèi)聯(lián)代碼塊,這時(shí)代碼很快變得難于管理并可能導(dǎo)致代碼重復(fù)。因此,使用方法與內(nèi)聯(lián)匿名方法都是委托/事件處理器的可選方案。
好了,這些就是匿名方法的基礎(chǔ)。本文的余下部分將討論在不同的場景下匿名方法內(nèi)部如何工作的。理解匿名方法如何被實(shí)現(xiàn)和內(nèi)部如何工作對于正確地使用它們是重要的。否則,你使用匿名方法的代碼的結(jié)果看起來將不可預(yù)知。
匿名方法的靜態(tài)數(shù)據(jù)成員的用法
匿名方法總是以一個(gè)delegate關(guān)鍵字開始,后面跟著用在方法和方法體(the method body)本身中的參數(shù)。正如從上面示例中所見,用戶不需要確定匿名方法的返回類型。它(譯注:指返回類型)由方法體中的return語句推斷而來。.net clr不能執(zhí)行像匿名方法一樣的自由流(free flowing)代碼塊。clr要求:它執(zhí)行的每個(gè)方法是一個(gè)類型的一部分,并且應(yīng)該是一個(gè)靜態(tài)(static)方法或?qū)嵗╥nstance)方法(譯注:若一個(gè)方法聲明中含有 static 修飾符,則稱該方法為靜態(tài)方法。若其中沒有 static 修飾符時(shí),則稱該方法為實(shí)例方法。靜態(tài)方法不對特定實(shí)例進(jìn)行操作,在靜態(tài)方法中引用 this 是編譯時(shí)錯(cuò)誤。實(shí)例方法對類的某個(gè)給定的實(shí)例進(jìn)行操作,而且可以用 this來訪問該實(shí)例)。因此當(dāng)你在一個(gè)類的代碼中寫匿名方法并編譯這個(gè)代碼時(shí),c#編譯器默默地在你定義匿名方法的相同的類中創(chuàng)建了一個(gè)靜態(tài)或?qū)嵗椒āK阅涿椒ㄖ皇且粋€(gè)在類中定義你自己方法以傳遞到委托(委托處理器/事件處理器)的方便的語法。
當(dāng)你編譯上面的示例時(shí),c#編譯器在類''program''內(nèi)部即我們定義匿名方法的地方創(chuàng)建了兩個(gè)private static方法。它此時(shí)用這些static方法的地址取代了匿名方法。編譯器決定如何創(chuàng)建靜態(tài)方法或?qū)嵗椒ㄈQ于匿名方法被定義的類中的靜態(tài)或?qū)嵗龜?shù)據(jù)成員的用法。在我們的示例中,我們沒有用到任何類''program''的數(shù)據(jù)成員,因?yàn)檎{(diào)用一個(gè)靜態(tài)方法而不是一個(gè)實(shí)例方法將是高效的,因此c#編譯器創(chuàng)建一個(gè)static方法來封裝我們的匿名方法的代碼。下面是這個(gè)示例程序集''program'' 類的ildasm視圖。高亮部分顯示了由c#編譯器默默添加到''program''類的新的靜態(tài)方法。

如果我們已經(jīng)使用了用匿名方法的''program'' 類的任何靜態(tài)數(shù)據(jù),c#編譯器將仍然在''program'' 類里創(chuàng)建一個(gè)靜態(tài)方法來包裝匿名方法。
匿名方法的實(shí)例數(shù)據(jù)成員用法
讓我們在我們的示例中的''program''類中定義一個(gè)新的實(shí)例方法,并使用示例類(譯注:即''program''類)一個(gè)實(shí)例數(shù)據(jù)成員。下面的代碼顯示了修改后的示例:
public class program
{
public delegate void mydelegate();
public static void main(string[] args)
{
//實(shí)例數(shù)據(jù)成員測試
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;
}
我們定義了一個(gè)新的實(shí)例方法:testinstancedatamembers,在''program''類中這個(gè)方法定義了一個(gè)匿名方法,匿名方法使用了實(shí)例數(shù)據(jù)成員:隸屬''program''類的m_icount。當(dāng)這個(gè)示例編譯時(shí),c#編譯器將創(chuàng)建一個(gè)private實(shí)例方法來包裝這個(gè)在testinstancedatamembers中定義的匿名方法。c#編譯器必須創(chuàng)建一個(gè)實(shí)例方法因?yàn)樵摲椒ㄐ枰L問''program''類的實(shí)例數(shù)據(jù)成員。下面是這個(gè)示例程序集''program''類的ildasm視圖。在圖的下部選中部分顯示了由c#編譯器默默添加到''program''類的新的private實(shí)例方法。

匿名方法的局部變量用法
到現(xiàn)在為止,我們對匿名方法如何工作以及內(nèi)部如何實(shí)現(xiàn)有了一點(diǎn)基本的理解。從根本上說,c#創(chuàng)建了private方法來包裝匿名方法。同時(shí)這些方法的簽名與它們被分配到的委托相匹配。現(xiàn)在,讓我們看看下面的代碼:
public class program
{
public delegate void mydelegate();
public static void main(string[] args)
{
int itemp = 100;
mydelegate dlg = delegate
{
console.writeline(itemp);
};
dlg();
}
}
對于我們到現(xiàn)在為止對匿名方法已了解的內(nèi)容來說,這段代碼不應(yīng)該編譯。因?yàn)槲覀儧]有使用如何實(shí)例數(shù)據(jù)成員,c#編譯器應(yīng)該在''program''類中創(chuàng)建一個(gè)private靜態(tài)方法來包裝這個(gè)匿名方法。但是新的方法如何訪問局部變量呢?這讓我們相信該代碼將不能被編譯。但是令人驚訝的是,c#編譯器成功編譯了這個(gè)代碼而沒有任何錯(cuò)誤或報(bào)警。而且,當(dāng)你執(zhí)行這個(gè)示例時(shí),在控制臺屏幕上輸出打印出itemp變量的正確的值。現(xiàn)在讓我們進(jìn)入匿名方法的高級話題。一個(gè)匿名方法有封裝在其方法體中使用了的環(huán)境變量的值的能力。這個(gè)封裝應(yīng)用于匿名方法被定義的方法中的所有局部變量。當(dāng)c#編譯器在一個(gè)匿名方法的方法體中識別出用到一個(gè)局部變量,它就會做如下事情:
1. 創(chuàng)建一個(gè)新的private類作為匿名方法被定義的類的一個(gè)內(nèi)部類。
2. 在新類(譯注:即內(nèi)部類)中創(chuàng)建一個(gè)公共數(shù)據(jù)成員,使用與用在匿名方法體中的局部變量相同的類型和名稱。
3. 在包裝匿名方法的新類中創(chuàng)建一個(gè)public實(shí)例方法。
4. 用新類中的聲明替代局部變量的聲明。創(chuàng)建該新類的一個(gè)實(shí)例代替局部變量的聲明。
5. 用新類實(shí)例的數(shù)據(jù)成員替代在匿名方法體內(nèi)部和外部使用的局部變量。
6. 用在新類中定義的實(shí)例方法的地址取代匿名方法的定義。
因此在編譯時(shí),上面的代碼將被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''類生成了一個(gè)private內(nèi)部類。在匿名方法中使用的局部變量作為新的已創(chuàng)建的內(nèi)部類的一個(gè)實(shí)例數(shù)據(jù)成員而捕獲。并且匿名方法本身被包裝在內(nèi)部類的實(shí)例方法中。最后,該實(shí)例方法在main方法中作為一個(gè)委托處理器而使用。這樣,當(dāng)委托被調(diào)用時(shí),對于在被封裝入匿名方法中的局部變量將會有一個(gè)正確的值。下面圖中選定的部分顯示了由c#編譯器默默添加到''program'' 類的新的private內(nèi)部類。

被用在匿名方法中的局部變量有著超出用到它們的外部常規(guī)方法的生命周期。這個(gè)技術(shù),在其它語言中,就是大家都知道的closures。除去匿名方法提供的簡單語法,closures是匿名方法提供給開發(fā)者的一個(gè)功能強(qiáng)大的技術(shù)。該技術(shù)允許委托處理器代碼(匿名方法)訪問在常規(guī)方法內(nèi)部被定義的局部變量。這就允許out-of-band數(shù)據(jù),除了委托參數(shù)之外還有數(shù)據(jù)將被傳遞到委托,以供在其方法執(zhí)行時(shí)使用。沒有這個(gè)技術(shù),每個(gè)委托和其相應(yīng)的處理器方法就不得不聲明表示局部上下文數(shù)據(jù)的參數(shù),隨著時(shí)間的過去這(譯注:指不斷聲明表示局部上下文數(shù)據(jù)的參數(shù))將變得難于管理。 匿名方法的作用域和局部變量用法
我們討論了在方法的主作用域(the main scope)中的匿名方法的實(shí)現(xiàn)。當(dāng)一個(gè)匿名方法在一個(gè)嵌套作用域中被定義時(shí),并且匿名方法中用到獨(dú)立作用域級的局部變量,c#為每個(gè)作用域創(chuàng)建一個(gè)private內(nèi)部類。比如,假設(shè)scope 1有局部變量itemp,而scope 2,是scope 1的嵌套作用域,有一個(gè)局部變量jtemp。讓在使用來自scope 1 和 scope 2局部變量itemp 和 jtemp的 scope 2中,我們定義一個(gè)匿名方法。下面的代碼顯示了上面描述的示例:
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();
}
}
當(dāng)上面的代碼被編譯時(shí),c#編譯器在''program''類中創(chuàng)建兩個(gè)內(nèi)部類。一個(gè)內(nèi)部類包裝局部變量itemp作為一個(gè)public數(shù)據(jù)成員。第二個(gè)內(nèi)部類包裝在嵌套作用域中的局部變量,jtemp,作為一個(gè)public數(shù)據(jù)成員,同時(shí)在相同的嵌套作用域中包裝匿名方法作為public實(shí)例方法。c#編譯器為上面的代碼生成下面的偽代碼:
public class program
{
//包裝來自外部作用域的局部變量''itemp''的類
private class innerclassscope1
{
public int itemp;
}
//包裝來自內(nèi)部作用域和匿名方法的局部變量的類
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();
}
}
正如上面的代碼所示,包裝匿名方法的內(nèi)部類將擁有所有代表外部作用域局部變量的對象,這些變量被用在匿名方法中,像public數(shù)據(jù)成員。下圖顯示了c#默默創(chuàng)建的內(nèi)部類的ildasm視圖:

在循環(huán)控制結(jié)構(gòu)內(nèi)使用匿名方法的局部變量的用法
當(dāng)處理循環(huán)控制結(jié)構(gòu)時(shí)將局部變量封裝入類的數(shù)據(jù)成員有著有趣但危險(xiǎn)的一面,讓我們看看下面代碼:
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();
}
}
上面的代碼運(yùn)行時(shí)將會有什么輸出呢?我們的意圖是捕獲在我們的匿名方法中的循環(huán)計(jì)數(shù)變量''i''并顯示之。我們預(yù)期的輸出應(yīng)該如下所示:
1
2
3
4
5
但是如果你運(yùn)行上面的代碼,輸出將是如下所示:
6
6
6
6
6
如果我們仔細(xì)回憶我們關(guān)于匿名方法的內(nèi)部工作機(jī)制的知識,我提到:在匿名方法中被捕獲的任何局部變量將會被該作用域的一個(gè)新的已創(chuàng)建內(nèi)部類的實(shí)例數(shù)據(jù)成員替代。對于循環(huán)控制變量,作用域是包含了for循環(huán)的作用域,這就是上面的簡單代碼所示的main方法體。因此當(dāng)該代碼編譯時(shí),c#編譯器生成創(chuàng)建了內(nèi)部類的實(shí)例的代碼,包裝了匿名方法和循環(huán)計(jì)數(shù)變量,在for循環(huán)的外部。并且該內(nèi)部類的實(shí)例的數(shù)據(jù)成員,代表了循環(huán)計(jì)數(shù)變量,將被用來替代用于for循環(huán)而且也在匿名方法中使用的原始循環(huán)計(jì)數(shù)變量。因此來自內(nèi)部類的相同實(shí)例的數(shù)據(jù)成員被用于for循環(huán)并且也用在包裝匿名方法的實(shí)例方法中。作為循環(huán)完成時(shí)的結(jié)果,實(shí)例數(shù)據(jù)成員會增加六次。這里有一個(gè)需要注意的重要地方:盡管這個(gè)循環(huán)在五次迭代后結(jié)束,在它跳出循環(huán)控制結(jié)構(gòu)時(shí)循環(huán)計(jì)數(shù)變量被增加了六次。既然該循環(huán)控制變量是一個(gè)實(shí)例數(shù)據(jù)成員,第六次增加觸發(fā)了已由循環(huán)計(jì)數(shù)變量提供的循環(huán)結(jié)束條件。既然相同實(shí)例的一個(gè)方法被用做匿名方法的委托處理器,在委托結(jié)束時(shí)被調(diào)用,所有委托的實(shí)例將被指向相同實(shí)例,同時(shí)將為數(shù)據(jù)成員顯示相同值,就是6。這就是我在本節(jié)開始已提到過的有危險(xiǎn)影響的一面。
為了克服這個(gè)問題并獲得預(yù)期的結(jié)果,匿名方法應(yīng)該在for循環(huán)的作用域中捕獲一個(gè)局部變量,它將有與循環(huán)計(jì)數(shù)變量的相同的值。這可以通過如下修改示例代碼獲得:
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();
}
}
在你運(yùn)行上面的代碼示例時(shí),將會獲得預(yù)期的輸出,也就是:
1
2
3
4
5
原因就是,c#編譯器將為for循環(huán)的每次迭代而包裝局部變量''k''的內(nèi)部類創(chuàng)建 實(shí)例。同時(shí)包裝了每個(gè)循環(huán)迭代的實(shí)例上的匿名方法的這個(gè)方法被用做一個(gè)委托處理器。
總結(jié)
匿名方法是c#2.0語言增加的一個(gè)非常有用和強(qiáng)大的功能。除了介紹的一些對委托聲明和用法上的語法改進(jìn),microsoft已在使匿名方法代碼自然融入所包含的方法體方面獲得很大進(jìn)展,包括訪問在包含(匿名方法)的方法定義的作用域中的局部變量。最后,我希望本文提供給c#開發(fā)人員正確而聰明地利用匿名方法的必備知識。