我最早开始使用委派
是在开发游戏功能的时候
当时有个需求是需要写一个角色升级的功能
(当年是个人吃人升级的时代...所以某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(不要) 最后他会把筛选过的结果交还给你 是不是很方便呢?
好了! 本篇实战篇就此结束!
终于可以去吃鱼喝茶了~ 饿...