iT邦幫忙

DAY 8
2

分享一些學習心得系列 第 8

LINQ自學筆記-打地基-泛型委派

匿名委派大幅精簡呼叫端使用委派的程式碼,那宣告端,我們就利用泛型委派來精簡吧!本篇將和大家分享泛型委派的實務應用方式。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
我們在具名委派時有說過,委派可分為宣告端、邏輯端和呼叫端,然後在上一篇匿名委派時,我們看到在 .Net 2.0 如何透過匿名委派的方式,大幅精簡呼叫端所需要撰寫的程式碼,但是當時並沒有說宣告端是否有可以精簡的部分。本篇文章,我們將先就 .Net 2.0 的泛型委派來看怎麼精簡宣告端,下一篇會再說明這種泛型委派的模式,到了 .Net 3.5,出現 Func 委派、Action 委派的變化。

先回顧一下在匿名委派時簡化後的程式碼:

//宣告端
public delegate string 除蟲(string 蟲); 
public class 豪宅 { 
    public void 蟲出沒(除蟲 人){ 
        Console.WriteLine(人("蟑螂")); 
    }
} 
//呼叫端
void Main()  { 
  豪宅 白宮 = new 豪宅(); 
  白宮.蟲出沒(delegate(string 蟲) { 
                return "噴殺蟲劑讓 " + 蟲 + " 掛點。\n";} 
              ); 
}

宣告端在委派的角色中,它做了兩件事:

  1. 定義一個委派結構,包含兩個部分:類別名稱和委派方法簽章。在範例中,委派名稱叫「除蟲」,方法簽章則是傳入一個 string 參數,回傳 string。
  2. 定義委派被使用的時候點,也就是那個方法會需要使用委派。在範例中,就是「蟲出沒」這個方法會要求一個名為「除蟲」的委派參數,並在方法的執行細節中要求委派執行工作(又稱之為引動(Invoke))。

但是實務中應用委派時,我們會發現一件事:開發人員宣告了很多委派,可是這些委派的方法簽章都雷同,有時候甚至一模一樣,所以其實這些委派定義應該可以共用,但是因為宣告委派要定義名稱,所以總不能把叫做「除蟲」的委派拿去給「種花」用,這樣未來維護系統時,很容易產生混淆:

//宣告端
public class 豪宅 {
    public delegate string 除蟲(string 蟲);
    public delegate string 種花(string 花);
    public delegate string 掃廁所(Location 地點);
    public delegate string 巡邏(Area 區域);
    
    public void 蟲出沒(除蟲 人){
        Console.WriteLine(人("蟑螂"));
    }
    public void 整理花園(種花 人){
        Console.WriteLine(人("玫瑰"));
    }
    public void 維護廁所清潔(掃廁所 人){
        Location l = new Location();
        l.Name = "主臥房";
        Console.WriteLine(人(l));
    }
    public void 維護住宅安全(巡邏 人){
        Area a = new Area();
        a.Name = "大門";
        Console.WriteLine(人(a));
    }
}
//呼叫端 - 管家派工
void Main()  {
  豪宅 白宮 = new 豪宅();
  白宮.蟲出沒(delegate(string 蟲) {
                return 蟲 + " 死光光。";}
              );
  白宮.整理花園(delegate(string 花) {
                return 花 + " 種好了。";}
              );
  白宮.維護廁所清潔(delegate(Location 地點) {
                        return 地點.Name + " 廁所亮晶晶。";}
                    );
  白宮.維護住宅安全(delegate(Area 區域) {
                        return 區域.Name + " 安全無虞。";}
                    );
}
public class Master{
    private string name;
    public string Name {get {return name;} set {name = value;}}
}
public class Area {
    private string name;
    public string Name {get {return name;} set {name = value;}}
}
public class Location {
    private string name;
    public string Name {get {return name;} set {name = value;}}
}
/* 輸出:
蟑螂 死光光。
玫瑰 種好了。
主臥房 廁所亮晶晶。
大門 安全無虞。
*/

所以我們思考如何共用委派定義:是不是可以比照匿名委派一樣,宣告時省略委派名稱,或者是以一個較為「抽象」的名稱來代表。因為委派會被當做方法的傳入參數,委派名稱就等於方法參數的型別名稱,不可能省略,所以匿名不大可能;那抽象名稱,該怎麼設定才恰當,又是一個大問題,因為還會牽扯到方法簽章的內容。於是,我們把關注的焦點拉到委派的方法簽章。

方法簽章就是方法的回傳值和傳入參數的型別及數量,而方法簽章影響到我們怎麼對這個方法所屬的委派命名,更進一步推論,其實把委派名稱就當做方法名稱,那整個委派不過就是一個方法,而我們在「泛型」時有講過泛型方法,如果我們讓方法的傳入參數都變成泛型,由呼叫端執行時才決定,也就是讓這個抽像概念的委派變成是泛型委派(generic delegate),如此這個委派的適用性就更高了,委派的名稱理所當然可以用很抽象的表達方式完成。
所以範例中,我們便可把「除蟲」、「掃廁所」、「巡邏」合併為一個「日常勤務」的委派,這樣只要是豪宅的工作任務,只有一個工作重點,那都可以透過「日常勤務」泛型委派來執行:

public class 豪宅 {
    public delegate TResult 日常勤務<T, TResult>(T 工作重點);
    
    public void 蟲出沒(日常勤務<string, string> 人){
        Console.WriteLine(人("蟑螂"));
    }
    public void 維護廁所清潔(日常勤務<Location, string> 人){
        Location l = new Location();
        l.Name = "主臥房";
        Console.WriteLine(人(l));
    }
    public void 維護住宅安全(日常勤務<Area, string> 人){
        Area a = new Area();
        a.Name = "大門";
        Console.WriteLine(人(a));
    }
}
//呼叫端 - 管家派工
void Main()  {
  豪宅 白宮 = new 豪宅();
  白宮.蟲出沒(delegate(string 蟲) {
                return 蟲 + " 死光光。";}
              );
  白宮.維護廁所清潔(delegate(Location 地點) {
                        return 地點.Name + " 廁所亮晶晶。";}
                    );
  白宮.維護住宅安全(delegate(Area 區域) {
                        return 區域.Name + " 安全無虞。";}
                    );
}

因為只有一個工作重點,日常勤務這個泛型委派都可以輕鬆處理,省去重覆定義委派的程式碼。但是問題來了,日常勤務只收一個參數,如果有一些勤務是需要兩個參數,該怎麼辦呢?套用前面的邏輯,我就再定義一個接收兩個參數的泛型委派即可。三個參數呢,定義三個參數的泛型委派,以此類推。但是不管要接收幾個參數,其實都是日常勤務啊,而且甚至非日常勤務的工作,因為是泛型委派,所以只要參數的數量匹配,就可以套用,那我們是不是可以把「日常勤務」這個名稱再更抽象化?甚至以此泛型委派的概念向外推展,我們會推論出一件事實:「只要是委派,其實差異就是委派的方法簽章而已」,也就是不同的是這三件事情:

  1. 回傳的資料型別。
  2. 委派方法參數型別。
  3. 委派方法參數的數量。

而透過「泛型委派」,我們就可以輕鬆解決上述 1、2 兩點和型別有關的差異,就只剩第 3 點而已了。那第 3 點的解法,就是上述的,多定義幾個不同數量的泛型委派囉。

好,那這三個差異都解決了,剩下什麼?泛型委派的名稱,前面有提到,委派名稱就是方法參數的型別名稱,因此不可能省略,一定要定義,但是解決上述三點後,這些不同數量的泛型委派名稱,似乎也沒有意義了,因為任何需要委派的地方都可以用啊!最後,我們只好定義把這些委派,都命名叫「委派」:

public delegate TResult 委派<T, TResult>(T 委派參數); 
public delegate TResult 委派<T1, T2, TResult>(T1 委派參數1, T2 委派參數2); 
public delegate TResult 委派<T1, T2, T3, TResult>(T1 委派參數1, T2 委派參數2, T3 委派參數3); 

至此,我們雖然沒有直接把委派宣告端須撰寫的程式精簡,但因為把這些委派變成是極度抽象、高度共用的泛型委派,所以我們可以事先把不同參數數量的泛型委派定義好,然後放到獨立的專案中,編譯成獨立的 DLL,其他專案開發時,只要加入這個 DLL 參考,匯入命名空間,便可省去撰寫委派命名和定義的程式碼。
PS. 為易於了解,下面的範例是 VS2005 的版本。
用 GenericDelegateStore.cs 類別存放泛型委派:

using System;
using System.Collections.Generic;
using System.Text;

namespace DelegatePractice_Net20.GenericDelegateStore
{
    public delegate TResult 委派<T, TResult>(T 委派參數);
    public delegate TResult 委派<T1, T2, TResult>(T1 委派參數1, T2 委派參數2);
    public delegate TResult 委派<T1, T2, T3, TResult>(T1 委派參數1, T2 委派參數2, T3 委派參數3);
    public delegate TResult 委派<T1, T2, T3, T4, TResult>(T1 委派參數1, T2 委派參數2, T3 委派參數3, T4 委派參數4);
}

在要使用的專案中,加入 GenericDelegateStore 的參考,並匯入命名空間,就可以直接使用,不用再自行定義委派了:

using System;
using System.Collections.Generic;
using System.Text;
using DelegatePractice_Net20.GenericDelegateStore;

namespace DelegatePractice_Net20
{
    //宣告端
    public class 豪宅
    {
        public void 蟲出沒(委派<string, string> 人)
        {
            Console.WriteLine(人("蟑螂"));
        }
        public void 維護廁所清潔(委派<Location, string> 人)
        {
            Location l = new Location();
            l.Name = "主臥房";
            Console.WriteLine(人(l));
        }
        public void 維護住宅安全(委派<Area, string> 人)
        {
            Area a = new Area();
            a.Name = "大門";
            Console.WriteLine(人(a));
        }
    }
    class Program
    {
        //呼叫端 - 管家派工
        static void Main(string[] args)
        {
            豪宅 白宮 = new 豪宅();
            白宮.蟲出沒(delegate(string 蟲)
            {
                return 蟲 + " 死光光。";
            }
                        );
            白宮.維護廁所清潔(delegate(Location 地點)
            {
                return 地點.Name + " 廁所亮晶晶。";
            }
                              );
            白宮.維護住宅安全(delegate(Area 區域)
            {
                return 區域.Name + " 安全無虞。";
            }
                              );
        }
    }
    public class Master
    {
        private string name;
        public string Name { get { return name; } set { name = value; } }
    }
    public class Area
    {
        private string name;
        public string Name { get { return name; } set { name = value; } }
    }
    public class Location
    {
        private string name;
        public string Name { get { return name; } set { name = value; } }
    }
}

上一篇
LINQ自學筆記-打地基-匿名委派
下一篇
LINQ自學筆記-打地基-Func委派、Action委派
系列文
分享一些學習心得30

尚未有邦友留言

立即登入留言