我最早開始使用委派
是在開發遊戲功能的時候
當時有個需求是需要寫一個角色升級的功能
(當年是個人吃人升級的時代...所以某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的 應該多少都對巨硬的
這東西有點印象 這些都是巨硬的事件 只要註冊對應的事件
就會在正確(?)的時機呼叫你註冊的方法 最常看到的大概就是Winform的 Button.Click
var button = new Button();
button.Click += (s, e) =>
{
//我被點了
};
另外一個很常用到委派 但可能不知道自己正在用的 Linq(我還真有同事用Linq不知道那個是委派
看到沒有?
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(不要) 最後他會把篩選過的結果交還給你 是不是很方便呢?
好了! 本篇實戰篇就此結束!
終於可以去吃魚喝茶了~ 餓...