iT邦幫忙

2

C# delegate 委派 實戰篇

我最早開始使用委派

是在開發遊戲功能的時候

當時有個需求是需要寫一個角色升級的功能

(當年是個人吃人升級的時代...所以某A角色要升級 需要吃其他角色

流程大概如下

選定A角色 -> 點擊升級按鈕 -> 跳出角色選擇視窗 -> 選好角色 -> 按下OK升級

程式碼就從 點擊升級按鈕的事件那邊開始寫 架構大概是這樣

/// <summary>
/// 定義一個角色的類別
/// </summary>
public class Character
{

}
/// <summary>
/// 寫一個角色升級的UI
/// </summary>
public class CharacterLevelUpUI
{
    /// <summary>
    /// 可以被吃的角色清單
    /// </summary>
    private List<Character> CharacterList { get; set; }
    /// <summary>
    /// 被選擇要吃掉的角色清單
    /// </summary>
    private List<Character> SelectedCharacterList { get; set; }
    public CharacterLevelUpUI(List<Character> characterList)
    {
        CharacterList = characterList;
    }

    public void ShowUI()
    {
        //這裡當作已經選完要吃的角色了
        SelectedCharacterList = new List<Character>();
    }
    /// <summary>
    /// 這裡當作按下OK鈕會觸發的事件
    /// </summary>
    public void OKBtnClick()
    {
        //這裡角色要被吃掉
        //SelectedCharacterList Delete
    }
}
/// <summary>
/// 點擊角色升級的按鈕
/// </summary>
public static void LevelUpButtonClick()
{
    var characters = new List<Character>();//可以被吃的角色清單
    var ui = new CharacterLevelUpUI(characters);
    ui.ShowUI();
}

static void Main()
{
    //執行
    LevelUpButtonClick();
}

看起來沒啥問題?

過了兩天 又接到一個功能要做

這次要做的事情是 要做角色冒險尋寶功能

流程大概是這樣

點擊冒險按鈕 -> 跳出角色選擇視窗 -> 選好角色 -> 按下OK派出選中的角色去冒險

喔耶~好棒棒 那就複製現有的UI稍微改一下就好~(大概有很多人會這樣做吧?

/// <summary>
/// 寫(複製)一個角色尋寶的UI
/// </summary>
public class CharacterTreasureHunt
{
    /// <summary>
    /// 可以被選擇尋寶的角色清單
    /// </summary>
    private List<Character> CharacterList { get; set; }
    /// <summary>
    /// 被選擇要派出尋寶的角色清單
    /// </summary>
    private List<Character> SelectedCharacterList { get; set; }
    public CharacterTreasureHunt(List<Character> characterList)
    {
        CharacterList = characterList;
    }

    public void ShowUI()
    {
        //這裡當作已經選完要派出的角色了
        SelectedCharacterList = new List<Character>();
    }
    /// <summary>
    /// 這裡當作按下OK鈕會觸發的事件
    /// </summary>
    public void OKBtnClick()
    {
        //這裡角色要去尋寶
        //SelectedCharacterList GO TO Treasure Hunt~~
    }
}

/// <summary>
/// 點擊角色冒險的按鈕
/// </summary>
public static void TreasureHuntButtonClick()
{
    var characters = new List<Character>();//可以派出冒險的角色清單
    var ui = new CharacterTreasureHunt(characters);
    ui.ShowUI();
}

static void Main()
{
    //執行
    TreasureHuntButtonClick();
}

恩恩~ 我真是太強大了! 這樣多來幾個也沒關係~

那就再來一個角色出戰吧!

/// <summary>
/// 寫(複製)一個角色出戰的UI
/// </summary>
public class CharacterBattle
{
    /// <summary>
    /// 可以被選擇出戰的角色清單
    /// </summary>
    private List<Character> CharacterList { get; set; }
    /// <summary>
    /// 被選擇要派出出戰的角色清單
    /// </summary>
    private List<Character> SelectedCharacterList { get; set; }
    public CharacterBattle(List<Character> characterList)
    {
        CharacterList = characterList;
    }

    public void ShowUI()
    {
        //這裡當作已經選完要出戰的角色了
        SelectedCharacterList = new List<Character>();
    }
    /// <summary>
    /// 這裡當作按下OK鈕會觸發的事件
    /// </summary>
    public void OKBtnClick()
    {
        //這裡角色要去出戰
        //SelectedCharacterList GO TO Fight!
    }
}

/// <summary>
/// 點擊出戰的按鈕
/// </summary>
public static void BattleButtonClick()
{
    var characters = new List<Character>();//可以派出出戰的角色清單
    var ui = new CharacterTreasureHunt(characters);
    ui.ShowUI();
}

static void Main()
{
    //執行
    BattleButtonClick();
}

這裡大概30秒就完成了 複製 貼上 修改一下註解跟實作

於是就這樣 我陸續複製了7-8個角色選擇功能的UI

然後晴天霹靂的事情來了!!(登愣!!

企劃想要修改UI?(謎之音:我覺得新版UI比較酷!

也就是說 我現在手上有將近10個長得一模一樣的UI必須要修改

這是一件會死人的事情....

仔細思考一下 其實這些功能大部分是一樣的 只有選完之後的事情不同

所以我們可以把最後選完角色的事情給參數化 也就是委派

/// <summary>
/// 寫一個角色選擇的UI
/// </summary>
public class CharacterSelectUI
{
    /// <summary>
    /// 可以被選的角色清單
    /// </summary>
    private List<Character> CharacterList { get; set; }
    /// <summary>
    /// 被選擇角色清單
    /// </summary>
    private List<Character> SelectedCharacterList { get; set; }
    private Action<List<Character>> SelectFinished { get; set; }
    public CharacterSelectUI(List<Character> characterList,Action<List<Character>> selectFinished)
    {
        CharacterList = characterList;
        SelectFinished = selectFinished;
    }

    public void ShowUI()
    {
        //這裡當作已經選完角色了
        SelectedCharacterList = new List<Character>();
    }
    /// <summary>
    /// 這裡當作按下OK鈕會觸發的事件
    /// </summary>
    public void OKBtnClick()
    {
        //你要做什麼事情我不知道 但是我把選好的角色交給你讓你去決定你要作什麼
        SelectFinished(SelectedCharacterList);
    }
}

/// <summary>
/// 點擊角色升級的按鈕
/// </summary>
public static void LevelUpButtonClick()
{
    var characters = new List<Character>();//可以被吃的角色清單
    var ui = new CharacterSelectUI(characters,(characters) =>
    {
        //TODO LevelUp
    });
    ui.ShowUI();
}
/// <summary>
/// 點擊角色冒險的按鈕
/// </summary>
public static void TreasureHuntButtonClick()
{
    var characters = new List<Character>();//可以派出冒險的角色清單
    var ui = new CharacterSelectUI(characters, (characters) =>
    {
        //TODO TreasureHunt
    });
    ui.ShowUI();
}

/// <summary>
/// 點擊出戰的按鈕
/// </summary>
public static void BattleButtonClick()
{
    var characters = new List<Character>();//可以派出出戰的角色清單
    var ui = new CharacterSelectUI(characters, (characters) =>
    {
        //TODO Battle
    });
    ui.ShowUI();
}

這樣把事情權責拆分 選擇角色的UI就只負責選擇角色 真正要做的事情由呼叫端處理就可以

於是早先複製那麼多UI是沒必要的 這是委派常用的情境 -- CallBack

再隨便給個CallBack委派的例子 詢問視窗

跳出詢問視窗 按下Yes要做某件事情,按下No要做另外一件事情

這邊先借用Winform的UI

public static void Ask(string message,Action yes,Action no)
{
   var result = MessageBox.Show(message, "詢問", MessageBoxButtons.YesNo);
    if(result == DialogResult.OK)
    {
        yes.Invoke();
    }
    else if(result == DialogResult.No)
    {
        no.Invoke();
    }
}
static void Main()
{
    Ask("你要吃魚嗎?", () => { /*開始吃魚*/ }, () => { /*沒魚吃*/ });
    Ask("你要喝茶嗎?", () => { /*開始喝茶*/ }, () => { /*沒茶喝*/ });
}

我只是晚餐時間有點餓了..請勿多做聯想!!!

委派另外一個常用的情境 就是 event 事件

public class AlarmClock
{
    public Action Alarm;

    public AlarmClock(DateTime alarmTime)
    {
        var sleep = DateTime.Now - alarmTime;//計算要等多久
        Task.Run(async () =>
        {
            await Task.Delay(sleep);
            Alarm.Invoke();
        });
    }
}

static void Main()
{
    //設定一個鬧鐘 在2021/3/8 8:00:00 會提醒
    var alarmClock = new AlarmClock(new DateTime(2021,3,8,8,0,0));
    alarmClock.Alarm = () =>
    {
        //三月八號婦女節快樂~
    };
}

像上面這樣寫 在指定的時間到的時候 就會呼叫你指定的匿名方法//三月八號婦女節快樂~

但是這樣寫並不好 主要是因為 就算不是鬧鐘本身 也可以去執行這個Alarm 例如

static void Main()
{
    //設定一個鬧鐘 在2021/3/8 8:00:00 會提醒
    var alarmClock = new AlarmClock(new DateTime(2021,3,8,8,0,0));
    alarmClock.Alarm = () =>
    {
        //三月八號婦女節快樂~
    };
    alarmClock.Alarm.Invoke();//在這裡就會被呼叫
}

或者有心人(白目同事?)也可以作這種處理

static void Main()
{
    //設定一個鬧鐘 在2021/3/8 8:00:00 會提醒
    var alarmClock = new AlarmClock(new DateTime(2021,3,8,8,0,0));
    alarmClock.Alarm = () =>
    {
        //三月八號婦女節快樂~
    };
    alarmClock.Alarm = null; //你指定的鬧鐘事件就會不見了!!!然後那天你沒幫老婆買禮物就死定!
}

所以 在 event情境上 我們需要在委派前面 加上 event關鍵字 加上去以後 除了自身類別以外

不能將其指定為null及invoke

public class AlarmClock
{
    public event Action Alarm;//前面加上event關鍵字

    public AlarmClock(DateTime alarmTime)
    {
        var sleep = DateTime.Now - alarmTime;
        Task.Run(async () =>
        {
            await Task.Delay(sleep);
            Alarm.Invoke();
        });
    }
}

static void Main()
{
    //設定一個鬧鐘 在2021/3/8 8:00:00 會提醒
    var alarmClock = new AlarmClock(new DateTime(2021,3,8,8,0,0));
    
    alarmClock.Alarm = () =>//error
    {
        //三月八號婦女節快樂~
    };
    alarmClock.Alarm = null; //error
}

加上event後 你會發現出現兩個error 原因是 event只能使用 +=, -= 不能使用 = 來註冊事件

這邊開個小副本 Action +=,-=,=的差別

public static void Print5()
{
    Console.WriteLine(5);
}
static void Main(string[] args)
{
    Action action = () => Console.WriteLine(1);
    action += () => Console.WriteLine(2);
    action += () => Console.WriteLine(3);
    action.Invoke();// print 1 2 3
    Console.WriteLine();
    action = () => Console.WriteLine(4);
    action.Invoke();//print 4
    Console.WriteLine();
    action -= () => Console.WriteLine(4);
    action.Invoke();//print 4 !!!??????
    Console.WriteLine();
    action += Print5;
    action.Invoke();//print 4 5
    Console.WriteLine();
    action -= Print5;
    action.Invoke();//print 4
}

委派 是可以疊加的 所以一開始我宣告 1 後續在疊加上 2 3 所以會印出 1 2 3

然後我把 4 直接指派給action 這邊不是疊加 是清空 所以只會印出4

然後我把 4 給減掉 可是為什麼還是會跑出4呢?

這邊就用到上一篇提到的匿名函式 雖然都是print4 但是實際上這兩個匿名方法是不同的記憶體 雖然執行的事情是相同

所以 -= 不起效用

我們把匿名方法換成具名方法 Print5 就會發現 +=上去後 會印出 4 5

-=後 堆疊上的print5也會被移除

副本結束 回歸正題!!

所以加上event關鍵字後 外面要註冊事件的人 只能跟委派說 我要註冊 或是註銷事件

我不能去執行(Invoke) 或是 重新指派事件給委派

真正能執行 跟重新指派的 只有在AlarmClock內才可以做到

static void Main()
{
    //設定一個鬧鐘 在2021/3/8 8:00:00 會提醒
    var alarmClock = new AlarmClock(new DateTime(2021,3,8,8,0,0));
    alarmClock.Alarm += () =>
    {
        //三月八號婦女節快樂~
    };
    alarmClock.Alarm += () =>
    {
        //這天要面試 別忘了!
    };
}

所以如果這樣寫 指定的時間一到 就會呼叫這兩個跟Alarm註冊的方法

平常有寫一點code的 應該多少都對巨硬的
https://ithelp.ithome.com.tw/upload/images/20210305/20135608hB3MuVpxoE.jpg

這東西有點印象 這些都是巨硬的事件 只要註冊對應的事件

就會在正確(?)的時機呼叫你註冊的方法 最常看到的大概就是Winform的 Button.Click

var button = new Button();
button.Click += (s, e) =>
{
    //我被點了
};

另外一個很常用到委派 但可能不知道自己正在用的 Linq(我還真有同事用Linq不知道那個是委派

https://ithelp.ithome.com.tw/upload/images/20210305/20135608zu3KoxTUAd.jpg

看到沒有?

static void Main()
{
    List<int> datas = new List<int>();
    // var result = datas.Where(x => x % 2 == 0).ToList();
    Func<int, bool> predicate = x => x % 2 == 0;
    List<int> result = new List<int>();
    foreach (var data in datas)
    {
        if (predicate(data))
        {
            result.Add(data);
        }
    }
}

Where裡面就差不多長這樣 但是這還扯到 yield 所以我稍微調整過 然後省略一些安全性檢查

有興趣的可以去看原始碼(巨硬已經開源了!

https://github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs

所以其實Where的方法就是要你提供一個刪選器 Func<int,bool>(我這個範例List的元素型別是int才會是int)

會把你List的元素一個個丟進去讓你驗證

你就依照喜好回傳true(要)false(不要) 最後他會把篩選過的結果交還給你 是不是很方便呢?

好了! 本篇實戰篇就此結束!

終於可以去吃魚喝茶了~ 餓...


尚未有邦友留言

立即登入留言