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的 应该多少都对巨硬的
http://img2.58codes.com/2024/20135608hB3MuVpxoE.jpg

这东西有点印象 这些都是巨硬的事件 只要注册对应的事件

就会在正确(?)的时机呼叫你注册的方法 最常看到的大概就是Winform的 Button.Click

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

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

http://img2.58codes.com/2024/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(不要) 最后他会把筛选过的结果交还给你 是不是很方便呢?

好了! 本篇实战篇就此结束!

终于可以去吃鱼喝茶了~ 饿...


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章