C#基础_委托详解

前言

本文略长,如果不想全部看的话,可以在上面的目录上选择自己感兴趣的

什么是委托

委托是用来处理其他语言(如 C++、Pascal 和 Modula)需用函数指针来处理的情况的。不过与 C++ 函数指针不同,委托是完全面向对象的;另外,C++ 指针仅指向成员函数,而委托同时封装了对象实例和方法。
委托声明定义一个从 System.Delegate 类派生的类。委托实例封装了一个调用列表,该列表列出了一个或多个方法,每个方法称为一个可调用实体。对于实例方法,可调用实体由该方法和一个相关联的实例组成。对于静态方法,可调用实体仅由一个方法组成。用一个适当的参数集来调用一个委托实例,就是用此给定的参数集来调用该委托实例的每个可调用实体。
委托实例的一个有趣且有用的属性是:它不知道也不关心它所封装的方法所属的类;它所关心的仅限于这些方法必须与委托的类型兼容。这使委托非常适合于“匿名”调用。
------------摘抄自C#语言定义文档

委托(delegate)是函数指针的"升级版"

为什么说委托(delegate)是函数指针的"升级版"实例演示:
首先声明一个名为Calculator的类,并为其声明三个实例方法,
第一个方法返回值为空,参数列表为空,它的任务是在控制台打印一行字
其余两个方法返回值是int类型,参数列表是两个整数类型变量

class Calculator
    {
    
    
        public void Report()
        {
    
    
            Console.WriteLine("我有三个方法");
        }
        public int Add(int a , int b)
        {
    
    
            int result = a + b;
            return result;
        }
        public int Sub(int a,int b)
        {
    
    
            int result = a - b;
            return result;
        }
    }

一切皆地址

程序的本质就是数据加算法

  1. 变量(数据)是以某个地址为起点的一段内存中所存储的值**
    变量的本质就是以变量名对应的地址为起点这样的内存,在这段内存中存储的就是这个变量的数据,这段内存有多长,是由变量的类型所决定的
  2. 函数(算法)是以某个地址为起点的一段内存中所存储的一组机器语言指令
    函数的本质也是地址,是以函数名所对应的内存地址为起点的一段内存,这段内存中它所存储的不是某个值,而是一组机器语言的指令,CPU就是按照这组指令一条一条的去执行,完成这个函数当中所包含的算法

也就是说,无论是数据还是算法都是保存在内存当中的,而变量是用来寻找数据的地址,函数,是用来寻找算法的地址,这就是所谓的一切皆地址

直接调用与间接调用

  1. 直接调用:通过函数名来调用函数,CPU通过函数名直接获得函数所在地址并开始执行->返回
  2. 间接调用:通过函数指针来调用函数,CPU通过读取函数指针存储的值获得函数所在地址并开始执行->返回

无论是直接调用还是间接调用CPU所访问的那组机器语言是一样的,所以说,无论是直接调用还是间接调用效果是完全一样的这就是直接调用与间接调用的本质

java中没有与委托相对应的功能实体

Java语言是C++语言发展而来的,在发展的过程当中,为了应用程序的安全性,Java语言禁止程序员直接去访问地址,也就是说Java语言把C++语言中所有与指针相关的内容都舍弃掉了,没有了指针,程序员就不能直接访问内存地址,而且java对指针的舍弃相当彻底,它没有保留与函数指针相对应的这部分功能,C#语言也是由C++语言发展而来的,但是它通过委托这种数据类型保留了与函数指针相对应的功能

委托的简单使用

在上面C++函数指针的例子当中,我们需要先声明函数指针类型,再使用这类型,
而在C#的类库当中已经为我们准备了很多可以直接使用的委托类型,下面挑选两个比较常用的做演示

Action演示

Action是委托类型当我敲出Action时可以看到Action的图标是一个小提包,这就是委托类型的图标
在这里插入图片描述

我们再按F12转到Action类型定义,可以看到Action是由delegate关键字修饰的
在这里插入图片描述
当我们new一个Action实例的时候可以看到他需要我们传一个返回值为空的方法,所以我们可以将刚刚声明的Calculator类里面的Report方法放进去
在这里插入图片描述

注意我们只放了calculator.Report
Report后面没有跟();
这是因为我们这里只需要calculator.Report这个方法的方法名,所以不用加();
加()的意识就是你想调用这个方法了,那就会报错了
action使用示例
现在我们已经用action这个委托指向了calculator.Report这个方法,上面我和你们说了,对于一个方法,我们可以直接调用,也可以间接调用,我们先看以下直接调用和间接调用

  class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            //先声明一个Calculator类型变量并引用一个Calculator实例
            Calculator calculator = new Calculator();
            //声明一个Action类型变量
            Action action = new Action(calculator.Report);
            //===============================================
            //直接调用Report方法
            calculator.Report();
            //使用委托间接调用Report方法
            action.Invoke();
            //使用委托简便的写法
            action();//这是为了模仿函数指针的写法
        }

    }
    /// <summary>
    /// 声明一个类,类里面包含三个方法,一个返回值为空的方法,两个返回值为int的方法
    /// </summary>
    class Calculator
    {
    
    
        public void Report()
        {
    
    
            Console.WriteLine("我有三个方法");
        }
        public int Add(int a , int b)
        {
    
    
            int result = a + b;
            return result;
        }
        public int Sub(int a,int b)
        {
    
    
            int result = a - b;
            return result;
        }
    }

运行效果
在这里插入图片描述

Func演示

func委托是泛型委托,由下图可以看到func委托拥有17个类型重载,当你按下下箭头按钮时你能分别查看这些重载
根据这些重载我们可以得出,Func泛型委托引用了一个带有一个返回值的方法,它可以传递0或者多到16个参数类型,和一个返回类型.
也就是说它可以没有传递参数,但是一定要有返回类型。
在这里插入图片描述
Func使用方法同上Action所以这里就直接演示

  class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            ///Func委托
            Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);
            Func<int, int, int> func2 = new Func<int, int, int>(calculator.Add);
            //声明一些int变量用于传值或接受值给委托
            int x = 100;
            int y = 200;
            int z = 0;
            z = func1.Invoke(x, y);
            Console.WriteLine(z);
        }

    }
    /// <summary>
    /// 声明一个类,类里面包含三个方法,一个返回值为空的方法,两个返回值为int的方法
    /// </summary>
    class Calculator
    {
    
    
        public void Report()
        {
    
    
            Console.WriteLine("我有三个方法");
        }
        public int Add(int a , int b)
        {
    
    
            int result = a + b;
            return result;
        }
        public int Sub(int a,int b)
        {
    
    
            int result = a - b;
            return result;
        }
    }

结果
在这里插入图片描述

委托的声明

在上面的例子里,介绍了Action,和Func委托,这两个委托是C#类库为我们准备好的委托,下面我将介绍怎么自己声明委托,也就是自定义委托

委托是一种类(class),类是数据类型所以委托也是一种数据类型

类是一种数据类型,而且是引用类型的数据类型用类我们可以声明变量和创建实例,委托既然是一种类,所以委托也能用来声明变量和创建实例,

	//声明一个类类型变量,并接收Action的类型
	Type t = typeof(Action);
	//查看Action类型是否为class
	Console.WriteLine(t.IsClass);

运行结果
在这里插入图片描述
可以看出委托是一种类

它的声明方式与一般的类不同,主要是为了照顾可读性和C/C++传统

//public公开的;表示无论从什么地方,都可以对其访问
// delegate 委托;,告诉编译器,我们现在要声明一个委托
// double 数据类型 ,这个double是目标方法的返回值类型
// Name ; 委托名称
//();这个圆括号1要写上目标方法的参数列表
 public delegate double Calc(double x, double y); //参数列表带两个double参数

注意声明委托的位置,避免写错地方结果声明成嵌套类型

因为我们的委托是一种类所以我们一般将它声明在命名空间里,这样它就和其他的类是平级的了
虽然C#允许在类里面声明嵌套类,也就是说当你不小心将委托声明在一个类里面,编译器能编译过去,运行也不会报错
但是我们声明的这个委托就已经不是一个独立的类了而是我们被嵌套的这个类的嵌套类,在使用嵌套类的时候
我们需要使用被嵌套类.嵌套类的形式来使用嵌套类(如System.Linq),Linq是System的嵌套类
所以为了方便使用,就在命名空间里声明委托类

委托与所封装的方法必须"类型兼容"

图解:
在这里插入图片描述
总结

  1. 返回值的数据类型需一致
  2. 参数列表在个数和数据类型上一致(参数名称不需要一样)

实例

声明一些实例方法用于委托

class Calculator
    {
    
    
        /// <summary>
        /// 声明一个double类型返回值的加法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Add(double x, double y)
        {
    
    
            return x + y;
        }
        /// <summary>
        /// 声明一个double类型返回值的减法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Sub(double x, double y)
        {
    
    
            return x - y;
        }
        /// <summary>
        /// 声明一个double类型返回值的乘法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Mul(double x, double y)
        {
    
    
            return x * y;
        }
        /// <summary>
        /// 声明一个double类型返回值的除法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Div(double x, double y)
        {
    
    
            return x / y;
        }
    }

然后声明一个和上面方法类型兼容的委托类

 public delegate double Calc(double x, double y);

声明上面方法类的实例

	//声明一个Calculator的变量和实例,有了实例才可以访问这个实例方法
	Calculator cal = new Calculator();

接下来声明委托的变量和实例,在声明实例的时候我们可以看到委托的初始化器要求我们传一个返回值为double类型的方法,并且该方法拥有两个double类型的传值参数,
在这里插入图片描述
这是因为我们在前面声明这个类的时候定义的就是一个double类型的返回值和两个double的传值参数如下

public delegate double Calc(double x, double y);

然后我们从刚刚声明的Calculator类里面找符合Calc委托类型兼容的方法,并将方法给到Calc的实例
(很明显,全部符合要求)所以我们赋值第一个
这样就创建好了Calc 委托的变量并引用好了该委托的实例了

 Calc calc1 = new Calc(cal.Add);

然后再创建三个Calc 委托和变量,并通过声明好的委托间接调用那四个方法,并打印出来方法所返回的值

	//下面我们来创建委托的变量和实例
	Calc calc1 = new Calc(cal.Add);
	Calc calc2 = new Calc(cal.Sub);
	Calc calc3 = new Calc(cal.Mul);
	Calc calc4 = new Calc(cal.Div);
	//声明三个double类型变量用与传值和接受方法所返回的值
	double a = 100;
	double b = 200;
	double c = 0;
	
	c = calc1.Invoke(a,b);
	Console.WriteLine("Add方法通过方法间接调用返回的值"+c);
	c = calc2.Invoke(a,b);
	Console.WriteLine("Sub方法通过方法间接调用返回的值" + c);
	c = calc3.Invoke(a,b);
	Console.WriteLine("Mul方法通过方法间接调用返回的值" + c);
	c = calc4.Invoke(a,b);
	Console.WriteLine("Div方法通过方法间接调用返回的值" + c);

代码执行结果,通过委托间接调用成功
在这里插入图片描述

完整代码:

 public delegate double Calc(double x, double y);
         
    class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            //声明一个Calculator的变量和实例,有了实例才可以访问这个实例方法
            Calculator cal = new Calculator();
            //下面我们来创建委托的变量和实例
            Calc calc1 = new Calc(cal.Add);
            Calc calc2 = new Calc(cal.Sub);
            Calc calc3 = new Calc(cal.Mul);
            Calc calc4 = new Calc(cal.Div);
            //声明三个double类型变量用与传值和接受方法所返回的值
            double a = 100;
            double b = 200;
            double c = 0;

            c = calc1.Invoke(a,b);
            Console.WriteLine("Add方法通过方法间接调用返回的值"+c);
            c = calc2.Invoke(a,b);
            Console.WriteLine("Sub方法通过方法间接调用返回的值" + c);
            c = calc3.Invoke(a,b);
            Console.WriteLine("Mul方法通过方法间接调用返回的值" + c);
            c = calc4.Invoke(a,b);
            Console.WriteLine("Div方法通过方法间接调用返回的值" + c);
        }

    }
    class Calculator
    {
    
    
        /// <summary>
        /// 声明一个double类型返回值的加法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Add(double x, double y)
        {
    
    
            return x + y;
        }
        /// <summary>
        /// 声明一个double类型返回值的减法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Sub(double x, double y)
        {
    
    
            return x - y;
        }
        /// <summary>
        /// 声明一个double类型返回值的乘法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Mul(double x, double y)
        {
    
    
            return x * y;
        }
        /// <summary>
        /// 声明一个double类型返回值的除法
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public double Div(double x, double y)
        {
    
    
            return x / y;
        }
    }

委托的常规使用

在日常工作当中使用委托的时候,一般都是把委托当中方法的参数传到方法里面去,这样做的好处是,你写了一个方法,这个方法有一个委托类型的参数,我们知道委托封装了一个方法,然后我们可以在方法体里使用传进来的这个委托,间接的去调用委托封装的那个方法,这样就形成了一种动态条用方法的代码结构
像这种把委托当做参数传进方法,具体分为两种,一种模板方法,一种回调方法

把方法当作参数传给另一个方法

模板方法,"借用"指定的外部方法来生产结果

模板方法指的是,你写了一个方法,然后通过传进来的委托参数,借用指定的外部方法,来产生一个结果,这就相当于在你写的方法有一个填空题,这个填空题的空白之处就用你传进来的委托类型的参数进行填补,也就是同传进来的委托类型参数间接的调用指定的外部方法,这个方法一般是具有返回值的,当你拿到这个返回值的时候,你再继续执行你所写的方法后面的逻辑,这也就解释了为什么叫做模板方法,就是你写了一个方法,这个方法它是一个模板,这个模板里有几处是不确定的,其余部分都是已经确定好了的,那么这个不确定的部分,就靠你传进来的委托类型的参数,所包含的方法来进行填补

模板方法实例讲解:

代码部分`

class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            //将产品系列实例化     //实例化才能拿到里面的实例类
            ProductFactory productFactory = new ProductFactory();
            //将包装方法实例化
            WrapFactory wrapFactory = new WrapFactory();
            //声明一个委托类型,然后将产品系列里面的产品封装进去,这样就可以间接调用了
            Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
            Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);
            //声明一个箱子类变量让其包装方法接收上面封装的委托类型
            Box box1 = wrapFactory.WrapProduct(func1);
            Box box2 = wrapFactory.WrapProduct(func2);
            Console.WriteLine(box1.Product.Name);
            Console.WriteLine(box2.Product.Name);
        }

    }
    /// <summary>
    /// 声明一个Product(产品)类,拥有一个Name实例字段用于表示产品的名字
    /// </summary>
    class Product
    {
    
    
        public string Name {
    
     get; set; }
    }
    /// <summary>
    /// 声明一个Box(包装箱)类,里面拥有一个Product实例字段,用于表示包装了那种产品
    /// </summary>
    class Box
    {
    
    
        public Product Product {
    
     get; set; }
    }
    /// <summary>
    /// 声明一个WrapFactory(包装工厂)
    /// 拥有一个WrapProduct实例类用于包装产品(给产品包装上盒子)
    /// </summary>
    class WrapFactory
    {
    
    
        /// <summary>
        /// 包装产品方法
        /// 拥有一个委托类型参数getProduct
        /// </summary>
        /// <param name="getProduct"></param>
        /// <returns></returns>
        public Box WrapProduct(Func<Product> getProduct)
        {
    
    
            //声明一个盒子的包装箱实例
            Box box = new Box();
            //声明一个产品让其产品名等于getProduct委托返回产品名
            Product product = getProduct.Invoke();
            //将产品装进箱子
            box.Product = product;
            //返回箱子
            return box;
        }

    }
    /// <summary>
    /// 声明一个ProductFactory(产品系列)类
    /// 用于给上面的getProduct传值
    /// </summary>
    class ProductFactory
    {
    
    
        /// <summary>
        /// 声明一个名为Pizza的产品
        /// </summary>
        /// <returns></returns>
        public Product MakePizza()
        {
    
    
            Product product = new Product
            {
    
    
                Name = "Pizza"
            };
            return product;
        }
        /// <summary>
        /// 声明一个名为ToyCar的产品
        /// </summary>
        /// <returns></returns>
        public Product MakeToyCar()
        {
    
    
            Product product = new Product();
            product.Name = "ToyCar";
            return product;
        }
    }

运行效果 输出Pizza ToyCar

在这里插入图片描述
直接这样看是很难看出什么效果的,我讲解以下逻辑顺序
首先是上面例子中的模板方法,如下,我们可以很轻易的看出,它的大部分逻辑都是固定的

public Box WrapProduct(Func<Product> getProduct)
   {
    
    
        //声明一个盒子的包装箱实例
        Box box = new Box();
        //声明一个产品让其产品名等于getProduct委托返回产品名
        Product product = getProduct.Invoke();
        //将产品装进箱子
        box.Product = product;
        //返回箱子
        return box;
    }

首先,它准备了一个Box 包装箱实例
然后,让通过委托传进来委托参数,间接调用一个方法,该方法返回一个产品
再然后Box包装箱将刚刚获取到的产品装进箱子
然后将装好产品的箱子返回
可以动态修改的地方是

 Product product = getProduct.Invoke();

也就是那个委托,它封装了一个方法
它里面封装了什么样的方法,我们在这个地方就能获取到什么样的产品
这就是使用模板方法的好处;
一旦我将代码写成这样之后,我的Product类Box类和WrapProduct就都不再需要进行修改,我只需要不停的去扩展我的ProductFactory类(产品工厂),我就能不停的生产不同的产品,只要我将生产其他产品的方法,封装在委托里面,传给我的模板方法,他就一定会给我的产品装上一个箱子,并且返回回来.
这就最大限度的实现了我的代码重复使用

代码复用Reuse
Reuse,重复使用,也叫"服用".代码的复用不但可以提高工作效率,还可以减少Bug的引入.良好的复用结构是所有优秀软件追求的共同目标之一

模板方法总结

  • 相当于填空题
  • 常位于代码中部
  • 委托有返回值

回调方法(callback)方法,调用指定的外部方法

回调函数就是一个通过函数指针
调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调方法 是 任何一个 被 以该回调方法为其第一个参数 的 其它方法 调用 的方法。很多时候,回调是一个当某些事件发生时被调用的方法。
-----百度百科

回调方法的实例讲解

从上面的百科可以看出,回调方法是通过委托类型的参数传进主调用方法的被调用方法,主调用方法可以根据逻辑选择是否调用这个方法,下面我们通过例子来看看上面是回调方法
因为前面已经写了一个模板方法了,所以我现在就直接在前面的例子中增加回调方法

增加一个Logger类用于记录程序的运行状态,并给其一个方法,当程序出现状况的时候调用这个方法

 class Logger
    {
    
    
        public void Log(Product product)
        {
    
    
            Console.WriteLine("Product'{0}' created at {1}.price is {2}", product.Name, DateTime.UtcNow, product.Price);
        }
    }

并给模板方法新曾一个Action委托参数并写出什么情况下使用Action

public Box WrapProduct(Func<Product> getProduct,Action<Product>logCallBack)
        {
    
    
            //声明一个盒子的包装箱实例
            Box box = new Box();
            //声明一个产品让其产品名等于getProduct委托返回产品名
            Product product = getProduct.Invoke();
            ///当产品的价格大于等于50的时候调用logCallBack委托
            if (product.Price>=50)
            {
    
    
                logCallBack(product);
            }
            //将产品装进箱子
            box.Product = product;
            //返回箱子
            return box;
        }

然后我们去修改产品类和产品工厂的产品为其新增价格

 class Product
    {
    
    
        public string Name {
    
     get; set; }
        //给产品类增加价格
        public int Price {
    
     get; set; }
    }
class ProductFactory
	 {
    
    
	     public Product MakePizza()
	     {
    
    
	         Product product = new Product
	         {
    
    
	             Name = "Pizza",
	             Price = 20
	         };
	         return product;
	     }
	     public Product MakeToyCar()
	     {
    
    
	         Product product = new Product();
	         product.Name = "ToyCar";
		     product.Price = 100;
		     return product;
	     }
	 }

这个时候你会发现,前面写好的模板方法报错了
在这里插入图片描述

这是因为我们前面修改了模板方法的传值参数列表,它现在还需要再接受一个Action委托参数,如下图,现在,他就不报错了
在这里插入图片描述
运行效果
在这里插入图片描述

完整代码

class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            //将产品系列实例化     //实例化才能拿到里面的实例类
            ProductFactory productFactory = new ProductFactory();
            //将包装方法实例化
            WrapFactory wrapFactory = new WrapFactory();
            //声明一个委托类型,然后将产品系列里面的产品封装进去,这样就可以间接调用了
            Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
            Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);
            //将Logger实例化
            Logger logger = new Logger();
            Action<Product> log = new Action<Product>(logger.Log);
            //声明一个箱子类变量让其包装方法接收上面封装的委托类型
            Box box1 = wrapFactory.WrapProduct(func1, log);
            Box box2 = wrapFactory.WrapProduct(func2, log);
            Console.WriteLine(box1.Product.Name);
            Console.WriteLine(box2.Product.Name);
        }

    }
    class Logger
    {
    
    
        public void Log(Product product)
        {
    
    
            Console.WriteLine("Product'{0}' created at {1}.price is {2}", product.Name, DateTime.UtcNow, product.Price);
        }
    }
    /// <summary>
    /// 声明一个Product(产品)类,拥有一个Name实例字段用于表示产品的名字
    /// </summary>
    class Product
    {
    
    
        public string Name {
    
     get; set; }
        public int Price {
    
     get; set; }
    }
    /// <summary>
    /// 声明一个Box(包装箱)类,里面拥有一个Product实例字段,用于表示包装了那种产品
    /// </summary>
    class Box
    {
    
    
        public Product Product {
    
     get; set; }
    }
    /// <summary>
    /// 声明一个WrapFactory(包装工厂)
    /// 拥有一个WrapProduct实例类用于包装产品(给产品包装上盒子)
    /// </summary>
    class WrapFactory
    {
    
    
        /// <summary>
        /// 包装产品方法
        /// 拥有一个委托类型参数getProduct
        /// </summary>
        /// <param name="getProduct"></param>
        /// <returns></returns>
        public Box WrapProduct(Func<Product> getProduct,Action<Product>logCallBack)
        {
    
    
            //声明一个盒子的包装箱实例
            Box box = new Box();
            //声明一个产品让其产品名等于getProduct委托返回产品名
            Product product = getProduct.Invoke();
            if (product.Price>=50)
            {
    
    
                logCallBack(product);
            }
            //将产品装进箱子
            box.Product = product;
            //返回箱子
            return box;
        }

    }
    /// <summary>
    /// 声明一个ProductFactory(产品系列)类
    /// 用于给上面的getProduct传值
    /// </summary>
    class ProductFactory
    {
    
    
        /// <summary>
        /// 声明一个名为Pizza的产品
        /// </summary>
        /// <returns></returns>
        public Product MakePizza()
        {
    
    
            Product product = new Product
            {
    
    
                Name = "Pizza",
                Price = 20
            };
            return product;
        }
        /// <summary>
        /// 声明一个名为ToyCar的产品
        /// </summary>
        /// <returns></returns>
        public Product MakeToyCar()
        {
    
    
            Product product = new Product();
            product.Name = "ToyCar";
            product.Price = 100;
            return product;
        }
    }

代码逻辑:
在回调方法WrapProduct()里面新增的if 判断语句,当条件成立的时候会去通过logCallBack委托去调用外部封装的Log方法
那为什么Pizza没有被打印呢?

	if(product.Price>=50) ///因为Pizza的价格没有大于等于50
	{
    
    
	    logCallBack(product);
	}

回调总结

  • 相当于流水线
  • 常位于代码末尾
  • 委托无返回值

模板/回调方法总结

无论是模板方法还是回调方法,他们的本质是一样的,都是用委托类型的参数,封装一个外部的方法,让后把这方法,传进我们方法的内部,再间接进行调用,这就是日常工作当中,我们对委托的常规用法

注意:

难精通+易使用+功能强大的东西,一旦被滥用则后果非常严重

缺点

  1. 这是一种方法级别的紧耦合,现实工作中要慎之又慎
  2. 使可读性下降,debug的难度增加
  3. 把委托回调,异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护
  4. 委托使用不当有可能造成内存泄漏和程序性能下降

委托的高级使用

在C#中的委托关键字是 Delegate,委托类似于C/C++中函数的指针。是存有对某个方法引用的引用类型变量,可在运行时被改变。一般用于实现事件和回调方法。

单/多播委托

一个方法赋值给委托变量,这种叫单播委托。但是在大部分情况下我们需要将多个方法赋值给委托,这是我们就用到了多播委托。要把多个方法赋值给委托变量,我们需要用到 +和 += 示例如下

单播委托实例

先上代码

 class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            Action action1 = new Action(stu1.DoHomework);
            Action action2 = new Action(stu2.DoHomework);
            Action action3 = new Action(stu3.DoHomework);
            action1.Invoke();
            action2.Invoke();
            action3.Invoke(); 
        }

    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(500);

            }
        }
    }

效果
在这里插入图片描述

多播委托

多播委托指的是一个委托内部,封装的不止一个方法
委托对象的一个有用属性在于可通过使用 + 运算符将多个对象分配到一个委托实例。
多播委托包含已分配委托列表。 此多播委托被调用时会依次调用列表中的委托。
仅可合并类型相同的委托。

多播委托实例

代码部分

 class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            Action action1 = new Action(stu1.DoHomework);
            Action action2 = new Action(stu2.DoHomework);
            Action action3 = new Action(stu3.DoHomework);
            //action1.Invoke();
            //action2.Invoke();
            //action3.Invoke();
            action1 += action2;
            action1 += action3;
            action1.Invoke();
        }

    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(500);

            }
        }
    }

效果图
在这里插入图片描述

从上面代码可以看出,我们将action2和action3通过+=号赋值给了action1,于是action1封装了三个方法,成为了多播委托,我们调用action1这个多播委托的时候,实现效果和分别调用action1,action2,action3没有什么不同,而且多播委托执行的顺序,是按照封装方法的先后顺序来执行的
我们调用action1的时候,间接调用的是stu1.DoHomework,
stu1.DoHomework执行完了之后是stu2.DoHomework然后是stu3.DoHomework,这样输出的颜色就是red,green,yellow了

隐式异步调用

隐式异步调用是多播委托高级使用方式,什么是隐式异步调用呢?,

同步与异步的简介

我们先了解什么是异步调用,异步调用与同步调用是相对的,而同步和异步在中英文当中呢有一些语义差别

  • 同步 :你作完了我(在你基础上)接着做
    同步调用图解(红色的是主线程,其他颜色是方法)
    在这里插入图片描述

  • 异步 :咱们两个同时做(相当于汉语当中的同步进行)
    异步调用图解(红色是主线程,其他颜色是方法)
    在这里插入图片描述

同步调用与异步调用的对比

每一个程序运行起来都是内存当中的一个进程(process),每一个进程包含一个或多个线程,那么当程序启动的时候,会形成一个进程,这个进程里面一定会有第一个运行起来的线程,这个线程就被称为主线程,一个进程除了主进程,还可以拥有其他的多个线程,主线程之外的线程叫做分支线程

了解了上面的概念之后呢.我们来看一下方法的调用
当我们在同一个线程内去调用方法的时候,方法的执行是前一个执行完了,后一个才能得到执行,这种在同一个线程内 ,依次执行的方法调用,叫做同步调用.如下,我们在主线程内调用了三个方法,
主线程先开始执行,然后调用第一个方法,第一个方法执行的时候,CPU的执行指针就进入第一个方法里,而主线程就暂停了,直到第一个方法执行完了之后,执行指针再返回到主线程内,主线程继续执行,执行到第二个方法之后呢,再跳到第二个方法里,这样依次执行下去,直到程序的结束
在这里插入图片描述

异步调用呢,指的是在不同的线程中去调用方法,每一个线程和另一个线程都不相干,各自执行,一个支线程的开始和结束,并不会影响到主线程的运行,而且各个支线程的开始和结束的时机又有不同的组合,(如下图) 这就是我们的多线程异步调用,可以看出,异步调用的底层机理就是多线程
异步调用图示一
在这里插入图片描述

异步调用图示二
在这里插入图片描述

总结

  1. 每一个运行的程序都是一个进程
  2. 每一个进程包含一个或多个线程
  3. 同步调用是在同一线程内
  4. 异步调用的底层原理是多线程
  5. 串行== 同步 ==单线程 , 并行 == 异步 ==多线程

同步调用与异步调用的实例

先看看同步调用

同步调用实例讲解

同步调用有三种形式

  1. 直接同步调用
  class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            ///同步直接调用1
            ///直接调用实例化的三个方法
            ///主线程会在调用这三个方法的时候暂停
            stu1.DoHomework();
            stu2.DoHomework();
            stu3.DoHomework();
            //当主线程调用完了那三个方法之后,再执行下面的代码
            for (int i = 0; i < 10; i++)
            {
    
    
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("德玛西亚的英雄{0}.",i);
                Thread.Sleep(500);
            }
        }

    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(500);

            }
        }
    }

效果图
在这里插入图片描述

  1. 使用委托进行间接的同步调用()
    *注意间接调用和异步调用没有直接的关系,间接调用可以间接异步调用也可以间接同步调用,这里实例是间接同步调用
 class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            //这下面使用的是单播委托的间接同步调用
            Action action1 = new Action(stu1.DoHomework);
            Action action2 = new Action(stu2.DoHomework);
            Action action3 = new Action(stu3.DoHomework);
            action1.Invoke();
            action2.Invoke();
            action3.Invoke();
            //当主线程执行完了上面的方法调用后,再执行下面的代码
            for (int i = 0; i < 10; i++)
            {
    
    
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("德玛西亚的英雄们{0}.", i);
            }
        }

    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(500);

            }
        }
    }

效果图
在这里插入图片描述

  1. 使用多播委托的间接同步调用
class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            //这下面使用的是单播委托的间接同步调用
            Action action1 = new Action(stu1.DoHomework);
            Action action2 = new Action(stu2.DoHomework);
            Action action3 = new Action(stu3.DoHomework);
            action1 += action2;
            action1 += action3;
            //注意,上面的多播委托也是同步调用的
            //当主线程执行完了上面的方法调用后,再执行下面的代码
            for (int i = 0; i < 10; i++)
            {
    
    
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("德玛西亚的英雄们{0}.", i);
            }
        }
    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(500);

            }
        }
    }

效果图
在这里插入图片描述
上面演示了三种同步调用的方式分别是

  1. 直接同步调用
  2. 单播委托的间接同步调用
  3. 多播委托的间接同步调用
    接下来看看委托进行隐式异步调用
委托隐式异步调用实例讲解

前面的例子已经展示了多播委托的同步调用,这里我将展示委托的隐式异步调用,
单播委托的隐式异步调用
前面我已经反复的演示过了,使用Invoke()方法是同步调用
而委托还有另外一种方法BeginInvoke(),使用这个方法进行间接调用,就是隐式异步调用
因为这个BeginInvoke()会为我们生成分支线程,然后在分支线程里面,调用他封装的方法

  class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            
            Action action1 = new Action(stu1.DoHomework);
            Action action2 = new Action(stu2.DoHomework);
            Action action3 = new Action(stu3.DoHomework);

            
            //BeginInvoke()需要两个参数,
            //第一个参数AsyncCallback异步调用的回调 指的是引用在相应异步操作完成时调用的方法.
            //第二个参数object
            action1.BeginInvoke(null, null);
            action2.BeginInvoke(null, null);
            action3.BeginInvoke(null, null);

            //主线程会直接执行这里的代码,上面的由隐式声明的分支线程执行
            for (int i = 0; i < 10; i++)
            {
    
    
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("德玛西亚的英雄们{0}.", i);
                Thread.Sleep(1000);
            }
            Console.ReadLine();
        }

    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(1000);

            }
        }
    }

执行效果
在这里插入图片描述

可以发现,现在,主线程和分支线程,他们并行执行,谁也不用等着谁,这就是典型的异步调用,
仔细观察能发现,这些打印出来的字符串并没有规律,
这是因为这四个线程都在访问我们控制台的前景色这个属性,
多个线程在访问同一个线程资源的时候,就有可能在争抢的时候发生冲突,上面的场景就是发生冲突了,这个时候就需要为线程加锁

显示异步调用

上面讲解的是由编译器隐式的声明并使用多线程,现在我们看看显式的声明并使用多线程
显示声明有两种方式 还是使用上面的实例类

  1. 第一种比较古老 是使用Thread
 class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            //使用Thread声明三个支线程

            Thread thread1 = new Thread(new ThreadStart(stu1.DoHomework));
            Thread thread2 = new Thread(new ThreadStart(stu2.DoHomework));
            Thread thread3 = new Thread(new ThreadStart(stu3.DoHomework));

            //让刚刚声明的三个线程启动
            thread1.Start();
            thread2.Start();
            thread3.Start();

            //主线程会直接执行这里的代码,上面的由隐式声明的分支线程执行
            for (int i = 0; i < 10; i++)
            {
    
    
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("德玛西亚的英雄们{0}.", i);
                Thread.Sleep(1000);
            }
            Console.ReadLine();
        }

    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(1000);

            }
        }
    }

执行效果图

在这里插入图片描述

这就是使用Thread 进行的显示异步调用除了使用Thread 进行显示的异步调用C#类库还为我准备了更高级的方式就是Task
2. 第二种显示声明Task进行异步调用

  class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Student stu1 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Red };
            Student stu2 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Green };
            Student stu3 = new Student() {
    
     ID = 1, PenColor = ConsoleColor.Yellow };

            //使用Task声明三个支线程
            Task task1 = new Task(new Action(stu1.DoHomework));
            Task task2 = new Task(new Action(stu2.DoHomework));
            Task task3 = new Task(new Action(stu3.DoHomework));


            //让刚刚声明的三个线程启动
            task1.Start();
            task2.Start();
            task3.Start();

            //主线程会直接执行这里的代码,上面的由隐式声明的分支线程执行
            for (int i = 0; i < 10; i++)
            {
    
    
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("德玛西亚的英雄们{0}.", i);
                Thread.Sleep(1000);
            }
            Console.ReadLine();
        }

    }
    class Student
    {
    
    
        public int ID {
    
     get; set; }
        public ConsoleColor PenColor {
    
     get; set; }
        //ConsoleColor控制台输出时显示的颜色
        public void DoHomework()
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Console.ForegroundColor = this.PenColor;
                Console.WriteLine("Student{0} doing homework{1} hour(s).", this.ID, i);
                // 你在那个线程当中调用了Thread.Sleep你就暂停500毫秒
                Thread.Sleep(1000);

            }
        }
    }

代码运行效果图
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44368963/article/details/107959260