C# 委托与事件总结
C# 中的委托和事件可以说是超级拦路虎了,一个不小心就容易让人直接放弃学习。最近几天花了点时间钻研了一下,查看了一些资料,希望做个总结。
我还达不到技术大佬的级别,主要就是做个自我总结,很推荐大家看一下我下面发的参考资料里的文章和视频,那些是真正的大佬产出的内容。
推荐资料
张子扬博客中的文章:
刘铁猛的视频:
- 刘铁猛《C#语言入门详解》- P19 委托详解
- 刘铁猛《C#语言入门详解》- P20 事件详解(上)
- 刘铁猛《C#语言入门详解》- P21 事件详解(中)
- 刘铁猛《C#语言入门详解》- P22 事件详解(下)
官方文档:C# 官方文档
把委托单独拿出来讲倒是不难理解,但是和和事件结合就会产生化学反应,第一次学习的时候直接爆炸了…
委托
委托其实就是个类(因此它定义时往往和类平级),它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递(其实就是一直说的函数式编程),主要目的就是为了让程序有更好的拓展型。
即使不学习这个概念,基本上不会影响我们编写程序(大不了就是 if else 走天下,来新需求就大规模的改代码…),但是这个概念对于写出高质量、优雅的程序至关重要。
类比一下其他语言,关于完成“传递方法”这件事,各个语言有不同的做法:
- C / C++ 通过函数指针传递函数,而委托可以看作函数指针的 “升级版”,它比函数指针更加安全
- Java 中没有委托的概念,需要依靠接口来实现传递方法,其中一些典型的函数式接口如:
Supplier
优化不一定执行的代码、Consumer
:接收一个值决定要做什么、Predicate
:让过滤条件更灵活、Function
:实现类型转换 - JavaScript 中“函数是一等公民”,可以直接实现将函数作为参数与返回值
可以看到,函数式编程是个重要的概念,各个语言基于这个概念只是实现方式不同,而 C# 的实现方式就是 委托。
使用 C# 内置委托:Action 和 Func
先学会怎么用,再探究怎么写,因此我们需要先学会使用 Aciton 和 Func
Action 和 Func:是 C# 中内置的委托类型:
- Action 用于委托没有返回值的函数
Action
表示无参,无返回值的委托
Action<int, string>
表示有传入参数 int、string 无返回值的委托
Action<int, string, bool>
表示有传入参数 int、string、bool 无返回值的委托 - Func 用于委托有返回值的函数
Func<int>
表示无参,返回值是 int 的委托
Func<int, int>
表示返回值是 int,传入参数 int 的委托
Func<double, double, int>
表示返回值是 double,传入参数是 double、int 的委托
class Test
{
class Calculator
{
public void Report() => Console.WriteLine("I have 3 methods");
public int Add(int a, int b) => a + b;
}
static void Main()
{
Calculator calculator = new Calculator();
// Action用于委托没有返回值的函数
Action action = new Action(calculator.Report);
action(); // action.Invoke() 也可以
// Func的范型的第一个参数是返回值,然后是函数的输入参数
Func<int, int, int> func = new Func<int, int, int>(calculator.Add);
Console.WriteLine(func(1, 2)); // func.Invoke(1, 2);
}
}
自定义委托
在学会使用委托的情况下,再来自己声明一个委托试试看,然后使用它
委托是类,所以声明位置是和 class 处于同一个级别。但 C# 允许嵌套声明类(一个类里面可以声明另一个类),所以有时也会有 delegate 在 class 内部声明的情况。
// 使用 delegate 声明委托
public delegate double Calc(double x, double y);
class Calculator
{
public double Mul(double x, double y) => x * y;
public double Div(double x, double y) => x / y;
}
class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
// 创建委托实例的完整写法
Calc calc1 = new Calc(calculator.Mul);
// 创建委托实例的简单写法
Calc calc2 = calculator.Div;
Console.WriteLine(calc1(6, 2)); // 12
Console.WriteLine(calc2(6, 2)); // 3
}
}
委托的综合实例
委托的一般使用场景:
- 模版方法,提高代码复用性
- 回调函数,将某个方法传入主调方法中,根据其逻辑决定是否调用该方法
综合实例:
using System;
class Program
{
static void Main(string[] args)
{
var productFactory = new ProductFactory();
// Func 用于有返回值的函数,范型中第一个类型即返回值类型
Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);
var wrapFactory = new WrapFactory();
var logger = new Logger();
// Action 用于没有返回值的函数,范型中的类型即传入参数类型
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)
{
// Now 是带时区的时间,存储到数据库应该用不带时区的时间 UtcNow。
Console.WriteLine("Product '{0}' created at {1}.Price is {2}", product.Name, DateTime.UtcNow, product.Price);
}
}
// 产品类 - 业务
class Product
{
public string Name { get; set; }
public double Price { get; set; }
}
// 包装类 - 业务
class Box
{
public Product Product { get; set; }
}
// 包装工厂类 - 用于包装产品
class WrapFactory
{
// 模板方法,提高复用性
// 所有的产品包装过程都需要遵守这个规范,
// 对WrapFactory来说不关心具体对象, 只负责执行这个操作
public Box WrapProduct(Func<Product> getProduct, Action<Product> logCallBack)
{
var box = new Box();
Product product = getProduct.Invoke();
// 只 log 价格高于 50 的
if (product.Price >= 50)
{
logCallBack(product);
}
box.Product = product;
return box;
}
}
// 产品工厂类 - 用于生产不同的产品
class ProductFactory
{
public Product MakePizza()
{
return new Product { Name = "Pizza", Price = 12 }; ;
}
public Product MakeToyCar()
{
return new Product { Name = "Toy Car", Price = 100 }; ;
}
}
多播委托
多播委托就是,一个委托内部有多个方法,当该委托被调用时,其中的方法依次调用:
class Program
{
static void Main(string[] args)
{
Student stu1 = new Student { Name = "aaa" };
Student stu2 = new Student { Name = "bbb" };
Action action1 = new Action(stu1.Study);
Action action2 = new Action(stu2.Study);
// 单播委托 -> 每个委托依次调用
//action1.Invoke();
//action2.Invoke();
// 多播委托 -> 多个委托合并为一个委托再调用
action1 += action2;
action1.Invoke();
}
}
class Student
{
public string Name { get; set; }
public void Study() => Console.WriteLine(Name + " is doing studying!");
}
事件
事件声明有完整声明和简略声明两种,简略声明是完整声明的语法糖
事件无论是从表层约束还是从底层实现都是依赖于委托的
事件声明的完整格式
声明委托类型 ≠ 声明委托类型字段
- 委托类型是与类同级别的
- 委托类型字段是类内部的一个字段
using System;
using System.Threading;
namespace EventExample
{
class Program
{
static void Main(string[] args)
{
// 1.事件拥有者
var customer = new Customer();
// 2.事件响应者
var waiter = new Waiter();
// 3.Order 事件成员 5. +=事件订阅
customer.Order += waiter.Action;
customer.Action();
customer.PayTheBill();
}
}
// 该类用于传递点的是什么菜,作为事件参数,需要以 EventArgs 结尾,且继承自 EventArgs
public class OrderEventArgs : EventArgs
{
// 菜品名
public string DishName { get; set; }
// 菜品大小
public string Size { get; set; }
}
// 声明一个委托类型,因为该委托用于事件处理,所以以 EventHandler 结尾
// 注意委托类型的声明和类声明是平级的
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
public class Customer
{
// 委托类型字段
private OrderEventHandler orderEventHandler;
// 事件声明
public event OrderEventHandler Order
{
add { this.orderEventHandler += value; }
remove { this.orderEventHandler -= value; }
}
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.", this.Bill);
}
public void WalkIn()
{
Console.WriteLine("Walk into the restaurant");
}
public void SitDown()
{
Console.WriteLine("Sit down.");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(1000);
}
if (this.orderEventHandler != null)
{
var e = new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
this.orderEventHandler.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
public class Waiter
{
// 4.事件处理器
public void Action(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
double price = 10;
switch (e.Size)
{
case "small":
price *= 0.5;
break;
case "large":
price *= 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
事件声明的简略格式
简略格式:一种 filed-like 的声明格式。
filed-like:像字段声明一样 。
简略格式与上例的完整格式只有事件声明和事件触发两处不同
using System;
using System.Threading;
namespace EventExample
{
class Program
{
static void Main(string[] args)
{
// 1.事件拥有者
var customer = new Customer();
// 2.事件响应者
var waiter = new Waiter();
// 3.Order 事件成员 5. +=事件订阅
customer.Order += waiter.Action;
customer.Action();
customer.PayTheBill();
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
public class Customer
{
// 简略事件声明,看上去像一个委托(delegate)类型字段
public event OrderEventHandler Order;
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.", this.Bill);
}
public void WalkIn()
{
Console.WriteLine("Walk into the restaurant");
}
public void SitDown()
{
Console.WriteLine("Sit down.");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(1000);
}
// 微软的语法糖使得事件变得像委托类型字段一样
if (this.Order != null)
{
var e = new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
// 事件触发
this.Order.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
public class Waiter
{
// 4.事件处理器
public void Action(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
double price = 10;
switch (e.Size)
{
case "small":
price *= 0.5;
break;
case "large":
price *= 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
事件的本质
事件本质上就是,对委托字段的一个包装器
- 事件这个包装器对委托字段的访问起限制作用,只让你访问 +=、-= ,让你只能给事件添加或移除事件处理器,让程序更加安全更好维护
- 事件对外界隐藏了委托实例的大部分功能,仅仅暴露添加/删除事件处理器的功能
使用 EventHandler
C# 中内置一个通用的委托声明:EventHandler
我们可以将前面代码中自己声明的委托去掉:
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
将上面这行代码注释后,在 Customer 中使用 EventHandler 作为委托声明:
public class Customer
{
// 使用默认的 EventHandler,而不是声明自己的
public event EventHandler Order;
// code..
}
命名约定
触发事件的方法一般命名为 OnXxx,且访问级别为 protected(自己的类成员及派生类能访问)
依据单一职责原则,把原来的 Think 中触发事件的部分单独提取为 OnOrder 方法
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(1000);
}
this.OnOrder("Kongpao Chicken","large");
}
protected void OnOrder(string dishName, string size)
{
if (this.Order != null)
{
var e = new OrderEventArgs();
e.DishName = dishName;
e.Size = size;
this.Order.Invoke(this, e);
}
}