程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 高階函數、委托與匿名方法

高階函數、委托與匿名方法

編輯:關於.NET

高階函數(higher-order function)是指把另一個函數作為參數或返回值的函數。例如 在JavaScript語言中,Function是頂級類型。一個函數就是類型為 Function的頂級對象,自 然就可以作為另一個函數的參數或返回值。例如在Microsoft AJAX Library(ASP.NET AJAX 的客戶端類庫)中有一個被廣泛使用的createDelegate方法。該方法接受一個對象A和一個函 數F作為參數,並返回一個函數R。當調用函 數R時,F函數將被調用,並且保證無論在什麼上 下文中,F的this引用都會指向對象A:

Function.createDelegate = function(instance, func) {
  return function() {
  return callback.apply(a, arguments);
  } 
}

委托是.NET平台中一種特殊的類型。有人說,它是一種強類型的函數指針。這種說法雖然 細節上略失偏頗,但是從功能和作用上講不無道理。有了委 托類型,一個方法就能被封裝成 一個對象被作為另一個方法的參數或返回值,這自然就為.NET平台上的語言(例如C#, VB.NET)引入了對高階函數的“ 原生支持”1。例如在System.Array類中就有許 多靜態的高階函數,其中的ConvertAll方法可謂是最常用的高階函數之一了:

private static DateTime StringToDateTime(string s) {
  return DateTime.ParseExact(s, "yyyy-MM-dd", null);
}

static void Main(string[] args) {
  string[] dateStrings = new string[] {
  "2009-01-01", "2009-01-02", "2009-01-03",
  "2009-01-04", "2009-01-05", "2009-01-06",
  };

  DateTime[] dates = Array.ConvertAll<string, DateTime>(
  dateStrings, new Converter<string, DateTime> (StringToDateTime));
}

ConvertAll

將一個數組映射為另一個數組,就好像Ruby中array類型的map方法一樣,但是如果您會發 現ruby的“內聯”寫法會方便許多。於是在C# 2.0中,又引入了匿名方法這一構建委托對象 的方式:

string[] dateStrings = new string[] {
  "2009-01-01", "2009-01-02", "2009-01-03",
  "2009-01-04", "2009-01-05", "2009-01-06",
};

DateTime[] dates = Array.ConvertAll<string, DateTime>(
  dateStrings,
  delegate(string s) {
  return DateTime.ParseExact(s, "yyyy-MM-dd", null);
  });

匿名方法並不只是“匿名”的方法,它甚至可以構造一個閉包給開發帶來極大的便利。可 見在2.0中已經為高階函數在C#中的運用打下了堅實的基 礎。而且,由於新增了Lambda表達 式和擴展方法等語言特性,再加上范型類型的自動判斷,在C# 3.0中使用匿名方法更是異常 簡潔,甚至與ruby的語法如出一轍:

IEnumerable<DateTime> dates = dateStrings.Select(
  s => DateTime.ParseExact(s, "yyyy-MM-dd", null));

從理論上說,委托從在.NET 1.x環境中即得到了完整的支持,但是直到C# 3.0之後高階函 數在.NET中的應用切實地推廣開來。善於使用高階函數的特性能夠有效地提高開發效率,同 時使代碼變得優雅、高效。為了方便開 發,.NET 3.5中甚至定義了三種泛化的委托類型: Action<>、Predicate<>以及Func<>,讓開發人員可 以在項目中直接使 用。如今,微軟官方的各種框架和類庫(例如著名的並行庫)中對於高階函數的使用幾乎將 其變成了一種事實標准。在這一點上,Lambda表達式和匿名方法可謂居功至偉。

高階函數的一個重要特點就是對參數方法的延遲執行。例如,對於普通的方法調用方式來 說:

DoSomething(Method1(), Method2(), Method3());

代碼中涉及到的四個方法調用順序已經完全確定:只有當Method1、Method2和Method3三 個方法依次調用完畢並返回之後,DoSomething方法才會執行。然而,如果我們改變 DoSomething方法的簽名,並使用這樣的方式:

DoSomething(() => Method1(), () => Method2(), () =>  Method3());

這樣,四個方法中DoSomething方法肯定首先被調用,然後根據方法體內的具體實現,其 余三個方法可能被調用任意次數——甚至一次也不會 被調用。利用這個特性,即“提供方法 體,但是不執行”,我們就可以在某些邏輯不確定的情況下避免不必要的開銷。例如,如果 不使用高階函數,一段處理數據的 邏輯可能是這樣的:

void Process(object dataThatIsExpensiveToGet) {
  bool canProcess = GetWhetherDataCanBeProcessedOrNot();
  if (canProcess) {
  DoSomeThing(dataThatIsExpensiveToGet);
  }
}

在上例中,Process方法只有在滿足特定前提的情況下才對參數進行處理,而且在很多時 候這個前提條件必須在Process方法中才能判斷。 這時,如果參數本身需要昂貴的代價才能 獲得,那麼獲取參數的損耗就白白被浪費了。為了避免這種無謂的消耗,我們可以在設計 Process方法API時使用 如下辦法:

void Process(Func<object> expensiveDataGetter) {
  bool canProcess = GetWhetherDataCanBeProcessedOrNot();
  if (canProcess) {
  object dataToProcess = expensiveDataGetter();
  DoSomeThing(dataToProcess);
  }
}

這樣,我們就可以使用如下的方式來調用Process方法:

// Process(GetExpensiveData(args));
Process(() => GetExpensiveData(args));

與注釋掉的代碼相比,消耗巨大GetExpensiveData方法並不會被直接調用,而只有在 Process方法內滿足前提條件時才會執行。有時候,我們甚至可以在第一個參數方法滿足特定 條件時才執行另一個參數方法。在《您善於使用匿名函數嗎?》一文中的CacheHelper便是這 樣一個例子:

public static class CacheHelper
{
  public delegate bool CacheGetter<TData>(out TData data);

  public static TData Get<TData>(
  CacheGetter<TData> cacheGetter, /* get data from cache */
  Func<TData> sourceGetter, /* get data from source (fairly  expensive) */
  Action<TData> cacheSetter /* set the data to cache */)
  {
  TData data;
  if (cacheGetter(out data)) {
  return data;
  }

  data = sourceGetter();
  cacheSetter(data);

  return data;
  }
}

CacheHelper的Get方法接受三個委托對象作為參數,只有當第一個方法(從緩存中獲取對 象)返回為False時,才會執行第二個(從 相對昂貴的數據源獲取數據)和第三個方法(將 數據源中得到的數據放入緩存)。同時,這個示例也展示了高階方法的另一個常用特點:封 裝一段通用的邏輯,將邏 輯中特定部分的交由外部實現——這不就是“模板方法(Template Method)模式”嗎?高階函數從某個角度可以看成是一種輕量級的模板方法實現,它提供了 模板方法中的主要特性,但是不需要使用“繼承”這種耦合性很高 的擴展方式。而且,由於 可以為一個委托參數提供任意的實現,我們也可以在某些場景下用它來代替“策略 (Strategy)模式”的使用。

不過也由此可見,高階函數並不一定需要“函數指針”或“委托類型”的支持。事實上, 面向對象語言中的對象可以攜帶方法,而一個方法可以接受另一 個對象作為參數(或返回一 個對象),那麼這個方法自然也就相當於一個接受或返回方法的“高階函數”了。例如,我 們可以使用Java來實現如上的CacheHelper輔助類:

public interface Func<T> {
  T execute();
}

public interface Action<T> {
  void execute(T data);
}

public class CacheHelper {
  public static<T> T get(
  Func<T> cacheGetter,
  Func<T> sourceGetter,
  Action<T> cacheSetter)
  {
  T data = cacheGetter.execute();
  if (data == null) {
  data = sourceGetter.execute();
  cacheSetter.execute(data);
  }

  return data;
  }
}

不過從C#的演變過程中可以看出,高階函數的特性要真正得到推廣,也必須由“匿名方法 ”等更多特性加以輔佐才行。Java中的“匿名類”與C#中的“匿名方法”有異曲同工之處, 例如,開發人員同樣可以使用內聯的寫法來調用CacheHelper:

public Object getData()
{
  return CacheHelper.get(
  new Func<Object>() {
  public Object execute() {
  /* get data from cache */
  return null;
  }
  },
  new Func<Object>() {
  public Object execute() {
  /* get data from source (fairly expensive) */
  return null;
  }
  },
  new Action<Object>() {
  public void execute(Object data) {
  /* set the data to cache */
  }
  });
}

可惜,有些時候類似的代碼在Java語言中相對並不那麼實用。其原因可能是因為Java中“ 匿名類”語法較為復雜,且匿名類的內部邏輯無法修改調用方法裡的局部變量——由此也可 對比出C#中匿名函數這一特性的美妙之處。

注1:嚴格來說,.NET只是提供了一個平台,一個“運行時(CLR)”,但“高階函數”其 實是個語言方面的概念。我們可以在.NET上實現任意一種語 言,而這種語言就算沒有得到平 台的直接支持,也能夠實現“高階函數”這個特性。因此,之所以是“原生支持”,其實指 的是.NET平台對高階函數所需的特性 有著直接的支持,它使得C#或VB.NET等語言中能夠直接 使用高階函數這一功能。

結論:

.NET 3.5對於創建委托對象的良好支持使得高階函數在.NET平台上的使用得到了卓有成效 的推廣。從微軟新發布的框架和類庫中來看,高階函數幾乎已經成為了一 種事實標准。善於 使用高階函數的特性能夠有效地提高開發效率,同時使代碼變得優雅、高效。可以料想的到 ,善於使用高階函數會逐步成為一個優秀的.NET開 發人員的必備技術。

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