【C#】委托的简单理解与使用

一、C#委托是什么?

1、在C#的委托之前,先了解一下C/C++的函数指针。

#include <stdio.h>

void (*funcP1)(void);
void PrintMessage()
{
    printf("方法被调用\n");
}

void (*funcP2)(int, int);
void PrintSum(int a, int b)
{
    printf("a+b=%d\n",a+b);
}

int main()
{

    funcP1 = PrintMessage;
    funcP2 = PrintSum;

    funcP1();
    funcP2(1,2);

    PrintMessage();
    PrintSum(1, 2);
}

Out:

方法被调用

a+b=3

 

2、一切皆地址

变量是以某个地址为起点的一段内存中所存储的值

函数是以某个地址为起点的一段内存中所存储的一组机器语言指令

 

① C/C++函数指针的汇编源码示例

从汇编代码来看C/C++函数指针的本质(内存中的一段机器指令):

以funP1和PrintMessage为例讲解函数指针的汇编代码在内存层面都做了什么

20:     funcP1 = PrintMessage;
00007FF67CDC190B  lea    rax,[PrintMessage (07FF67CDC131Bh)]  //这行代码使用 lea 指令将 PrintMessage 函数的地址加载到寄存器 rax 中。
00007FF67CDC1912  mov    qword ptr [funcP1 (07FF67CDCC710h)],rax  //这行代码将 rax 中存储的 PrintMessage 函数的地址,存储到函数指针 funcP1 所指向的内存地址中。这样,funcP1 现在指向了 PrintMessage 函数。

24:     funcP1();
00007FF67CDC1927  call   qword ptr [funcP1 (07FF67CDCC710h)]  //这行代码通过函数指针 funcP1 调用了 PrintMessage 函数。实际上,这行代码会跳转到 PrintMessage 函数的地址,然后执行 PrintMessage 函数中的代码。

27:     PrintMessage();
00007FF67CDC193D  call   PrintMessage (07FF67CDC131Bh) //通过函数名直接调用函数则一样会跳转到 PrintMessage 函数的地址

综合起来,整个过程的目标是将函数指针 funcP1初始化为指向PrintMessage函数,并通过函数指针调用PrintMessage函数。

 

② C#委托类型的简单源码与汇编代码

internal class Program
{
    static void Main(string[] args)
    {
        CalculateSum cal = GetIntSum;
        cal.Invoke(1,2);

    }

    delegate void CalculateSum(int num1,int num2);
    public static void GetIntSum(int num1, int num2)
    {
        Console.WriteLine(num1+num2);
    }
}

C#委托的汇编源码

(因为C#的委托涉及到的知识较多,笔者修为还不够在此就不强行解释C#的汇编代码了)

 

③ C#委托的多播(多播委托)

 internal class Program
    {
        static void Main(string[] args)
        {
            CalculateSum cal = Function1;
            cal += Function2;
            cal += Function3;
        }

        delegate void CalculateSum();
        public static void Function1() { }
        public static void Function2() { }
        public static void Function3() { }
    }

C#委托与函数指针不同点之一就是:C#的委托可以注册多个方法,并且委托被调用时,函数按照注册顺序从前到后以此执行。

 

3、C#委托的简单总结

① 委托其实就是函数指针的“升级版”

从C/C++函数指针与C#委托的汇编源码对比来看,其实委托就是函数指针的“升级版”。

它们的核心逻辑就是记录函数方法的首地址,在委托调用时直接将执行位置跳转到该函数首地址。

 

② 委托的数据结构

如果你之前有了解过数据结构的链表,从多播委托不难看出委托的数据结构可以说是一根单链表,而每个节点都是注册的函数的首地址。

 

二、C#委托的使用

1、对委托的误解

肯定有不少C#初学者(包括我)在学到委托部分时,在看到示例代码时肯定一头雾水:“这跟直接调用函数有什么区别?”“搞这么麻烦,为什么不直接调用函数?”。

从汇编代码层面来看,委托确实起到了“函数的包装器”的作用,因为在调用委托和直接调用函数的时候,都是将执行位置跳转到函数的首地址。

但在经过一段时间项目(屎山)实战沉淀,我总结一句话:“存在即合理”。之所以会冒出“委托无用论”的想法,是因为接触的项目不够多以及项目体量不大,与委托有紧密联系的“事件”也是一样的(事件是委托的包装器),且这一块大多就已经涉及到设计模式相关知识了。

 

2、委托的广泛使用场景

C#委托常用于事件处理,回调函数,多播委托等,因为委托能将函数作为参数传递,从而提高了项目代码的灵活性和复用性。

 

3、委托的声明

① 自定义模版

delegate

return type:指定返回类型

delegate-name:该委托类的名称

parameter list:参数列表

  delegate void CalculateSum<T>(T num1, T num2);
  public static void Function1(int a, int b) { Console.WriteLine(a+b); }
  static void Main()
  {
      CalculateSum<int> cal = Function1;
  }

 

② 微软官方提供的委托模版,Action与Func

public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

Action返回值为void ,通过重载16次Action函数(最朴素的一集),允许最多16个形式参数。

微软官方文档链接:Action 委托 (System)

public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

Func必须要有返回值,最后一个参数TResult为返回值类型,同样允许最多16个形参和1个返回类型

微软官方文档链接:Func 委托 (System)

PS:委托在注册方法时,方法的参数列表和委托的参数列表必须是一 一对应的,隐式类型转换也不行。

 

4、委托的一般使用

将外部方法由委托包装后参数化,传给另一个方法内部来决定是否调用(间接的)。

① 正确使用1:模版方法,“借用”指定的外部方法来产生结果

·相当于选词(方法)填空

·常位于代码中部

·委托有返回值

利用委托类型能够在程序运行时动态的增删注册的函数,以此来提高代码的灵活性

using System;

namespace Delegate_Event_Learn
{
    internal class Program
    {

        static void Main(string[] args)
        {
            ProductFactory productFactory = new ProductFactory();
            WarpFactory warpFactory = new WarpFactory();

            Func<Product> func1 = new Func<Product>(productFactory.MakePiza);
            Func<Product> func2 = new Func<Product>(productFactory.MakeCake);

            Box box1 = warpFactory.WarpProductInBox(func1);
            Box box2 = warpFactory.WarpProductInBox(func2);

            Console.WriteLine(box1.Product.Name);
            Console.WriteLine(box2.Product.Name);
        }

    }

    public class Box : BigBox
    {
        public Product Product { get; set; }
    }

    public class BigBox
    { }
    public class Product
    {
        public string Name { get; set; }
    }

    public class ProductFactory
    {
        public Product MakePiza()
        {
            Product product = new Product();
            product.Name = "Piza";
            return product;
        }
        public Product MakeCake()
        {
            Product product = new Product();
            product.Name = "Cake";
            return product;
        }
    }

    public class WarpFactory
    {
        public Box WarpProductInBox(Func<Product> GetProduct)
        {
            Box box = new Box();
            Product product = GetProduct.Invoke();
            box.Product = product;
            return box;
        }
    }
}

 

② 正确使用2:回调方法(callback),调用制定的外部方法

·相当于“流水线”

·常位于代码末尾

·委托无返回值

using System;

namespace Delegate_Event_Learn
{
    internal class Program
    {
        static void Main(string[] args)
        {
            ProductFactory productFactory = new ProductFactory();
            WarpFactory warpFactory = new WarpFactory();

            Func<Product> pizaProductionLine = new Func<Product>(productFactory.MakePiza);
            Func<Product> cakeProductionLine = new Func<Product>(productFactory.MakeCake);

            Logger logger = new Logger();
            Action<Product> log = new Action<Product>(logger.LogProductionInfo);

            Box box1 = warpFactory.WarpProductInBox(pizaProductionLine, log);
            Box box2 = warpFactory.WarpProductInBox(cakeProductionLine, log);

            Console.WriteLine(box1.Product.Name);
            Console.WriteLine(box2.Product.Name);
        }

    }

    //回调方法
    public class Logger
    {
        public void LogProductionInfo(Product product)
        {
            Console.WriteLine($"产品{product.Name}于{DateTime.UtcNow}生产,价格是{product.Price}");
        }
    }

    public class Box : BigBox
    {
        public Product Product { get; set; }
    }

    public class BigBox
    { }
    public class Product
    {
        public string Name { get; set; }
        public int Price { get; set; }
    }

    public class ProductFactory
    {
        public Product MakePiza()
        {
            Product product = new Product();
            product.Name = "Piza";
            product.Price = 20;
            return product;
        }
        public Product MakeCake()
        {
            Product product = new Product();
            product.Name = "Cake";
            product.Price = 10;
            return product;
        }
    }

    public class WarpFactory
    {
        public Box WarpProductInBox(Func<Product> GetProduct,Action<Product> logCallback)
        {
            Box box = new Box();
            Product product = GetProduct.Invoke();

            //根据价格判断是否执行回调方法
            if(product.Price>15) logCallback.Invoke(product);

            box.Product = product;
            return box;
        }
    }
}

 

5、使用委托时需要注意的

委托是一种难精通、易使用、功能强大的东西、一旦被滥用则后果非常严重

·缺点1:委托是一种方法级别的“紧耦合”,实际项目中对委托的使用应该慎之又慎

·缺点2:使可读性降低,debug难度增加

·缺点3:将委托回调、异步调用、多线程纠缠在一起时,会让代码难以阅读和维护

·缺点4:委托使用不当有可能造成内存泄漏和降低程序性能。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇