2016年4月19日 星期二

【C#】Event & Delegate


Event (事件)
是方法的一種,一般是 UI 行為引發的方法,比方
private void Button_Click(object sender, EventArgs e)
{
    ...
}
當按鈕按下就會觸發,sender 即按鈕 instance,e 是事件參數,在特定事件行為有機會用到,例如
private void DataGridView_RowValidating(object sender, DataGridViewCellCancelEventArgs e)
{
    if(e.RowIndex > 0)
        ...
}
這是 WinForm DataGridView 的資料列驗證事件,這裡 e 提供了 RowIndex 屬性讓我們可以知道現在驗證中的具體是哪一列。

當然事件是可以自己寫,比方自己動態產生的多個按鈕需要指派不同的事件就需要自己去寫,不過大部分的情況我們會是用系統建出來的。

接著,讓我們走進細節;具體來說 .Net 中事件必須是 event 形式的委派。

delegate (委派)
是一個方法類別化、變數化的做法,看程式比較容易懂
delegate void PrinterMethodClass(string msg);
static void PrintMethod1(string msg)
{
    Console.WriteLine("type1: " + msg);
}
static void PrintMethod2(string msg)
{
    Console.WriteLine("type2: " + msg);
}
static void Main()
{
    PrinterMethodClass Print = new PrinterMethodClass(PrintMethod1);
    Print.Invoke("some message.");
    Console.ReadLine();
}
其中 PrinterMethodClass 是委派(方法的 class)
PrintMethod1、PrintMethod2 是符合 PrinterMethodClass 方法簽名與回傳值的實作(方法的 instance)
Print 是宣告為特定委派的變數(方法的 variable),其實際運行的是 PrintMethod1 instance。

這個設計的目的在於提供外部注入方法的彈性
static void Foo(PrinterMethodClass concretePrint)
{
    var print = new PrinterMethodClass(concretePrint);
    print.Invoke("some message.");
    Console.ReadLine();
}
和附加自己的事件到 UI 的事件上。

Event (事件) #2
我們已經知道委派長怎樣,事件相對的會是像
static event EventHandler _eh;
其中 event 等同 delegate,但是它指定了這個委派屬於事件,方法簽名與回傳值格式必為 void (object sender, EventArgs e),所以也不需要像前面的 void (string msg)。
EventHandler 相當於 PrinterMethodClass,但它是所有 .Net 事件的 class。

EventHandler(事件處理函式)
handle 是從「把手」發展來的講法,可以想成遊戲的手把,接一條線去控制主機。
static event EventHandler _eh;

static void Main()
{
    ConsoleKeyInfo keyinfo;
    do
    {
        keyinfo = Console.ReadKey();
        if (keyinfo.Key == ConsoleKey.UpArrow)
        {
            _eh = new EventHandler(Up);
        }
        else if (keyinfo.Key == ConsoleKey.DownArrow)
        {
            _eh = new EventHandler(Down);
        }
        else if (keyinfo.Key == ConsoleKey.LeftArrow)
        {
            _eh = new EventHandler(Left);
        }
        else if (keyinfo.Key == ConsoleKey.RightArrow)
        {
            _eh = new EventHandler(Right);
        }
        _eh.Invoke(new object(), new EventArgs());
    }
    while (keyinfo.Key != ConsoleKey.X);
}

static void Up(object sender, EventArgs e)
{
    Console.Write("↑ ");
}

static void Down(object sender, EventArgs e)
{
    Console.Write("↓ ");
}

static void Left(object sender, EventArgs e)
{
    Console.Write("← ");
}

static void Right(object sender, EventArgs e)
{
    Console.Write("→ ");
}
因為是事件用的所以 sender 跟 EventArgs 是必要的。

匿名方法 (Anonymous Method)
實務上指派給委派變數的方法常常就只會用在這麼一個地方,比方 PrintMethod1() 要拆開寫在Main()外面又沒有別的地方會用到,其實有點多餘所以
PrinterMethodClass Print = delegate(string msg)
{
    Console.WriteLine("anonymous type: " + msg);
};
其實可以寫成這樣,delegate(string msg) 即是一個匿名方法,因為假定別的地方不會用到所以方法名稱就省下來了,直接把整個方法 instance 指給委派變數。

Lambda
就是高度簡化的匿名方法,最常應用於 Linq,比如前述的式可以再簡化
PrinterMethodClass Print = (string msg) =>
{
    Console.WriteLine("anonymous type: " + msg);
};
delegate 字樣就省下來了,因為只有一行所以
PrinterMethodClass Print = (string msg) => Console.WriteLine("anonymous type: " + msg);
可以這樣,又因為只有一個參數所以
PrinterMethodClass Print = msg => Console.WriteLine("anonymous type: " + msg);
連括號跟型別都可以省掉,我個人是覺得有點省過頭了,對第一次看到不知道這個演進流程的人來說,應該是一頭霧水。我自己是參考 這篇 跟 這篇,真的是少數寫給人類看的文章。

CallBack (回呼)
一個 CallBack,是指一個被當成引數傳進其他方法的 method instance,在得到該 instance 的方法中達成特定條件後才會觸發這個 instance。 這個設計主要的功能在預期需要擴充的類似功能方法組合從業務邏輯類別裡隔離出來,例如
public class Program
{
    public delegate string GetName();

    static void Main()
    {
        string input = Console.ReadLine();
        GetName MethodFromExpand = ExpandClass.GetMethod(input);
        PrintName(MethodFromExpand());

        Console.ReadLine();
    }

    private static void PrintName(string output)
    {
        Console.WriteLine(output);
    }
}

public class ExpandClass
{
    public static MyLab.Program.GetName GetMethod(string input)
    {
        switch (input)
        {
            case "ENG":
                return GetEngName;
            default:
                return GetChineseName;
            //break;
        }
    }

    private static string GetChineseName()
    {
        return "王小明";
    }

    private static string GetEngName()
    {
        return "HSIAO MING WANG";
    }        
}
如前例,當我要再擴充其他語系的 GetName 就只需要擴在 ExpandClass 即可,能保持主程式業務邏輯的乾淨。

※ 額外補充,在 javascript 中 Callback 的作法,除了方法作為參數傳遞之外,另一個很重要的用途是讓傳遞進去的方法實體在接收端方法完成後才執行,因為 javascript 是邊編譯邊執行所以需要這樣作流程控制,這個部分有興趣的朋友可以自己搜尋一下。

Action 與 Func
考慮到 Callback 在現代的程式設計已經算頗常用的功能,每一次要自己宣告 delegate,且幾乎都只會用在同個地方,實在是很搞剛,所以 .NET 提供了 Action 與 Func 兩個固定的委派來簡化其處理,比如上面的程式就可以改成
public class Program
{
    //public delegate string GetName(); 不需要

    static void Main()
    {
        string input = Console.ReadLine();
        Func<string> MethodFromExtand = ExpandClass.GetMethod(input); // 由 Func 取代
        PrintName(MethodFromExtand());

        Console.ReadLine();
    }
    ...
}

public class ExpandClass
{
    public static Func<string> GetMethod(string input) // 由 Func 取代
    {
        ...      
}
其中 Action 跟 Func 的不同只在於 Action 是用於沒有回傳值的方法 Action<T>、Action<T, T>等,T為參數,而 Func 是用於有回傳值的情況 Fun <TResult>、Func<T, TResult>等,TResult 為回傳值。

沒有留言:

張貼留言