什么是委托 Delegate

  • 委托(delegate)是函数指针的“升级版”
    • 示例:C/C++ 中的函数指针
  • 一切皆地址
    • 变量(数据)是以某个地址为起点的一段内存中所存储的值
    • 函数(算法)是以某个地址为起点的一段内存中所存储的一组机器语言指令
  • 直接调用与间接调用
    • 直接调用:通过函数名来调用函数,CPU 通过函数名直接获得函数所在地址并开始执行 -> 返回
    • 间接调用:通过函数指针来调用函数,CPU 通过读取函数指针存储的值获得函数所在地址并开始执行 -> 返回
  • Java 中没有与委托相对应的功能实体
  • 委托的简单使用
    • Action 委托
    • Func 委托

C 语言函数指针

声明函数指针与函数:
019 委托 - 图1

使用函数指针:
019 委托 - 图2

Java

Java 语言由 C++ 发展而来,为了提高应用安全性,Java 语言禁止程序员直接访问内存地址。即 Java 语言把 C++ 中所有与指针相关的内容都舍弃掉了。

委托实例 Action 与 Func

Action 和 Func 是 C# 内置的委托实例,它们都有很多重载以方便使用。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var calculator = new Calculator();
  6. // Action 用于无形参无返回值的方法。
  7. Action action = new Action(calculator.Report);
  8. calculator.Report();
  9. action.Invoke();
  10. // 模仿函数指针的简略写法。
  11. action();
  12. Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);
  13. Func<int, int, int> func2 = new Func<int, int, int>(calculator.Sub);
  14. int x = 100;
  15. int y = 200;
  16. int z = 0;
  17. z = func1.Invoke(x, y);
  18. Console.WriteLine(z);
  19. z = func2.Invoke(x, y);
  20. Console.WriteLine(z);
  21. // Func 也有简略写法。
  22. z = func1(x, y);
  23. Console.WriteLine(z);
  24. z = func2(x, y);
  25. Console.WriteLine(z);
  26. }
  27. }
  28. class Calculator
  29. {
  30. public void Report()
  31. {
  32. Console.WriteLine("I have 3 methods.");
  33. }
  34. public int Add(int a, int b)
  35. {
  36. return a + b;
  37. }
  38. public int Sub(int a, int b)
  39. {
  40. return a - b;
  41. }
  42. }

委托的声明

019 委托 - 图3

委托是一种类:

  1. static void Main(string[] args)
  2. {
  3. Type t = typeof(Action);
  4. Console.WriteLine(t.IsClass);
  5. }

019 委托 - 图4

委托是类,所以声明位置是和 class 处于同一个级别。但 C# 允许嵌套声明类(一个类里面可以声明另一个类),所以有时也会有 delegate 在 class 内部声明的情况。

实例:

  1. public delegate double Calc(double x, double y);
  2. class Program
  3. {
  4. static void Main(string[] args)
  5. {
  6. var calculator = new Calculator();
  7. var calc1 = new Calc(calculator.Mul);
  8. Console.WriteLine(calc1(5, 6));
  9. }
  10. }
  11. class Calculator
  12. {
  13. public double Mul(double x, double y)
  14. {
  15. return x * y;
  16. }
  17. public double Div(double x, double y)
  18. {
  19. return x / y;
  20. }
  21. }

委托的一般使用

019 委托 - 图5

模板方法

利用模板方法,提高代码复用性。
下例中 Product、Box、WrapFactory 都不用修改,只需要在 ProductFactory 里面新增不同的 MakeXXX 然后作为委托传入 WrapProduct 就可以对其进行包装。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var productFactory = new ProductFactory();
  6. Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
  7. Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);
  8. var wrapFactory = new WrapFactory();
  9. Box box1 = wrapFactory.WrapProduct(func1);
  10. Box box2 = wrapFactory.WrapProduct(func2);
  11. Console.WriteLine(box1.Product.Name);
  12. Console.WriteLine(box2.Product.Name);
  13. }
  14. }
  15. class Product
  16. {
  17. public string Name { get; set; }
  18. }
  19. class Box
  20. {
  21. public Product Product { get; set; }
  22. }
  23. class WrapFactory
  24. {
  25. // 模板方法,提高复用性
  26. public Box WrapProduct(Func<Product> getProduct)
  27. {
  28. var box = new Box();
  29. Product product = getProduct.Invoke();
  30. box.Product = product;
  31. return box;
  32. }
  33. }
  34. class ProductFactory
  35. {
  36. public Product MakePizza()
  37. {
  38. var product = new Product();
  39. product.Name = "Pizza";
  40. return product;
  41. }
  42. public Product MakeToyCar()
  43. {
  44. var product = new Product();
  45. product.Name = "Toy Car";
  46. return product;
  47. }
  48. }

Reuse,重复使用,也叫“复用”。代码的复用不但可以提高工作效率,还可以减少 bug 的引入。

良好的复用结构是所有优秀软件所追求的共同目标之一。

回调方法

回调方法是通过委托类型参数传入主调方法的被调用方法,主调方法根据自己的逻辑决定是否调用这个方法。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var productFactory = new ProductFactory();
  6. // Func 前面是传入参数,最后一个是返回值,所以此处以 Product 为返回值
  7. Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
  8. Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);
  9. var wrapFactory = new WrapFactory();
  10. var logger = new Logger();
  11. // Action 只有传入参数,所以此处以 Product 为参数
  12. Action<Product> log = new Action<Product>(logger.Log);
  13. Box box1 = wrapFactory.WrapProduct(func1, log);
  14. Box box2 = wrapFactory.WrapProduct(func2, log);
  15. Console.WriteLine(box1.Product.Name);
  16. Console.WriteLine(box2.Product.Name);
  17. }
  18. }
  19. class Logger
  20. {
  21. public void Log(Product product)
  22. {
  23. // Now 是带时区的时间,存储到数据库应该用不带时区的时间 UtcNow。
  24. Console.WriteLine("Product '{0}' created at {1}.Price is {2}", product.Name, DateTime.UtcNow, product.Price);
  25. }
  26. }
  27. class Product
  28. {
  29. public string Name { get; set; }
  30. public double Price { get; set; }
  31. }
  32. class Box
  33. {
  34. public Product Product { get; set; }
  35. }
  36. class WrapFactory
  37. {
  38. // 模板方法,提高复用性
  39. public Box WrapProduct(Func<Product> getProduct, Action<Product> logCallBack)
  40. {
  41. var box = new Box();
  42. Product product = getProduct.Invoke();
  43. // 只 log 价格高于 50 的
  44. if (product.Price >= 50)
  45. {
  46. logCallBack(product);
  47. }
  48. box.Product = product;
  49. return box;
  50. }
  51. }
  52. class ProductFactory
  53. {
  54. public Product MakePizza()
  55. {
  56. var product = new Product
  57. {
  58. Name = "Pizza",
  59. Price = 12
  60. };
  61. return product;
  62. }
  63. public Product MakeToyCar()
  64. {
  65. var product = new Product
  66. {
  67. Name = "Toy Car",
  68. Price = 100
  69. };
  70. return product;
  71. }
  72. }

注意委托滥用

恐怖的委托滥用示例,基本上工作在这段代码上的人,三个月内就离职了。

以后技术上去了,记得回看这段代码,警示自己。

019 委托 - 图6

019 委托 - 图7

委托的高级使用

019 委托 - 图8

多播(multicast)委托

多播委托即一个委托内部封装不止一个方法。

  1. using System;
  2. using System.Threading;
  3. namespace DelegateExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. var stu1 = new Student { ID = 1, PenColor = ConsoleColor.Yellow };
  10. var stu2 = new Student { ID = 2, PenColor = ConsoleColor.Green };
  11. var stu3 = new Student { ID = 3, PenColor = ConsoleColor.Red };
  12. var action1 = new Action(stu1.DoHomework);
  13. var action2 = new Action(stu2.DoHomework);
  14. var action3 = new Action(stu3.DoHomework);
  15. // 单播委托
  16. //action1.Invoke();
  17. //action2.Invoke();
  18. //action3.Invoke();
  19. // 多播委托
  20. action1 += action2;
  21. action1 += action3;
  22. action1.Invoke();
  23. }
  24. }
  25. class Student
  26. {
  27. public int ID { get; set; }
  28. public ConsoleColor PenColor { get; set; }
  29. public void DoHomework()
  30. {
  31. for (int i = 0; i < 5; i++)
  32. {
  33. Console.ForegroundColor = PenColor;
  34. Console.WriteLine("Student {0} doing homework {1} hour(s)", ID, i);
  35. Thread.Sleep(1000);
  36. }
  37. }
  38. }
  39. }

隐式异步调用

019 委托 - 图9

异步互不相干:
这里说的“互不相干”指的是逻辑上,而现实工作当中经常会遇到多个线程共享(即同时访问)同一个资源(比如某个变量)的情况,这时候如果处理不当就会产生线程间争夺资源的冲突。

三种同步调用

  1. using System;
  2. using System.Threading;
  3. namespace DelegateExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. var stu1 = new Student { ID = 1, PenColor = ConsoleColor.Yellow };
  10. var stu2 = new Student { ID = 2, PenColor = ConsoleColor.Green };
  11. var stu3 = new Student { ID = 3, PenColor = ConsoleColor.Red };
  12. // 直接同步调用
  13. //stu1.DoHomework();
  14. //stu2.DoHomework();
  15. //stu3.DoHomework();
  16. var action1 = new Action(stu1.DoHomework);
  17. var action2 = new Action(stu2.DoHomework);
  18. var action3 = new Action(stu3.DoHomework);
  19. // 间接同步调用
  20. //action1.Invoke();
  21. //action2.Invoke();
  22. //action3.Invoke();
  23. // 多播委托,同步调用
  24. action1 += action2;
  25. action1 += action3;
  26. action1.Invoke();
  27. // 主线程模拟在做某些事情。
  28. for (var i = 0; i < 10; i++)
  29. {
  30. Console.ForegroundColor=ConsoleColor.Cyan;
  31. Console.WriteLine("Main thread {0}",i);
  32. Thread.Sleep(1000);
  33. }
  34. }
  35. }
  36. class Student
  37. {
  38. public int ID { get; set; }
  39. public ConsoleColor PenColor { get; set; }
  40. public void DoHomework()
  41. {
  42. for (int i = 0; i < 5; i++)
  43. {
  44. Console.ForegroundColor = PenColor;
  45. Console.WriteLine("Student {0} doing homework {1} hour(s)", ID, i);
  46. Thread.Sleep(1000);
  47. }
  48. }
  49. }
  50. }

三种同步调用的结果一样:
019 委托 - 图10

使用委托进行隐式异步调用 BeginInvoke

  1. using System;
  2. using System.Threading;
  3. namespace DelegateExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. var stu1 = new Student { ID = 1, PenColor = ConsoleColor.Yellow };
  10. var stu2 = new Student { ID = 2, PenColor = ConsoleColor.Green };
  11. var stu3 = new Student { ID = 3, PenColor = ConsoleColor.Red };
  12. var action1 = new Action(stu1.DoHomework);
  13. var action2 = new Action(stu2.DoHomework);
  14. var action3 = new Action(stu3.DoHomework);
  15. // 使用委托进行隐式异步调用。
  16. // BeginInvoke 自动生成分支线程,并在分支线程内调用方法。
  17. action1.BeginInvoke(null, null);
  18. action2.BeginInvoke(null, null);
  19. action3.BeginInvoke(null, null);
  20. // 主线程模拟在做某些事情。
  21. for (var i = 0; i < 10; i++)
  22. {
  23. Console.ForegroundColor = ConsoleColor.Cyan;
  24. Console.WriteLine("Main thread {0}",i);
  25. Thread.Sleep(1000);
  26. }
  27. }
  28. }
  29. class Student
  30. {
  31. public int ID { get; set; }
  32. public ConsoleColor PenColor { get; set; }
  33. public void DoHomework()
  34. {
  35. for (int i = 0; i < 5; i++)
  36. {
  37. Console.ForegroundColor = PenColor;
  38. Console.WriteLine("Student {0} doing homework {1} hour(s)", ID, i);
  39. Thread.Sleep(1000);
  40. }
  41. }
  42. }
  43. }

019 委托 - 图11

使用 Thread 与 Task 进行异步调用

  1. using System;
  2. using System.Threading;
  3. using System.Threading.Tasks;
  4. namespace DelegateExample
  5. {
  6. class Program
  7. {
  8. static void Main(string[] args)
  9. {
  10. var stu1 = new Student { ID = 1, PenColor = ConsoleColor.Yellow };
  11. var stu2 = new Student { ID = 2, PenColor = ConsoleColor.Green };
  12. var stu3 = new Student { ID = 3, PenColor = ConsoleColor.Red };
  13. // 老的显式异步调用方式 Thread
  14. //var thread1 = new Thread(new ThreadStart(stu1.DoHomework));
  15. //var thread2 = new Thread(new ThreadStart(stu2.DoHomework));
  16. //var thread3 = new Thread(new ThreadStart(stu3.DoHomework));
  17. //thread1.Start();
  18. //thread2.Start();
  19. //thread3.Start();
  20. // 使用 Task
  21. var task1 = new Task(new Action(stu1.DoHomework));
  22. var task2 = new Task(new Action(stu2.DoHomework));
  23. var task3 = new Task(new Action(stu3.DoHomework));
  24. task1.Start();
  25. task2.Start();
  26. task3.Start();
  27. // 主线程模拟在做某些事情。
  28. for (var i = 0; i < 10; i++)
  29. {
  30. Console.ForegroundColor = ConsoleColor.Cyan;
  31. Console.WriteLine("Main thread {0}", i);
  32. Thread.Sleep(1000);
  33. }
  34. }
  35. }
  36. class Student
  37. {
  38. public int ID { get; set; }
  39. public ConsoleColor PenColor { get; set; }
  40. public void DoHomework()
  41. {
  42. for (int i = 0; i < 5; i++)
  43. {
  44. Console.ForegroundColor = PenColor;
  45. Console.WriteLine("Student {0} doing homework {1} hour(s)", ID, i);
  46. Thread.Sleep(1000);
  47. }
  48. }
  49. }
  50. }

image.png

适时地使用接口(interface)取代委托

Java 完全使用接口取代了委托功能。

以前面的模板方法举列,通过接口也能实现方法的可替换。

  1. using System;
  2. namespace DelegateExample
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. IProductFactory pizzaFactory = new PizzaFactory();
  9. IProductFactory toyCarFactory = new ToyCarFactory();
  10. var wrapFactory = new WrapFactory();
  11. Box box1 = wrapFactory.WrapProduct(pizzaFactory);
  12. Box box2 = wrapFactory.WrapProduct(toyCarFactory);
  13. Console.WriteLine(box1.Product.Name);
  14. Console.WriteLine(box2.Product.Name);
  15. }
  16. }
  17. interface IProductFactory
  18. {
  19. Product Make();
  20. }
  21. class PizzaFactory : IProductFactory
  22. {
  23. public Product Make()
  24. {
  25. var product = new Product();
  26. product.Name = "Pizza";
  27. return product;
  28. }
  29. }
  30. class ToyCarFactory : IProductFactory
  31. {
  32. public Product Make()
  33. {
  34. var product = new Product();
  35. product.Name = "Toy Car";
  36. return product;
  37. }
  38. }
  39. class Product
  40. {
  41. public string Name { get; set; }
  42. }
  43. class Box
  44. {
  45. public Product Product { get; set; }
  46. }
  47. class WrapFactory
  48. {
  49. // 模板方法,提高复用性
  50. public Box WrapProduct(IProductFactory productFactory)
  51. {
  52. var box = new Box();
  53. Product product = productFactory.Make();
  54. box.Product = product;
  55. return box;
  56. }
  57. }
  58. }