C#委托(Delegate)学习日记

时间:2023-03-09 15:35:52
C#委托(Delegate)学习日记

在.NET平台下,委托类型用来定义和响应应用程序中的回调。事实上,.NET委托类型是一个类型安全的对象,指向可以以后调用的其他方法。和传统的C++函数指针不同,.NET委托是内置支持多路广播和异步方法调用的对象。

委托类型包含3个重要信息:

  • 它所调用的方法的名称
  • 该方法的参数
  • 该方法的返回值

1.定义一个委托类型

// 这个委托可以指向任何传入两个整数,返回整数的方法 
public delegate int BinaryOp(int x,int y);

创建一个委托类型时,需要使用delegate关键字。当C#编译器处理委托类型时,它先自动产生一个派生自System.MulticastDelegate的密封类。

通过ildasm.exe来查看BinaryOp委托,如下图

C#委托(Delegate)学习日记

编译器是如何确切的定义 Invoke(),BeginInvoke(),EndInvoke() 的呢?让我们看看下面这段代码:

sealed class BinaryOp : System.MulticastDelegate
{
  public int Invoke(int x,int y);   public IAsyncResult BeginInvoke(int x,int y,AsyncCallback cb,object state);   public int EndInvoke(IasyncResult result);
}

Invoke() 方法定义的参数和返回值完全匹配BinaryOp委托的定义。

BeginInvoke() 成员签名的参数也基于BinaryOp委托;但BenginInvoke()方法将总是提供最后两个参数(AsyncCallback,object),用于异步方法的调用。

EndInvoke() 方法的返回值与初始的委托声明相同,总是以一个实现了IasyncResult接口的对象作为其唯一的参数。

我们再定义一个 带有 out/ref 参数的委托类型如下:

public delegate string MyOtherDelegate(out bool a,ref bool b,int c);

试想一下,产生的代码还和上面一样吗?好。

sealed class MyOtherDelegate: System.MulticastDelegate
{
  public string Invoke(out bool a,ref bool b,int c);   public IAsyncResult BeginInvoke(out bool a,ref bool b,int c,AsyncCallback cb,object state);   public stringEndInvoke(out bool a,ref bool b,IasyncResult result);
}

我们发现,Invoke() 和 BeginInvoke() 方法的签名不出所料,但 EndInvoke() 略有差异,其中包括了委托类型定义的所有 out/ref 参数。

2.System.Delegate和System.MulticastDelegate基类

使用关键字创建委托的时候,也就间接的声明了一个派生自System.MulticastDelegate的类。

下面是System.MulticastDelegate部分成员源代码:

public abstract class MulticastDelegate : Delegate
{
// 用来在内部管理委托所维护的方法列表
private Object _invocationList;
private IntPtr _invocationCount; // 返回所指向的方法的列表
public override sealed Delegate[] GetInvocationList()
{
Contract.Ensures(Contract.Result<Delegate[]>() != null); Delegate[] del;
Object[] invocationList = _invocationList as Object[];
if (invocationList == null)
{
del = new Delegate[];
del[] = this;
}
else
{
// Create an array of delegate copies and each
// element into the array
int invocationCount = (int)_invocationCount;
del = new Delegate[invocationCount]; for (int i = ; i < invocationCount; i++)
del[i] = (Delegate)invocationList[i];
}
return del;
} // 重载的操作符
public static bool operator ==(MulticastDelegate d1, MulticastDelegate d2)
{
if ((Object)d1 == null)
return (Object)d2 == null; return d1.Equals(d2);
} public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2)
{
if ((Object)d1 == null)
return (Object)d2 != null; return !d1.Equals(d2);
}
}

MulticastDelegate

下面是System.Delegate部分成员源代码:

 public abstract class Delegate : ICloneable, ISerializable
{
// 与函数列表交互的方法
public static Delegate Combine(Delegate a, Delegate b)
{
if ((Object)a == null) // cast to object for a more efficient test
return b; return a.CombineImpl(b);
} public static Delegate Combine(params Delegate[] delegates)
{
if (delegates == null || delegates.Length == )
return null; Delegate d = delegates[];
for (int i = ; i < delegates.Length; i++)
d = Combine(d,delegates[i]); return d;
} public static Delegate Remove(Delegate source, Delegate value)
{
if (source == null)
return null; if (value == null)
return source; if (!InternalEqualTypes(source, value))
throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis")); return source.RemoveImpl(value);
} public static Delegate RemoveAll(Delegate source, Delegate value)
{
Delegate newDelegate = null; do
{
newDelegate = source;
source = Remove(source, value);
}
while (newDelegate != source); return newDelegate;
} // 重载的操作符
public static bool operator ==(Delegate d1, Delegate d2)
{
if ((Object)d1 == null)
return (Object)d2 == null; return d1.Equals(d2);
} public static bool operator != (Delegate d1, Delegate d2)
{
if ((Object)d1 == null)
return (Object)d2 != null; return !d1.Equals(d2);
} // 扩展委托目标的属性
public MethodInfo Method
{
get
{
return GetMethodImpl();
}
}
public Object Target
{
get
{
return GetTarget();
}
} }

Delegate

System.Delegate / System.MulticastDelegate 部分成员
 继承成员      作用
Method 此属性返回System.Reflection.MethodInfo对象,用以表示委托维护的静态方法的详细信息
Target 如果方法调用是定义在对象级别的(而非静态方法),Target返回表示委托维护的方法对象。如果Target返回null,调用的方法是一个静态成员
Combine() 此静态方法给委托维护列表添加一个方法,在C#中,使用重载+=操作符作为简化符号调用此方法
GetInvocationList() 此方法返回一个System.Delegate类型的数组,其中数组中的每个元素都表示一个可调用的特定方法

Remove()

RemoveAll()

 这些静态从调用列表中移除一个(所有)方法;在C#中,Remove方法可以通过使用重载-=操作符来调用

一. 创建简单的委托

好了,了解了以上这些委托的基础信息,我们开始创建属于我们的第一个委托:

public class MyDelegate
{
// 这个委托可以指向任何传入两个整数并返回一个整数的方法
private delegate int BinaryOp(int x, int y); // BinaryOp委托将指向的方法
private static int Add(int x, int y) => x + y; public static void Show()
{
// 创建一个指向 Add() 方法的BinaryOp委托对象
BinaryOp b = new BinaryOp(Add); // 使用委托对象间接调用Add()方法的两种方法:
Console.WriteLine($"b(2, 3)-->{b(2, 3)}");
Console.WriteLine($"b.Invoke(2,3)-->{ b.Invoke(2, 3)}"); DisplayDelegateInfo(b);
} // 将输出委托调用列表那个每个成员的名称
public static void DisplayDelegateInfo(Delegate delObj)
{
foreach (Delegate item in delObj.GetInvocationList())
{
// 若delObj委托指向的是静态方法,则 item.Target 为null
Console.WriteLine($"{item.Method},{item.Target}");
}
}
}

二.使用委托发送对象状态通知

以上的示例纯粹用来说明委托的作用,因为仅为两个数相加创建一个委托没有多大必要,为了更现实的委托应用,我们用委托来定义Car类,它可以通知外部实体当前引擎的状态。步骤如下:

  1. 定义将通知发送给调用者的委托类型
  2. 声明Car类中的每个委托类型的成员变量
  3. 在Car上创建辅助函数使调用者能指定由委托成员变量保存的方法
  4. 修改Accelerate()方法以在适当的情形下调用委托的调用列表。

定义Car类:

    public class Car
{
// 内部状态数据
public int CurrentSpeed { get; set; } public int MaxSpeed { get; set; } public string PetName { get; set; } // 汽车是否可用
private bool CarIsDead; public Car()
{
MaxSpeed = ;
} public Car(string name, int maxSp, int currSp)
{
CurrentSpeed = currSp;
MaxSpeed = maxSp;
PetName = name;
} // 1. 定义委托类型
public delegate void CarEngineHandler(string msgForCaller); // 2. 定义每个委托类型的成员变量
private CarEngineHandler listOfHandlers; // 3. 向调用者添加注册函数
public void RegisterWithCarEngine(CarEngineHandler methodCall)
{
listOfHandlers = methodCall;
} // 4. 实现Accelerate()方法以在某些情况下调用委托的调用列表 public void Accelerate(int delta)
{
if (CarIsDead)
{
if (listOfHandlers != null)
{
listOfHandlers("sorry,this car is dead///");
}
}
else
{
CurrentSpeed += delta; if ((MaxSpeed - CurrentSpeed) == && listOfHandlers != null)
{
listOfHandlers("careful buddy ! gonna blow !");
} if (CurrentSpeed >= MaxSpeed)
{
CarIsDead = true;
}
else
{
Console.WriteLine($"CurrentSpeed={CurrentSpeed}");
}
}
}
}

注意:我们在调用 listOfHandlers 成员变量保存方法之前,需要坚持该变量是否为空,因为在调用 RegisterWithCarEngine() 方法分配这些对象是 调用者的任务。如果调用者没有调用这个方法而试图调用 listOfHandlers ,将引发空异常。

我们来看看具体该如何调用:

public class CarDelegate
{
public static void Show()
{
Console.WriteLine("******Delegate Car******"); // 创建一个Car对象
Car c1 = new Car("bmw", , ); // 告诉汽车,它想要向我们发送信息时条用哪个方法
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEvent)); // 加速,触发事件
Console.WriteLine("******Speeding up******");
for (int i = ; i < ; i++)
{
c1.Accelerate();
}
} // 要传入事件的方法
private static void OncarEngineEvent(string msg)
{
Console.WriteLine($"=>{msg}");
}
}

注意:RegisterWithCarEngine() 方法要求传入一个内嵌的 CarEngineHandler 的委托的实例,与其他委托一样,我们指定一个“所指向的方法”作为构造函数的参数。OncarEngineEvent() 方法与相关的委托完全匹配,也包含 string 类型的输入参数和 void 返回类型。

运行结果如下图:

C#委托(Delegate)学习日记

a.支持多路广播

一个委托对象可以维护一个可调用方法的列表而不只是单独的一个方法。给委托对象添加多个方法时,重载+=操作符即可。为上面的Car类支持多路广播,修改RegisterWithCarEngine()方法,具体如下:

// 现在支持多路广播
// 注意蓝色"+="操作符,而非赋值操作符"="
public void RegisterWithCarEngine(CarEngineHandler methodCall)
{
listOfHandlers += methodCall;
}

如此一来,调用者就可以为同样的回调注册多个目标对象了,这个,第二个采用打印大写的传入信息。

public class CarDelegate
{
public static void Show()
{
Console.WriteLine("******Delegate Car******"); // 创建一个Car对象
Car c1 = new Car("bmw", , ); // 为通知注册多个目标
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEvent));
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEcentToUpper)); // 加速,触发事件
Console.WriteLine("******Speeding up******");
for (int i = ; i < ; i++)
{
c1.Accelerate();
}
} //现在在发送通知信息时,Car类将调用以下这两个方法
private static void OncarEngineEvent(string msg)
{
Console.WriteLine($"=>{msg}");
} private static void OncarEngineEcentToUpper(string msg)
{
Console.WriteLine($"message form car object=>{msg.ToUpper()}");
}
}

b.从委托调用列表中移除成员
Dlegate类还定义了静态Remove()方法,允许调用者动态从委托对象的调用列表中移除方法,这样一来,调用者就在运行是简单的“退订”某个已知的通知。你可以直接使用Delegate.Remove(),也可以 “-=” 操作符。

为Car类添加 UnRegisterWithCarEngine() 方法,允许调用者从调用列表中移除某个方法。

public void UnRegisterWithCarEngine(CarEngineHandler methodCall)
{
listOfHandlers -= methodCall;
}

调用如下:

public static void Show()
{
Console.WriteLine("******Delegate Car******"); // 创建一个Car对象
Car c1 = new Car("bmw", , ); // 告诉汽车,它想要向我们发送信息时条用哪个方法
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEcent)); // += 先绑定委托对象,稍后注销
Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OncarEngineEcentToUpper);
c1.RegisterWithCarEngine(handler2); // 加速,触发事件
Console.WriteLine("******Speeding up******");
for (int i = ; i < ; i++)
{
c1.Accelerate();
} // -= 注销第二个处理程序
c1.UnRegisterWithCarEngine(handler2); // 看不到大写的消息了
Console.WriteLine("******Speeding up******");
for (int i = ; i < ; i++)
{
c1.Accelerate();
}
}

c.方法组转换语法

我们显示的创建了 Car.CarEngineHandler 委托对象的实例,以注册和注销引擎通知:

Car c1 = new Car("bmw", , );

c1.RegisterWithCarEngine(new Car.CarEngineHandler(OncarEngineEcent));
Car.CarEngineHandler handler2 = new
Car.CarEngineHandler(OncarEngineEcentToUpper);
c1.RegisterWithCarEngine(handler2); c1.UnRegisterWithCarEngine(handler2);

如果要调用MulticastDelegate或Delegate总继承的任何成员,手工创建一个委托变量是最直接的方式。但是大多数情况下,我们并不依靠委托对象。我们可以使用C#提供的方法组转换的方法,它允许我们在调用以委托作为参数的方法时直接提供了与委托期望的签名想匹配的方法的名称(返回 void,参数 string),而不是创建委托对象。

public class CarDelegateMethodGroupConversion
{
public static void Show()
{
Console.WriteLine("******Delegate Car******");
Car c1 = new Car("slogbug", , );
     
     // 注册简单的类型名称
c1.RegisterWithCarEngine(CallMethere); Console.WriteLine("******Speeding up******");
for (int i = ; i < ; i++)
{
c1.Accelerate();
}      // 注册简单的类型名称
c1.UnRegisterWithCarEngine(CallMethere); Console.WriteLine("******Speeding up******");
for (int i = ; i < ; i++)
{
c1.Accelerate();
}
} private static void CallMethere(string msg) => Console.WriteLine($"=>{msg}");
}

四:泛型委托

如果我们希望定义一个委托类型来调用任何返回void并且接受单个参数的方法。如果这个参数可能会不同,我们就可以通过类型参数来构建。

下面我们看一个小示例:

public class GenericDelegate
{
public delegate void MyGenericDelegate<T>(T arg); public static void Show()
{
     // 注册目标
MyGenericDelegate<string> stringTarget = new MyGenericDelegate<string>(StringTarget);
stringTarget("i am ok"); MyGenericDelegate<int> intTarget = new MyGenericDelegate<int>(IntTarget);
intTarget.Invoke();
} static void StringTarget(string arg) => Console.WriteLine($"StringTarget--> {arg.ToUpper()}"); static void IntTarget(int arg) => Console.WriteLine($"IntTarget--> {++arg}"); }

a. 泛型Action<> 和 Func<> 委托
从以上的学习中我们已经了解到,使用委托在应用程序中进行回调需要遵循以下步骤:

  • 自定义一个与要指向的方法格式相匹配的委托
  • 创建自定义委托的实例,将方法名作为构造函数的参数
  • 通过调用委托对象的Invoke()方法来间接调用该方法

其实,这种方式通常会构建大量只用于当前任务的自定义委托。当委托名无关紧要的时候,我们可以使用框架内置的Action<> 和 Func<> 泛型委托,可指向至多传递16个参数的方法。

Action<>:无返回值: 定义 public delegate void Action<...>

public class MyActionDelegate
{
public static void Show()
{
// 使用Action<>委托来指向 DisplayMessage()
Action<string, ConsoleColor, int> actionTarget = new Action<string, ConsoleColor, int>(DisplayMessage);
actionTarget("actionTarget", ConsoleColor.Red, );
} // Action<> 委托的一个目标
private static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount)
{
ConsoleColor previous = Console.ForegroundColor;
Console.ForegroundColor = txtColor; for (int i = ; i < printCount; i++)
{
Console.WriteLine(msg);
} Console.ForegroundColor = previous;
}
}

运行效果如下图:

C#委托(Delegate)学习日记

Func<>:有返回值 public delegate TResult Func<..., out TResult>

public class FuncDelagate
{
public static void Show()
{
Func<int, int, int> funcTarget = new Func<int, int, int>(Add);
int result = funcTarget(, );
Console.WriteLine(result); Func<int, int, string> funcTarget2 = new Func<int, int, string>(SumToString);
string sumStr = funcTarget2(, );
Console.WriteLine(sumStr);
} static int Add(int x, int y) => x + y; static string SumToString(int x, int y) => (x + y).ToString();
}

运行结果:3,7

鉴于 Action<> 和 Func<> 节省了手工创建自定义委托的步骤,but 总是应该使用他们吗?

答案:“视情况而定”。

很多情况下 Action<> 和 Func<> 都是首选,但如果你觉得一个具有自定义名称的委托更有助于捕获问题范畴,那么构建自定义委托不过就是一行代码的事儿。

注:Linq中就大量的用到了 Action<> 和 Func<>。

本文参考《精通C#》

学无止境,望各位看官多多指教。