目录
1.委托是什么?
微软官方文档对其定义个人认为还是比较准确的:委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联,然后可以通过委托实例调用该方法。
概念还是有点抽象,通过简单的代码对概念进行解构就清晰多了:
①创建委托:
//这个委托的特征是:无返回值,一个string形参
public delegate void SaySomething(string value);
②定义一个无返回值、一个string形参的方法:
public static void TellTime(string time)
{
Console.WriteLine($"报时:现在是:{time}");
}
根据概念可知,方法的返回类型和签名必须完全与定义的委托相同,这是委托一个方法的关键因素。
③将方法传递给委托:
SaySomething say = TellTime;
注意写法:SaySomething代表定义的委托名称,say是委托实例,TellTime是要引用的方法。
④将委托作为实参传递给一个方法:
//这个方法的动作是当摆钟到整点时,需要做整点报时
public static void DoSomething(SaySomething say)
{
Console.WriteLine("动作:大摆正分钟摆动到12点");
Console.WriteLine("提醒:接下来,即将整点报时");
say("12");
}
⑤执行方法,查看结果:
可以看到,方法在合适的时间执行了委托传递的方法。
2.委托的作用?
对方法调用的解耦:有了委托之后,就可以把方法的引用封装在委托对象中,然后将这个委托对象传递给一个方法,该方法内部便使用的是委托对象而不是直接调用某一个方法,所以在编译阶段,方法内部是不会知道具体调用了哪一个方法的,只有在运行时,当给委托对象委托了一个方法之后,方法在执行时才确定的会调用某一个(多个)方法。
来看一个使用委托解耦的程序:
不使用委托时:
public static string GetDataByMysql(int id)
{
//模拟查询过程
//Mysql过于复杂的查询算法
if ((id * 2 / 2) == 1)
{
return "小张的员工数据";
}
if ((id * 2 / 2) == 2)
{
return "小王的员工数据";
}
return "未查询到员工数据";
}
public static void GetEmployeeData()
{
Random r = new Random();
int employeeId = r.Next(0, 3);
string employeeData = GetDataByMysql(employeeId);
Console.WriteLine(employeeData);
}
当上面的代码一写完,你发现SQL Server性能更高一点,这时你想切换SQL Server来查询员工数据:
public static string GetDataBySqlServer(int id)
{
//模拟查询过程
if (id == 1)
{
return "小张的员工数据";
}
if (id == 2)
{
return "小王的员工数据";
}
return "未查询到员工数据";
}
public static void GetEmployeeData()
{
Random r = new Random();
int employeeId = r.Next(0, 3);
string employeeData = GetDataBySqlServer(employeeId);
Console.WriteLine(employeeData);
}
除了增加了使用Sqlserver查询数据的代码之外,最主要的就是在GetEmployeeData方法中使用GetDataBySqlServer的方法签名替换了GetDataByMySql的方法签名,注意,这样的做法导致了GetEmployeeData()方法的内部变动,想想当这样的方法调用遍布在程序的各个位置,然后程序还经常重构的话,那么就真的知道什么是牵一发而动全身了。
接下来使用委托解决以上问题:
第一次使用Mysql查数据:
public static string GetDataByMysql(int id)
{
...
}
public static void GetEmployeeData(EmployeeData data)
{
Random r = new Random();
int employeeId = r.Next(0, 3);
string employeeData = data(employeeId);
Console.WriteLine(employeeData);
}
public static void Main(string[] arg)
{
EmployeeData data = GetDataByMysql;
GetEmployeeData(data);
Console.ReadLine();
}
然后重构为使用Sql server查数据:
public static string GetDataBySqlServer(int id)
{
...
}
public static void GetEmployeeData(EmployeeData data)
{
Random r = new Random();
int employeeId = r.Next(0, 3);
string employeeData = data(employeeId);
Console.WriteLine(employeeData);
}
public static void Main(string[] arg)
{
EmployeeData data = GetDataBySqlServer;
GetEmployeeData(data);
Console.ReadLine();
}
可以看到,程序现在可以使用Sqlserver的方式查询数据,但GetEmployeeData方法一点都没变动,所以程序的变化对GetEmployeeData方法在代码层面没有一点影响。这就体现了使用委托对程序做解耦的优势了!
3.委托的内部构造
表面上,我们使用delegate就可以定义一个委托,但其实委托的内部构造还是依托于类。那么通过反编译工具来看一下一个委托的内部构造:
//定义一个委托
public delegate object FeedBack(Int32 value);
//编译器和CLR协同实现的委托内部
public class FeedBack:System.MulticastDelegate {
public FeedBack(Object @object,IntPtr method);
public virtual void Invoke(Int32 value);
public birtual IAsyncResult BeginInvoke(Int32 value,AsyncCallback callback,Object @object);
public virtual void EndInvoke(IAsyncResult result);
}
对于构造器
- Object:
- 实例方法会保存对象引用
- 静态方法是null
- IntPtr: 标识要回调的方法
所以每个委托对象实际是一个包装器,其中包装了一个方法和调用该方法时要操作的对象。
4.如何使用委托?
哪些地方可以定义委托:委托既可以在一个类型中定义,也可在全局范围中定义。简单的说,由于委托是类,所以凡是能够定义类的地方,都能定义委托。
4.1 用委托回调静态方法
调用静态方式的例子:
public class Program
{
public delegate void FeedBack(int value);
private static void FeedBackToConsole(int value)
{
Console.WriteLine($"value is {value}");
}
public static void Main(string[] arg)
{
FeedBack feedBack = FeedBackToConsole;
TestDelegate.Out(feedBack);
Console.ReadLine();
}
}
class TestDelegate
{
public static void Out(FeedBack feedBack)
{
feedBack?.Invoke(1);
}
}
这个例子是为了说明:在一个类型中通过委托来调用另一个类型的私有成员,只要委托对象是由具有足够安全/可访问性的代码创建的,便没有问题。
委托的协变性与逆变性:将方法绑定到委托时,C#和CLR都允许引用类型的协变性和逆变性。协变性是指方法能返回从委托的返回类型派生的一个类型。逆变性是指方法获取的参数可以是委托的参数类型的基类。
例子:
delegate Object MyCallback(FileStream s);
String SomeMethod(Stream);
4.2 用委托回调多个方法(委托链)
委托链是委托对象的集合,可利用委托链调用集合中的委托所代表的全部方法。
通过了解委托的内部构造可知,定义的委托都会继承MulticastDelegate类,而该类中有一个_invocationList字段:
该字段存储了所有的委托对象,所以该字段就相当于是委托链,然后在执行委托对象(调用Invoke方法)时,就会调用该字段的每一个委托方法。
伪码:
public void Invoke(Int32 value){
Delegate[] delegateSet = _InvocationList as Delegate[];
foreach(Feedback d in delegateSet)
{
d(value);
}else{
_methodPtr.Invoke(_target,value);
}
}
委托链的例子:
public class Program
{
public delegate void FeedBack(Int32 value);
private static void FeedBackToConsole(Int32 value)
{
Console.WriteLine($"FeedBackToConsole is {value}");
}
public static void FeedBackToOthers(Int32 value)
{
Console.WriteLine($"FeedBackToOthers is {value}");
}
public static void Main(string[] arg)
{
//FeedBack feedBack = new FeedBack(FeedBackToConsole);同下
FeedBack feedBack = FeedBackToConsole;
//FeedBack feedBack1 = FeedBackToOthers;
//feedBack = (FeedBack)Delegate.Combine(feedBack, feedBack1);同下
feedBack += FeedBackToOthers;
TestDelegate.Out(feedBack);
Console.WriteLine("------------------------");
//feedBack = (FeedBack)Delegate.Remove(feedBack, feedBack1);同下
feedBack -= FeedBackToConsole;
TestDelegate.Out(feedBack);
Console.ReadLine();
}
}
class TestDelegate
{
public static void Out(FeedBack feedBack)
{
feedBack?.Invoke(1);
}
}
结果:
需要注意: +=和-=这两个操作符是被重载过的,+=相当于Combine函数,-=相当于Remove函数。
4.3 C#为委托提供的简化语法:
4.3.1 不需要构造委托对象:
private static void FeedBackToConsole(Int32 value)
{
tonsole.WriteLine($"FeedBackToConsole is {value}");
}
//直接传入与Out方法要求的委托对象行为一致的方法
TestDelegate.Out(FeedBackToConsole);
public delegate void FeedBack(Int32 value);
public static void Out(FeedBack feedBack)
{
feedBack?.Invoke(1);
}
这是因为C#编译器能自己推断,所以可以省略构造FeedBack委托对象的代码,使代码可读性更佳,也更容易理解。