[C#]Delegate、Action、Func、Predicate介紹

  • 8605
  • 0
  • C#
  • 2016-07-08

Delegate、Action、Func、Predicate介紹

起因

最近因為被一位上課認識朋友啟發開始我的部落格之路,

最近在上91的自動測試與TDD實務開發時寫功課剛好遇到的問題。

題目 :

一個任意型別Collection,使用者可以輸入要針對欄位、幾個為一組進行加總,結果產出一個IEnumerable<int>回傳。

題目內的欄位這項需求十分的有趣,要如何讓使用者可以自己指定要哪一欄位呢?

  1. 直接傳欄位名稱進去比對 (Bad Idea! 如果輸入錯誤怎辦!? )
  2. 使用委派方式,讓使用者自己去處理欄位

計算的Method :

public IEnumerable<int> GetOrderSetSum(int groupSize,IEnumerable<Order> order,Func<Order,int> orderType)
{
     var count = 0;
     while (count <= order.Count())
     {
          yield return order.Skip(count).Take(groupSize).Sum(orderType);
          count += groupSize;
     }
}

呼叫的Method :

[TestMethod()]
public void GroupSize_Is_3_CalGroupCost_Should_Be_6_15_24_21()
{
     //arrange
     var target = new CalGroupTotal();
     var groupSize = 3;
     var order = GetOrderList();
     //act
     var actual = target.GetOrderSetSum(groupSize,order, x => x.Cost);
     //assert
     var excepted = new List<int> { 6, 15, 24, 21 };
     actual.Should().Equal(excepted);
}

在這兩段Code當中,只是將Production Code的指定欄位的Linq部分交由呼叫端去設定,這樣設計的方式較具有彈性。

正文

以下就開始進入今天的正題了,在當初設計這種API呼叫的方式的時候其實還不知道要用哪種委派,

那時弱弱的我只聽見腦海裏面的Delegate Q_Q,後來翻翻MSDN才發現有種四種委派(Delegate、Action、Func、Predicate)就深入探討了一下。

1.Delegate

MSDN註解

  • 委派是事件的基礎。
  • 將委派與具名方法或匿名方法建立關聯,即可具現化 (Instantiated) 委派。
  • 委派必須以具有相容傳回型別和輸入參數的方法或 Lambda 運算式具現化。

MSDN範例(註解改成中文 by powen)

// 1.宣告 delegate -- 定義簽章:
delegate double MathAction(double num);

class DelegateTest
{
 // 2.一般的方法跟delegate的簽章相符:
 static double Double(double input)
 {
    return input * 2;
 }

 static void Main()
 {
  // 3.藉由方法名稱 Instantiate delegate
  MathAction ma = Double;

  // 4.呼叫 delegate ma
  double multByTwo = ma(4.5);
  Console.WriteLine("multByTwo: {0}", multByTwo);

  // 3.藉由匿名方法 Instantiate delegate
  MathAction ma2 = delegate(double input)
  {
     return input * input;
  };
  // 4.呼叫 delegate square
  double square = ma2(5);
  Console.WriteLine("square: {0}", square);

  // 3.藉由lambda expression Instantiate delegate
  MathAction ma3 = s => s * s * s;
  // 4.呼叫 delegate cube
  double cube = ma3(4.375);
  Console.WriteLine("cube: {0}", cube);
  }
  // 輸出:
  // multByTwo: 9
  // square: 25
  // cube: 83.740234375
}

2.Action(無回傳值)

MSDN註解

  • 您可以使用這個委派以傳遞做為參數的方法,而不必明確宣告自訂委派。
  • 封裝的方法必須對應由這個委派所定義的方法簽章。這表示封裝方法必須沒有參數且沒有傳回值。
  • 一般而言,這種方法是用於執行作業。

MSDN範例(註解改成中文 by powen)

1.直接實體化並指定方法

using System;
using System.Windows.Forms;

public class Name
{
   private string instanceName;

   public Name(string name)
   {
      this.instanceName = name;
   }

   public void DisplayToConsole()
   {
      Console.WriteLine(this.instanceName);
   }
   //1.建立沒有回傳值的方法
   public void DisplayToWindow()
   {
      MessageBox.Show(this.instanceName);
   }
}

public class testTestDelegate
{
   public static void Main()
   {
      Name testName = new Name("Koani");
      //2.實體化Action並指派方法
      Action showMethod = testName.DisplayToWindow;
      showMethod();
   }
}

 2.匿名方法指派給Action

using System;
using System.Windows.Forms;

public class Name
{
   private string instanceName;

   public Name(string name)
   {
      this.instanceName = name;
   }

   public void DisplayToConsole()
   {
      Console.WriteLine(this.instanceName);
   }

   public void DisplayToWindow()
   {
      MessageBox.Show(this.instanceName);
   }
}

public class Anonymous
{
   public static void Main()
   {
      Name testName = new Name("Koani");
      //Action實體化並直接將方法內容也撰寫在這,通常用於一次性呼叫
      Action showMethod = delegate() { testName.DisplayToWindow();} ;
      showMethod();
   }
}

3.Lambda運算式指派給Action

using System;
using System.Windows.Forms;

public class Name
{
   private string instanceName;

   public Name(string name)
   {
      this.instanceName = name;
   }

   public void DisplayToConsole()
   {
      Console.WriteLine(this.instanceName);
   }

   public void DisplayToWindow()
   {
      MessageBox.Show(this.instanceName);
   }
}

public class LambdaExpression
{
   public static void Main()
   {
      Name testName = new Name("Koani");
      //● 使用Lambda運算式指派給Action,程式碼更加簡潔
      //● 通常用於一次性應用
      Action showMethod = () => testName.DisplayToWindow();
      showMethod();
   }
}

3.Func(有回傳值)

MSDN註解

  • 您可以使用這個委派表示可做為參數傳遞的方法,而不必明確宣告自訂委派。
  • 封裝的方法必須對應由這個委派所定義的方法簽章。
  • 這表示封裝方法必須沒有參數且必須傳回值

基本語法 (以一個參數為例): 

public delegate TResult Func<in T, out TResult>(
	T arg
)
 Func可以封裝具有多個參數的方法,並傳回TResult參數所指定的型別。

MSDN範例(註解改成中文 by powen)

1.直接實體化並指定方法

using System;

public class GenericFunc
{
   public static void Main()
   {
      //藉由Func<T,TResult>委派執行實體化
      Func<string, string> convertMethod = UppercaseString;
      string name = "Dakota";
      Console.WriteLine(convertMethod(name));
   }

   private static string UppercaseString(string inputString)
   {
      return inputString.ToUpper();
   }
}

 2.匿名方法指派給Func

using System;

public class Anonymous
{
   public static void Main()
   {
      Func<string, int, string[]> extractMeth = delegate(string s, int i)
         { char[] delimiters = new char[] {' '}; 
           return i > 0 ? s.Split(delimiters, i) : s.Split(delimiters);
         };

      string title = "The Scarlet Letter";
      // 使用Func實體化並委派給匿名方法
      foreach (string word in extractMeth(title, 5))
         Console.WriteLine(word);
   }
}

3.Lambda運算式指派給Func

using System;

public class LambdaExpression
{
   public static void Main()
   {
      char[] separators = new char[] {' '};
      Func<string, int, string[]> extract = (s, i) => 
           i > 0 ? s.Split(separators, i) : s.Split(separators) ;

      string title = "The Scarlet Letter";
      // 使用Func實體化並委派給Lambda運算式
      foreach (string word in extract(title, 5))
         Console.WriteLine(word);
   }
}

4.Predicate(回傳布林值)

  • 通常, Predicate<T> 委派使用 Lambda 運算式。
  • 定義一組準則的方法,並判斷指定的物件是否符合這些準則。

1.定義Predicate並指派方法 

using System;
using System.Drawing;

public class Example
{
   public static void Main()
   {
      // Create an array of Point structures.
      Point[] points = { new Point(100, 200), 
                         new Point(150, 250), new Point(250, 375), 
                         new Point(275, 395), new Point(295, 450) };

      // 定義 Predicate<T> delegate.
      Predicate<Point> predicate = FindPoints;

      // 尋找第一個點 X * Y 大於 100000
      Point first = Array.Find(points, predicate);

      // 輸出第一點大於 100000
      Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
   }

   private static bool FindPoints(Point obj)
   {
      return obj.X * obj.Y > 100000;
   }
}
// The example displays the following output:
//        Found: X = 275, Y = 395

2.使用Lambda運算式表示Predicate<T>委派

using System;
using System.Drawing;

public class Example
{
   public static void Main()
   {
      // Create an array of Point structures.
      Point[] points = { new Point(100, 200), 
                         new Point(150, 250), new Point(250, 375), 
                         new Point(275, 395), new Point(295, 450) };

      // 尋找第一個點 X * Y 大於 100000
      Point first = Array.Find(points, x => x.X * x.Y > 100000 );

      // 輸出第一點大於 100000
      Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
   }
}
// The example displays the following output:
//        Found: X = 275, Y = 395
不同的點在於這邊不是明確定義他是Predicate<T>,而是利用Lambda運算式去呈現Predicate委派。

結論:

由上面介紹可以得知後面三種方法基本上都是由delegate延伸出來的,

都是C#內簡化程式碼實作的衍生委派作法,

我們可以根據自己使用的情境去決定該使用哪種委派,

  1. Action (無回傳值)
  2. Func (參數傳入、有回傳值)
  3. Predicate(回傳布林值)

簡單跟各位介紹以上四種差異,

謝謝收看,

有緣再會啦 XD