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(不要) 最後他会把筛选过的结果交还给你 是不是很方便呢?

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

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


<<:  证书颁发机构(CA)-Web服务器证书格式

>>:  进击的软件工程师之路-软件战斗营 第二周

Day06 - Parsing Ptt(补充)

接续Day04,在确认连上Ptt後,会将页面跳转至Login页,原本Day04应该要把这些都写进去的...

用React刻自己的投资Dashboard Day29 - 替股票加上名称

tags: 2021铁人赛 React 上一篇提到台股技术面的最新收盘资讯只有股票代号,似乎少了名称...

【PHP Telegram Bot】Day08 - 官方范例程序

今天不写程序,先来看看官方的机器人范例 官方范例 完整程序码:https://core.teleg...

Day25-"指标变数"

普通变数宣告後是占用某一块记忆体空间,该空间内则存放变数资料,例如:整数变数就存放整数资料,字元变数...

[Day8] 从入门到入狱! 用Python窃听电脑键盘事件!

《刑法》第315之1条:「无故利用『工具』或『设备』窥视、窃听」或无故以『录音』、『照相』、『录影...