開發人員通常都會選擇最熟悉的語言特性來描述組件之間的契約,對於大多數開發者來說,一般會使用基類或接口來定義其他類型所需要的方法,然後根據這些接口編寫代碼,通常來說這沒什麼問題,不過使用函數參數能夠讓其他開發者在使用你的組件和類庫時更容易些。使用函數參數意味著你的組件無需負責提供類型所需的具體處理邏輯,而是將其抽象出來,交給調用者實現。
我們都已經熟悉了通過接口或抽象類來實現分離,不過有些時候,定義並實現接口仍舊顯得過於笨重,雖然這樣做復合了傳統面向對象的理念,不過其他的技術則可以實現更簡單的API。使用委托一樣可以創建契約,同時也降低了客戶的代碼量。我們的目的是盡可能的將你的工作與客戶使用者代碼分離,降低兩者之間的依賴,如果不這樣做的話,會給你和你的使用者帶來不少的困難。你的代碼越是依賴其他代碼,就越難以單元測試或在其他地方重用。從另一方面考慮,你的代碼越是需要客戶代碼遵守某類特定的模式,那麼客戶也會承受越多的約束。
使用函數參數即可降低你的代碼與其他使用者的偶合程度。如下示例:.Net內部類List<T>中
List<T>.RemoveAll()方法就接受了一個委托類型PrePreDicate<T>,當然.Net的設計者也可以通過定義接口來實現該方法。
//帶來不恰當的額外耦合 public interface IPredicate<T> { bool Match(T soughtObject); } public class List<T> { public int RemoveAll(IPredicate<T> match) { //省略 return 0;//共移除多少條記錄 } //其他API省略 } //第二個版本的使用方式就有些復雜 public class MyPredicate : IPredicate<int> { public bool Match(int soughtObject) { return soughtObject < 100; } }
通過對比兩者,使用委托等更松散的方式來定義契約的話,那麼其他開發者使用起來將會更加容易。之所以用委托而不是接口定義契約,是因為委托並不是類型的基本屬性。這與方法的個數無關——很多.Net Framework中的接口都包含一個方法,例如IComparable<T>和IEquatable<T>等都是不錯的定義,實現這些接口意味著你的類型擁有了某些特定的屬性,支持相互之間進行比較或相等性的判斷。不過實現了這個假定的IPredicate<T>卻並沒有說明類型的特定屬性,對於那些單個API來說,定義一個方法就足夠了。
通常,在你開始考慮使用基類或接口時,可以考慮使用函數參數與泛型方法來配合使用,如下給出一個Concat示例。第一個方法為普通的序列拼接,第二個使用了泛型方法與函數參數來構造輸出序列
/// <summary> /// 對2個序列的每個元素進行拼接 /// </summary> public static IEnumerable<string> Concat(this IEnumerable<string> first, IEnumerable<string> second) { using (IEnumerator<string> firstSequence = first.GetEnumerator()) { using (IEnumerator<string> sencodSequence = second.GetEnumerator()) { while (firstSequence.MoveNext() && sencodSequence.MoveNext()) { yield return string.Format("{0} {1}", firstSequence.Current, sencodSequence.Current); } } } } /// <summary> /// 根據指定委托joinFunc,對2個序列的每個元素進行拼接 /// </summary> public static IEnumerable<TResult> Concat<T1, T2, TResult>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, TResult> joinFunc) { using (IEnumerator<T1> firstSequence = first.GetEnumerator()) { using (IEnumerator<T2> sencodSequence = second.GetEnumerator()) { while (firstSequence.MoveNext() && sencodSequence.MoveNext()) { yield return joinFunc(firstSequence.Current, sencodSequence.Current); } } } }
隨後調用者必須給出joinFunc的實現,如下,這樣就更加降低了Concat方法與調用者之間的耦合。
IEnumerable<string> result = CombinationSequence.Concat(first, second, (one, two) => string.Format("{0} {1}", one, two));
有些時候,我們想要在序列的每個元素上執行某個操作,最後返回一個匯總的數據。例如,下面的3個方法都將統計序列中所有整數的和,但三個方法都有差別,
1)第一個為一般性的統計,只能統計int類型,統計的累加方式為固定的。
2)第二個是針對一個方法進行了抽象,改成了泛型累加器,將Sum算法提取出來用一個委托來代替。那麼就可以統計任意類型的數據,累加的計算方式可以自己定義。
3)第三個是針對第二個進行修改,因為第二個寫法中,Sum仍舊有不少限制,其必須使用與序列中的元素、返回值、初始值同樣的類型,而我們可能需要使用一些不同的類型。所以可以對Sum方法進行少量的修改,允許序列中的元素與累加的結果使用不同的類型。
public static int Sum(IEnumerable<int> nums) { int total = 0; foreach (var num in nums) { total += num; } return total; } public static T Sum<T>(this IEnumerable<T> sequence, Func<T, T, T> accumulator) { T total = default(T); foreach (var item in sequence) { total = accumulator(total, item); } return total; } public static TResult Sum<T, TResult>(this IEnumerable<T> sequence, Func<TResult, T, TResult> accumulator) { TResult total = default(TResult); foreach (var item in sequence) { total = accumulator(total, item); } return total; }
使用函數參數能夠很方便的將算法與特定的數據類型分離。若你的對象保存了傳入的委托以備稍後調用,那麼這個對象就控制了委托中對象的生命周期,這就可能延長了此類對象的生命周期,這和先讓一個對象引用另一個對象(通過存放對接口或基類的引用),然後再使用的情況沒什麼不同,但在閱讀代碼時更難以發現。
在定義組件與其他客戶代碼的通信契約時,默認的選擇仍然是接口。抽象基類則能提供一些默認的公共實現,讓客戶代碼無需重復編寫,而為方法定義委托則提供了最大的靈活性,但也意味著你得到的支持會更少,總的來講,就是用更多的工作換來更好的靈活性。