用C# 2.0的朋友可能會經常使用匿名方法(Anonymous Methods)和匿名委托(Anonymous Delegate)。在這裡我說2個比較常用的應用環境。
C#和C++, Java一樣擁有異常處理機制,我相信很多朋友和我一樣,第一次接觸異常的時候,都非常希望異常能夠像內核捕獲內存page fault異常一樣類似的擁有Retry(重試)的機制,很可惜這些語言中都沒有給我們提供Retry機制。
當然原因有很多,比如如果重試,那麼到底是重試哪部分代碼呢?是try{}catch{}中的代碼?還是發生異常那一行的代碼?重試多少次?重試之間是不是要再等一會兒?如果再失敗怎麼處理?而且如果不同層有Retry,那麼會出現Retry次數以乘法形式遞增(底層Retry 5次,高層Retry 3次,那麼實際Retry次數將達到15次)。
在應用環境中的Retry不比內核,處理後只需要重新執行產生異常的那一條指令,應用環境的要求要更復雜。
可是在實際應用中,特別是面對網絡應用時候,有可能會有大量的類似於TimeOut(超時)、或者外部資源被臨時占用、暫時性的設備較忙的異常產生,只要再重試幾次就會正常。如果此時將raise異常,並且扔向更高層,顯然是不太明智的。於是就開始有人用有限次循環的辦法來模擬Retry。典型的代碼如下:
public static void TraditionalSolution()
...{
int retryTimes = 10;
for (int i = 0; i < retryTimes; ++i)
...{
try
...{
// do something here,
// such as call WebClIEnt to fetch a webpage.
// the code might throw an exception.
break;
}
catch (Exception)
...{
if (i == retryTimes - 1)
...{
throw;
}
else
...{
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
}
}
}
}使用這樣的代碼可以成功的實現Retry,如果中間的代碼發生異常,循環體會捕捉到,如果沒有到達retryTimes的要求,那麼繼續重復執行中間的過程。每次retry前都會先等待5秒鐘。如果到達retryTimes後依舊還發生異常,那就沒辦法,throw到上層。這段代碼很典型,在C++和Java中也可以使用類似的方式進行處理。
這段代碼的缺點就是,這層循環、等待、判斷循環終結拋出異常的代碼總要附著在你需要retry的代碼塊外面的事情周圍。如果程序中有不少需要retry的地方,這部分代碼反復出現就顯得很臃腫了。而且,如果這段代碼有一點小小的錯誤,或者有更多的需求加入進去,要同步項目中其它地方retry代碼,這就很不合理了。下面我們就利用C#的匿名方法(anonymous methods)和匿名委托(anonymous delegate)來美化我們的代碼。
首先,我們先聲明一個無參數委托。
public delegate void NoArgumentHandler();然後,我們寫一個專門處理Retry機制的函數。
public static void Retry(int retryTimes, TimeSpan interval, bool throwIfFail, NoArgumentHandler function)
...{
if (function != null)
...{
for (int i = 0; i < retryTimes; ++i)
...{
try
...{
function();
break;
}
catch (Exception)
...{
if (i == retryTimes - 1)
...{
if (throwIfFail)
...{
throw;
}
else
...{
break;
}
}
else
...{
if (interval != null)
...{
System.Threading.Thread.Sleep(interval);
}
}
}
}
}
}
這段代碼實現了前面代碼的Retry機制,把需要執行的部分,換成了NoArgumentHandler的函數參數。使用者可以定義function()執行需要retry的次數,retry間隔的等待時間,當retryTimes跑完後,依舊未能成功地話,是否拋出這個異常。
最後,我們應用這個Retry()函數。大家都看到了delegate,那麼我們需要對每一個retry塊都定義一個函數麼?不需要了,C# 2.0早就提供了anonymous methods(匿名方法)來處理。我們看一下,新的Retry調用代碼:
public static void NewApproach()
...{
// ...
Retry(10, TimeSpan.FromSeconds(10), true, delegate
...{
// do something here,
// such as call WebClIEnt to fetch a webpage.
// the code might throw an exception.
});
// ...
}
這樣我們的Retry代碼簡潔多了。對現有代碼增加Retry機制,只需要加上一層括號就可以了,而且Retry行為可以很方便的在參數上調整。
我們可以進一步注意到delegate後面連()也省略掉了,也就是說delegate沒有函數調用簽名,我們這裡使用的是匿名委托(anonymous delegate),讓C#編譯器去自己推導正確的delegate類型。在這個case裡面,由於Retry()對應位置只有NoArgumentHandler一個delegate類型,因此,將delegate{}自動視為NoArgumentHandler類型。如果有什麼進一步需求的話,可以自己改進Retry()函數,定制為自己的Retry()。舉個例子,可以為Retry()再增加一個delegate函數參數,用以進行異常發生後的處理。
注意:Retry機制很方便用,但是不要過分使用Retry。特別是在不同層使用Retry的時候更要細心。特別要注意乘法效應導致Retry次數和時間成倍增長的情況,否則反而不美。
2、WinForm 的線程安全的訪問用C#寫Windows程序少不了要寫WinForm程序。很多時候,我們還需要寫多線程的WinForm應用。最典型的就是為了不因為核心代碼執行影響用戶對應用程序的響應,當執行一個比較耗時的操作時,為了方式用戶界面死掉的情況,常需要建立一個背景線程去運行耗時的代碼,並且實時將結果表現在當前窗口上。
在多線程訪問WinForm的時候,我們會注意到,WinForm的那些Control不是線程安全的。因此不建議直接訪問它們,否則會導致競爭冒險,甚至可能出現死鎖。比如在線程中直接使用下面的代碼就是不推薦的:
textBox1.Text = “OK”;微軟建議的辦法,使建立一個SetTextBox(string text)的函數,裡面寫上上面這句。然後判斷this.InvokeRequired,如果需要,就調用this.Invoke(SetTextBox, “OK”);,否則直接調用textBox1.Text = “OK”。
總結一下會發現,這個建議的方法不合理,因為同樣是textBox1.Text = “OK”; 在不同的位置上被寫了兩次。兩次?驚醒。凡是同一個東西,在不同的位置出現了兩次,我們就需要驚醒了。很多安全問題都是由於這種兩個不同位置表達一個意思而造成。如果修改了一個地方,而忘了修改另一個地方怎麼辦?如果是數據的話,還會出現,用戶代碼到底會用哪個數據作為基准?雖然程序員在“盡量”保證兩個位置一致,但是歷史已經無數次告訴我們這種“盡量”非常不可靠。那麼我們如何解決這個問題呢?這次匿名方法和匿名委托又一次顯身手了。
首先,我們定義一個線程安全的訪問Control的函數 DoThreadSafe():
private static void DoThreadSafe(Control control, MethodInvoker function)
...{
if (function != null)
...{
if (control.InvokeRequired)
...{
control.Invoke(function);
}
else
...{
function();
}
}
}這裡的代碼實現了微軟推薦的采用InvokeRequired判斷,然後通過Invoke()調用具體操作的邏輯,但是通過入口的MethodInvoker function函數參數避免了同一個東西被寫兩次的情況。這裡MethodInvoker是System.Windows.Forms名字空間下的一個delegate,和上面的NoArgumentHandler定義一樣:
delegate void MethodInvoker();有了這個小小的幫助函數,我們寫線程安全的 WinForm 操作就很簡單了,比如,這回我們需要設置進度條的Value:
private void SetProgressBar(int value)
...{
DoThreadSafe(progressBar1, delegate
...{
progressBar1.Value = value;
});
}
凡是需要訪問 WinForm 空間,我們都包裹上這麼一行代碼,就能夠保證對WinForm所作的操作時線程安全的了,很方便。
如果進一步注意,我們會發現,雖然我們的delegate是無參數傳遞的,但是,在上面的調用代碼裡面,prograssBar1.Value = value,這個value是delegate外的函數地參數。神奇吧?這是合法的,雖然名義上,這是一個匿名方法,已經不屬於當前scope了,但是依舊可以訪問當前scope裡面的變量,這就給我們很大的便利,我們可以充分利用這一點,而不再需要定義各種各樣的有參數的delegate來完成對不同控件所需要的線程安全的操作。呵呵,否則,按照微軟的建議,那幾乎是要對WinForm上每一個元素都做一個SetXxxxxText(string text), GetXxxxxText()函數了。
這個小小的DoThreadSafe(),大大降低了對那些Delegate的需求,並且利用匿名方法(anonymous methods)大大減少了聲明函數的工作量。(當然實際上編譯器在生成執行代碼的時候會幫你自動產生對應的函數,不信你就用Reflector來看看。)
3、總結
匿名方法可以降低另寫method的工作量,而且匿名方法可以訪問調用者同scope的變量,利用這點我們可以大大簡化委托的聲明,和降低傳參的復雜度。
好好的利用匿名方法和匿名委托,會讓你的代碼看起來更加優雅,優雅的代碼也會降低錯誤發生的可能性。