17.委托
.NET框架通过委托来提供回调函数机制,委托能确保回调函数是类型安全的(即方法的签名相同)。登记回调方法来获得通知。
17.1初识委托
internal sealed class DelegateIntroduce
{
internal delegate void Feedback(int value); // 委托类型Feedback的声明,可以把Feedback看成一个类型
public static void Go()
{
StaticDelegateDemo();
InstanceDelegateDemo();
ChainDelegateDemoBad(new DelegateIntroduce());
ChainDelegateDemoGood(new DelegateIntroduce());
}
private static void StaticDelegateDemo() // 静态委托
{
Console.WriteLine("----静态委托举例----");
Counter(1, 2, new Feedback(WriteToConsole)); // 登记回调方法,第三个参数是新构造的Feedback委托对象
Counter(1, 2, new Feedback(WriteToFile)); // 委托对象是方法的包装器,WriteToFile被传给委托类型Feedback的构造器
Console.WriteLine();
}
private static void InstanceDelegateDemo() // 实例委托
{
Console.WriteLine("----实例委托举例----");
DelegateIntroduce di = new DelegateIntroduce();
Counter(1, 2, new Feedback(di.WriteToMsgBox));
Console.WriteLine();
}
private static void ChainDelegateDemoBad(DelegateIntroduce di)
{
Console.WriteLine("----ChainDelegateDemoBad-----");
Feedback fb1 = new Feedback(WriteToConsole);
Feedback fb2 = new Feedback(di.WriteToMsgBox);
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
Counter(1, 2, fbChain);
fbChain = (Feedback)Delegate.Remove(fbChain, fb1); // 移除一个委托,Remove是新建一个委托对象,所以务必进行赋值
Counter(1, 2, fbChain);
Console.WriteLine();
}
private static void ChainDelegateDemoGood(DelegateIntroduce di)
{
Console.WriteLine("----ChainDelegateDemoGood----");
Feedback fb1 = new Feedback(WriteToConsole);
Feedback fb2 = new Feedback(di.WriteToMsgBox);
Feedback fbChain = null;
fbChain += fb1; // C#编译器为委托类型的实例重载了+=和-=操作符,这些操作符分别调用Combine和Remove
fbChain += fb2;
Counter(1, 2, fbChain);
fbChain -= fb1;
Counter(1, 2, fbChain);
Console.WriteLine();
}
private static void Counter(int from, int to, Feedback fb)
{
for (int i = from; i <= to; ++i)
{
if (null != fb)
fb(i);
}
}
private static void WriteToConsole(int value)
{
Console.WriteLine("item = " + value);
}
private static void WriteToFile(int value)
{
StreamWriter sw = new StreamWriter("result.text", true); // true表示append,result.text文件与.exe同目录
sw.WriteLine("item = " + value);
sw.Close();
}
private void WriteToMsgBox(int value)
{
MessageBox.Show("item = " + value);
}
}
17.2用委托回调静态方法
如上所述。
17.3用委托回调实例方法
如上所述。另外,包装实例方法很有用,因为对象可以维护一些状态,并可以在回调方法执行期间利用这些状态信息
17.4委托揭秘
本节介绍C#编译器和CLR如何协同工作来实现委托。
重新审视这行代码internal delegate void Feedback(int value);
,编译器实际会定义一个完整的类型,该类型里有构造器public Feedback(Object @object, intPtr method);
、一个签名与这行代码相同的虚方法Invoke(该节讨论)、异步回调的两个方法(第27章讨论)。用ILDasm查看生成的程序集,编译器的确生成了Feedback类。
由于委托是类,所以凡是能定义类的地方(嵌套在一个类型里或者在全局范围内),就能定义委托。
所有委托类型都派生自MulticastDelegate,所以它们都继承了MulticastDelegate的字段、属性和方法,其中有三个非公共字段(注意:在MulticastDelegate上按F12看不到非公共字段)最重要:
1._target,类型为System.Object,该字段引用的是回调方法要操作的对象
2._methodPtr,类型为System.IntPtr,IntPtr为指针的平台特定类型,一个内部的整数值,该字段标识要回调的方法
3._invocationList,类型为System.Object,该字段常为null,它引用一个委托数组
构造器的两个实参分别保存在_target和_methodPtr私有字段中,如下图:
了解委托对象的内部结构后,再来看看回调方法是如何调用的:
举例fb(val)
编译器知道Feedback的实例fb是一个引用了委托对象的变量(如上图的一个圆角方框就是一个委托对象),所以会生成代码调用该委托对象的Invoke方法。所以,fb(val)
等价于fb.Invoke(val)
。所以,if (null != fb) fb(i);
可以简写为fb?.Invoke(i);
17.5用委托回调多个方法(委托链)
internal static class GetInvokeList // 得到调用列表(全部以实例方法为例)
{
internal sealed class Fan
{
public string Speed()
{
throw new InvalidOperationException("The fan broke due to overheating.");
}
}
internal sealed class Speaker
{
public string Volume()
{
return "The volume is loud.";
}
}
internal delegate string GetStatus(); // 声明一个委托
public static void Go()
{
GetStatus getStatus = null; // 声明一个委托链
getStatus += new GetStatus(new Fan().Speed);
getStatus += new GetStatus(new Speaker().Volume);
Console.WriteLine(GetComponentStatusReport(getStatus));
}
private static string GetComponentStatusReport(GetStatus getStatus)
{
if (null == getStatus) return "";
Delegate[] delegatesArray = getStatus.GetInvocationList();
StringBuilder report = new StringBuilder();
foreach (GetStatus status in delegatesArray)
{
try
{
report.AppendFormat("{0}{1}{1}", status(), Environment.NewLine);
}
catch (InvalidOperationException e)
{
Object component = status.Target; // status是委托,status.Target引用的是回调方法要操作的对象
report.AppendFormat("Failed to get status from {1}{2}{0} Error: {3}{0}", Environment.NewLine,
(component == null) ? "" :component.GetType() + ".", status.GetMethodInfo().Name,
e.Message); // status.GetMethodInfo()返回指定委托所表示的方法
}
}
return report.ToString();
}
}
17.6委托定义不要太多(泛型委托)
.NET框架提供了17个Action委托供用户选择,从0个类型参数到最多16个类型参数;还提供了17个Func委托以允许回调方法返回值。
17.7C#为委托提供的简化语法
1.可以不定义回调方法(用lambda表达式实现匿名方法)
lambda表达式操作符是=>,编译器看到=>就会自动定义一个新的私有方法,该方法称为匿名函数。=>左侧是参数名称;=>右侧是函数主体,被放入编译器生成的匿名函数中。
=>左侧的规定:
Func<String> f1 = () => "jump"; // 委托不获取任何参数时
Func<int, string> f2 = n => n.ToString(); // 委托是1个参数时,可以不加参数的括号
// 两个int是方法的输入类型,一个string是方法的输出类型
Func<int, int, string> f3 = (int n1, int n2) => (n1 + n2).ToString(); // 委托获取1个或多个参数时,可显示指定类型
Func<int, int, string> f4 = (n1, n2) => (n1 + n2).ToString(); // 委托获取1个或多个参数时,编译器也可推断类型
=>右侧的规定:
主体只有一个语句就可以不加大括号,否则就要加。如果委托还期待返回值,还得在主体中加return语句,且必须加大括号。
2.不需要构造委托对象
lambda表达式 替代 委托对象 举例:
String[] names = { "Jeff", "Kristin", "Aidan" };
Char charToFind = 'i';
names = Array.FindAll(names, (String name) => { return (name.IndexOf(charToFind) >= 0); });
names = Array.ConvertAll<String, String>(names, (String name) => { return name.ToUpper(); });
Array.Sort(names, (String name1, String name2) => { return String.Compare(name1, name2); });
Array.ForEach(names, (String name) => { Console.WriteLine(name); });
输出:
AIDAN
KRISTIN
补充:不是所有匿名函数的的表达方式都叫做lambda表达式,Array.ForEach(names, delegate (String name) { Console.WriteLine(name); });
这个写法也叫匿名方法,在C# 2.0中引入,现在已经被lambda表达式这种写法淘汰。
17.8委托和反射
internal delegate Object TwoInt(int num1, int num2);
internal delegate Object OneStr(string str);
internal static class DelegateReflectionTest
{
public static void Go(params string[] args) // 用params实现数量可变的参数
{
if (args.Length <= 2)
{
Console.WriteLine("error:args count is wrong.");
return;
}
Type delegateType = Type.GetType(args[0]); // 第一个参数是委托类型
if (null == delegateType)
{
Console.WriteLine("Invalid delegateType arg: " + args[0]);
return;
}
Delegate myDelegate;
try
{
// 第二个参数是方法名,反射,Type.GetTypeInfo()返回表示类类型的类型声明,mi是引用了回调方法的MethodInfo对象
// GetDeclaredMethod:获取类中声明的所有方法(从父类继承的不算),包括public、protected和private修饰的方法
// GetMethod:获取当前类和父类的所有public的方法
MethodInfo methodInfo = typeof(DelegateReflectionTest).GetTypeInfo().GetDeclaredMethod(args[1]);
// 编译时,在不知道委托的所有必要信息的前提下,根据第一个参数(委托类型)来创建委托对象
myDelegate = methodInfo.CreateDelegate(delegateType);
}
// ArgumentNullException来自于GetDeclaredMethod方法,而ArgumentNullException是ArgumentException的子类
catch (ArgumentException)
{
Console.WriteLine("Invalid methodName argument: " + args[1]);
return;
}
// 要传给方法的参数
Object[] callbackArgs = new Object[args.Length - 2];
if (myDelegate.GetType() == typeof(TwoInt))
{
try
{
for (int i = 2; i < args.Length; ++i)
callbackArgs[i - 2] = int.Parse(args[i]); // 字符串转化为整数
}
catch (FormatException)
{
Console.WriteLine("Params must be int.");
return;
}
}
if (myDelegate.GetType() == typeof(OneStr))
{
Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);
}
try
{
Object result = myDelegate.DynamicInvoke(callbackArgs);
Console.WriteLine("Resule = " + result);
}
catch(ArgumentException)
{
Console.WriteLine("Incorrect number of parameters specified.");
return;
}
}
private static Object Add(int num1,int num2)
{
return num1 + num2;
}
private static Object CharsLength(String s1)
{
return s1.Length;
}
}
输出:
error:args count is wrong.
Resule = 444
Resule = 11