委托#

委托是一个类,它定义了方法的类型,指明了这个委托类型的变量可接受的函数,表示对具有特定参数列表和返回类型的方法的引用,使得可以将方法当作另一个方法的参数来进行传递

不管什么函数只要返回值类型和参数能匹配委托所指定的返回值类型和参数能匹配上,那么这个函数就能存储为一个委托变量的引用。

为什么需要委托#

  • 委托可以将方法作为参数
  • 逻辑解耦,保持稳定
  • 代码复用,保证项目规范

委托使用步骤#

  • 使用 delegate 关键字定义委托
  • 声明委托对应的方法
  • 实例化委托将方法作为参数传入
   class DelegateTest
    {
        //step01:使用delegate关键字定义委托
        public delegate int Sum(int x int y);

        static void Main(string[] args)
        {
            // step03:实例化委托将方法作为参数传入
            Sum sum = new Sum(new DelegateTest().Add);
            int result = sum.Invoke(1 2);
            Console.WriteLine(result);
            Console.ReadKey();
        }
        // step02:声明委托对应的方法
        public int Add(int x int y)
        {
            return x + y;
        }
    }
  1. 使用 delegate 关键字定义委托
  2. 声明委托对应的方法
  3. 实例化委托将方法作为参数传入

至此,一个委托就完成了。

匿名方法定义委托#

上面说到完成一个委托要分三步走缺一步都不行,但是微软可能感觉这么实现比较麻烦,非要把三步做成两步来走!所以就用匿名方法来简化上边的三个步骤。

// step01:首先用delegate定义一个委托 
public delegate int Sum(int x int y);

static void Main(string[] args)
{
    // step02:使用匿名方法的写法把一个方法赋值给委托
    Sum sum = delegate (int x int y) { return x + y; };
    int result = sum.Invoke(1 2);
    Console.WriteLine(result);
}

step01:使用 delegate 关键字定义委托 step02:使用匿名方法的写法把一个方法赋值给委托

这时会发现这里省略了定义方法这一步,将三步简化成了两步。

Lambda 表达式定义委托#

微软对C#的设计理念是简单易用。这时候发现对匿名方法的方式依旧不太满意,就想方设法的来简化 delegate(int x, int y) { return x + y; } 这个匿名方法,Lambda 就出现了。

lambda 运算符 => 左边列出了需要的参数,右边定义了赋予 lambda 变量的方法实现代码。

// step01:首先用delegate定义一个委托 
public delegate int Sum(int x int y);

static void Main(string[] args)
{
    // 方法一:
    Sum sum1 = (int x int y) => { return x + y; };
    int result1 = sum1(1 2);

    // 方法二:
    Sum sum2 = (x y) => { return x + y; };
    int result2 = sum2(1 2);

    // 方法三:
    Sum sum3 = (x y) => x + y;
    int result3 = sum3(1 2);
}

方法一:简单的把 delegate 去掉,在 (){} 之间加上 => 方法二:在方法一的基础上把参数类型都干掉了 方法三:要干就干彻底些,把 {} 以及 return 关键字都去掉了

注意:这三种方法随便怎么写都行

Lambda 表达式简写#

如果 lambda 表达式只有一句,方法块内就可以省略花括号和 return 语句,这时编译器会添加一条隐式的 return 语句。

Func<double double> func = param => param * param;

等价于

Func<double double> func = param =>
{
    return param * 2;
};

泛型委托#

随着.Net版本的不断升级,微软又来玩新花样了,不管是匿名方法还是 Lambda 表达式,完成一个委托的应用,都逃不过两个步骤,一步是定义一个委托,另一步是用一个方法来实例化一个委托。微软干脆把这两步都合成一步来走了。用 Func 来简化一个委托的定义。

static void Main(string[] args)
{
    //方法一:
    Func<int, int, int> add1 = (int x, int y) => { return x + y; };
    int result1 = add1(1 2);

    //方法二:
    Func<int, int, int> add2 = (x y) => { return x + y; };
    int result2 = add2(1 2);

    //方法三:
    Func<int int int> add3 = (x y) => x + y;
    int result3 = add3(1 2);
}

至此一个委托的应用就可用 Func<int, int, int> add3 = (x, y) => x + y; 一句话来完成了,其中的 Func 就是所谓的泛型委托。

微软提供了 Action<T>Func<T> 两种泛型委托,用于简化方法定义。

Action#

表示引用一个 void 返回类型的方法,可以传递最多16种不同的参数类型,没有泛型参数的 Action 类可调用没有参数的方法。

    // Action:无参数
    Action action1 = () => { Console.WriteLine("啦啦啦啦"); };
    action1();
    // Action:一个参数
    Action<string> action2 = p => { Console.WriteLine("啦啦啦啦,name:{0}",p); };
    action2("wang");
    // Action:多个参数
    Action<string, int> action3 = (name,age) => { Console.WriteLine("啦,name:{0},age:{1}", name,age); };
    action3("wang",25);

Func#

Func<T> 允许调用带返回类型的方法,可以传递 16种不同类型的参数和一个返回类型Func<out TResult> 委托类型可以调用带返回值且无参数的方法。

总结

  • Action<T> 用于没有返回值的方法(参数根据自己情况进行传递)
  • Func<T> 用于有返回值的方法(参数根据自己情况传递)

记住无返回就用 Action<T>,有返回就用 Func<T>

表达式树#

表达式树其实与委托已经没什么关系了,如果非要扯上关系,表达式树是存放委托的容器。如果非要说的更专业一些,表达式树是存取 Lambda 表达式的一种数据结构。要用 Lambda 表达式的时候,直接从表达式中获取出来 Compile() 就可以直接用了。

static void Main(string[] args)
{
    Expression<Func<int, int, int>> exp = (x, y) => x + y;
    Func<int, int, int> fun = exp.Compile();
    int result = fun(1, 2);
}

Invoke#

 Sum sum = delegate (int x, int y) { return x + y; };
 int result = sum.Invoke(1, 2);
 //等价于
 int result = sum(1,2);

委托数组#

定义 Math 类提供两个静态方法接收一个 double 类型的参数,用于计算倍数和阶乘。

class Math
{
    public static double MultipleTwo(double value)
        {
            return value * 2;
        }

    public static double Square(double value)
        {
            return value * value;
        }
}
public void Delegate_Array()
{
    // 定义委托数组
    Func<double, double>[] delegates = [
         Math.MultipleTwo,
         Math.Square
    ];

    // 使用委托数组
    for (int i = 0; i < delegates.Length; i++)
    {
        Console.WriteLine(delegates[i](3.7));
        Console.WriteLine(delegates[i](3));
    }
}

多播委托#

之前的每个委托都只包含一个方法调用,调用委托的次数与调用方法的次数相同,如果要调用多个方法,就需要多次显式调用这个委托。

但委托中也可以包含多个方法,称为多播委托。多播委托可以按顺序调用多个方法,为此委托的签名必须返回void,否则就只能得到委托最后调用的最后一个方法的结果。

Func<double, double> func = Math.MultipleTwo;
func += Math.Square;
var result = func(3.0);
Console.WriteLine(result);

只返回了3.0阶乘的值

+= 和 -=#

多播委托使用 +=-=,在委托中增加或删除方法调用。

static void Main(string[] args)
{
    Action action = Print.First;
    action += Print.Second;
    action();

    Action action2 = Print.First;
    action2 += Print.Second;
    action2 -= Print.First;
    action2();
    Console.ReadKey();
}
class Print
{
    public static void First()
    {
        Console.WriteLine("FirstMethod");
    }
    public static void Second()
    {
        Console.WriteLine("SecondMethod");
    }
}

如果要使用多播委托,就要知道对同一个委托调用方法链的顺序并未正式定义,因此要避免编写依赖于特定顺序调用方法的代码。

多播委托异常处理#

使用多播委托,意味着多播委托里包含一个逐个调用的委托集合,如果集合其中一个方法抛出异常.整个迭代就会停止。

Action action = () =>
{
    Console.WriteLine("hello");
    throw new Exception();
};
action += () => { Console.WriteLine("world"); };
action();

委托只调用了第一个方法,因为第一个方法抛出了异常,委托的迭代停止

GetInvocationList#

使用 Delegate的GetInvocationList() 方法迭代方法列表。

public void Delegate_GetInvocationList()
{
    Action action = () => { Console.WriteLine("hello"); throw new Exception(); };
    action += () => { Console.WriteLine("world"); };
    var delegates = action.GetInvocationList();

    // 如果不处理异常程序在抛出异常后停止
    //foreach (Action item in delegates)
    //{
    //    item();
    //}

    // 这里必须显式指定item类型为Action委托
    foreach (Action item in delegates)
    {
        // 修改后,程序在捕获异常后,会迭代下一个方法
        try
        {
            item();
        }
        catch (Exception error)
        {
            Console.WriteLine(error.Message);
        }
    }
}

修改后,程序在捕获异常后,会迭代下一个方法。

闭包的陷阱#

https://www.cnblogs.com/aehyok/p/3730417.html

源码:

List<Action> list = new List<Action>();
for (int i = 0; i < 5; i++)
{
    Action t = () => Console.WriteLine(i.ToString());
    list.Add(t);
}
foreach (Action t in list)
{
    t();
}

IL反编译

List<Action> list = new List<Action>();
TempClass tempClass = new TempClass();
for (tempClass.i = 0; tempClass.i < 5; tempClass.i++)
{
    Action t = tempClass.TempFunc;
    list.Add(t);
}
foreach (Action t in list)
{
    t();
}
 public class TempClass
 {
     public int i;
     public void TempFunc()
     {
         Console.WriteLine(i.ToString());
     }
 }

所谓的闭包对象,指的是上面这种情形中的 TempClass 对象。如果匿名方法(Lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到该闭包对象中。即将for循环中的变量 i 修改成了引用闭包对象的公共变量 i。这样一来,即使代码执行后离开了原局部变量 i 的作用域(如for循环),包含该闭包对象的作用域也还存在。

在Lambda表达式(或匿名方法)中所引用的外部变量称为捕获变量。而捕获变量的表达式就称为闭包。捕获的变量会在真正调用委托时“赋值”,而不是在捕获时“赋值”,即总是使用捕获变量的最新的值

注意:从C#5.0开始,foreach认为循环变量都应该是“新”的变量。所以,每次循环中创建委托时捕获的变量都不是同一个变量。所以遍历时使用foreach会自动捕获不同变量

修改一下源代码

List<Action> list = new List<Action>();
for (int i = 0; i < 5; i++)
{
    int temp = i;
    Action t = () => Console.WriteLine(temp.ToString());
    list.Add(t);
}
foreach (Action t in list)
{
    t();
}

事件#

事件是一种引用类型,实际上也是一种特殊的委托。事件基于委托,是提供了发布/订阅机制的委托,事件是将委托封装,并对外公布了订阅和取消订阅的接口。

有关事件的重要事项#

1、事件提供了对它的私有控制委托的结构化访问。我们无法直接访问该委托。

2、事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。

3、事件被触发时,它调用委托来依次调用调用列表中的方法。

有关事件的概念#

发布者(Publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。

订阅者(Subscriber):注册并在事件发生时得到通知的类或结构。

事件处理程序(event handler):由订阅者注册到事件的方法,在发布者触发事件时执行。

触发(raise)事件:调用(invoke)或触发(fire)事件的术语。当事件触发时,所有注册到它的方法都会被依次调用。

发布订阅模式示例#

/// <summary>
/// 发布者
/// </summary>
public class Pub
{
    // 定义事件所需委托
    public delegate void OpenEventHandler();

    // 使用委托类型定义事件
    public event OpenEventHandler OpenEvent;

    /// <summary>
    /// 定义事件触发的函数
    /// </summary>
    public void Open()
    {
        // 这个简单的修改可确保在检查空值和发送通知之间,如果一个不同的线程移除了所有OpenEvent订阅者,将不会引发NullReferenceException异常
        OpenEventHandler openEventHandler = OpenEvent;

        Console.WriteLine("总服务上线...");
        // 为确保有事件可用需要使用?.
        OpenEvent?.Invoke();
    }
}
/// <summary>
/// 事件订阅者A
/// </summary>
public class ServiceA
{
    public void ServiceAOpen()
    {
        Console.WriteLine("服务A已连接");
    }
}

/// <summary>
/// 事件订阅者B
/// </summary>
public class ServiceB
{
    public void ServiceBOpen()
    {
        Console.WriteLine("服务B已连接");
    }
}

调用

public void Example_01()
{
    Pub pub = new();
    ServiceA serviceA = new();
    ServiceB serviceB = new();

    // 事件订阅
    pub.OpenEvent += serviceA.ServiceAOpen;
    pub.OpenEvent += serviceB.ServiceBOpen;

    // 调用函数触发事件
    pub.Open();
}

标准 .NET事件模式#

  • 委托类型的名称都应该以EventHandler 结束
  • 委托的原型定义:有一个 void 返回值,并接受两个输入参数:
  • 委托原型具有两个参数:sender表示事件触发者,e表示事件参数;一个 Object 类型,一个 EventArgs 类型(或继承自EventArgs)
  • 事件的命名为委托去掉 EventHandler 之后剩余的部分
  • 继承自 EventArgs 的类型应该以 EventArgs 结尾
        public class Pub
        {
            /// <summary>
            /// 自定义事件参数以EventArgs结尾
            /// </summary>
            public class OpenEventArgs
            {
                public OpenEventArgs()
                {

                }
            }

            // 标准事件模式委托名称以EventHandler结尾
            // 委托的原型定义:有一个 void 返回值,并接受两个输入参数:一个 Object 类型,一个 EventArgs 类型(或继承自EventArgs)
            public delegate void OpenEventHandler(object sender, OpenEventArgs e);

            // 事件的命名为委托去掉 EventHandler 之后剩余的部分
            public event OpenEventHandler Open;

            public void OpenConn()
            {
                Console.WriteLine("总服务上线...");
                OpenEventArgs e = new();
                Open?.Invoke(this, e);
            }
        }

        public class ServiceA
        {
            public void ServiceAOpen(object sender, Pub.OpenEventArgs e)
            {
                Console.WriteLine("服务B已连接");
            }
        }

        public class ServiceB
        {
            public void ServiceBOpen(object sender, Pub.OpenEventArgs e)
            {
                Console.WriteLine("服务B已连接");
            }
        }
        public void Example()
        {
            Pub pub = new();
            ServiceA serviceA = new();
            ServiceB serviceB = new();

            // 事件订阅
            pub.Open += serviceA.ServiceAOpen;
            pub.Open += serviceB.ServiceBOpen;

            // 调用函数触发事件
            pub.OpenConn();
        }

参考#