认识委托
internal sealed class DelegateIntro
{
// 声明一个实例委托
// 该方法获取一个Int32参数,返回void
// Feedback 反馈,回复
internal delegate void Feedback(Int32 value);
public static void Go()
{
// 用委托回调静态方法
StaticDelegateDemo();
// 用委托回调实例方法
InstanceDelegateDemo();
ChainDelegateDemo1(new DelegateIntro());
ChainDelegateDemo2(new DelegateIntro());
}
/// <summary>
/// 用委托回调静态方法
/// </summary>
private static void StaticDelegateDemo()
{
Console.WriteLine("----- Static Delegate Demo -----");
Counter(1, 3, null);
// 委托对象是方法的包装器(wrapper),使方法能通过包装器来间接回调
// FeedbackToConsole方法作用就是向控制台输出字符
Counter(1, 3, new Feedback(FeedbackToConsole)); // DelegateIntro. 前缀可选
Counter(1, 3, new Feedback(FeedbackToMsgBox));
Console.WriteLine();
// 即使Counter在其他类中定义, 但是要求委托对象是具有足够安全性和可访问性代码创建的
// 通过委托来调用另一个类型的私有方法FeedbackToConsole也是没问题的
// 这个例子中的所有操作都是类型安全的,在构造Feedback委托对象时,
// 编译器确保FeedbackToConsole和FeedbackToMsgBox方法的签名都兼容于Feedback委托定义的签名
// 将方法绑定到委托时, CLR和C#都允许引用类型的协变性(out返回子类)和逆变性(in参数基类)
// 只有引用类型才支持逆变性和协变性,,值类型或void不支持
// --> 之所以不支持是因为值类型的存储结构是变化的,不像引用类型始终是一个指针
// delegate Object MyCallback(FileStream fs);
// 通过协变性和逆变性可以绑定以下方法
// String SomeMethod(Stream s);
}
private static void InstanceDelegateDemo()
{
Console.WriteLine("----- Instance Delegate Demo -----");
// 先要构造对象
DelegateIntro di = new DelegateIntro();
// 用委托对象包装一个实例方法
Counter(1, 3, new Feedback(di.FeedbackToFile));
Console.WriteLine();
}
private static void ChainDelegateDemo1(DelegateIntro di)
{
Console.WriteLine("----- Chain Delegate Demo 1 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(di.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
private static void ChainDelegateDemo2(DelegateIntro di)
{
Console.WriteLine("----- Chain Delegate Demo 2 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(di.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(1, 2, fbChain);
}
/// <summary>
/// 从from计数到to
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="fb"> Feedback委托对象的引用 </param>
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
// 遍历所有整数
for (Int32 val = from; val <= to; val++)
{
// 如果指定了任何回调,则调用它们
// 并将val值传给回调方法
if (fb != null)
fb(val);
}
}
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item=" + value);
}
private static void FeedbackToMsgBox(Int32 value)
{
MessageBox.Show("Item=" + value);
}
private void FeedbackToFile(Int32 value)
{
StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Item=" + value);
sw.Close();
}
}
委托对象是方法的包装器(wrapper),使方法能通过包装器来间接回调.
将方法绑定到委托时, CLR和C#都允许引用类型的协变性(out返回子类)和逆变性(in参数基类)
,只有引用类型才支持逆变性和协变性,值类型或void不支持(之所以不支持是因为值类型的存储结构是变化的,不像引用类型始终是一个指针)
delegate Object MyCallback(FileStream fs);
通过协变性和逆变性可以绑定以下方法String SomeMethod(Stream s);
委托揭秘
使用delegate
关键字定义, 用new
操作符构造委托实例,用委托对象的变量替代方法名调用回调函数.
实际上编译器和CLR在幕后做了大量工作来隐藏复杂性.
internal delegate void Feedback(Int32 value);
编译器会像下面这样定义一个完整的类:
internal class Feedback: System.MulticastDelegate
{
// 构造器
public Feedback(Object object, IntPtr method);
// 这个方法和源代码指定的原型一样
public virtual void Invoke(Int32 value);
// 以下方法实现了对回调方法的异步回调
public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, Object object);
public virtual void EndInvoke(IAsyncResult result);
}
所有委托都派生自MulticastDelegate
,MulticastDelegate
派生自Delegate
.
由于委托是类, 凡是能定义类的地方都能定义委托.
以下是MulticastDelegate
的三个重要的非公共字段:
- 所有委托都有一个
构造器
: 需要获取2个参数- 对象引用
- 引用了回调方法的整数(
_IntPtr
)
根据前面的源码, 传递的是p.FeedbackToFile
这样的值, C#编译器知道要构造的是委托, 所以会分析源代码来确定引用的是哪个对象和方法. 对象引用被传递给构造器的object参数
, 标识了一个特殊IntPtr值
(从MethodDef或MemberRef元数据token获得)被传给构造器的method
参数. 对于静态方法,会为object参数
传递null值
.
所以每个委托对象都是一个包装器,其中包装了:
- 一个方法.
- 调用该方法时要操作的对象.
Feedback fbStatic = new Feedback(Program.FeedbackToConsole);
Feedback fbInstance = new Feedback(new Program.FeedbackToFile());
以上是委托对象如何构造和内部结构,再来看回调方法如何调用.
/// <summary>
/// 从from计数到to
/// </summary>
/// <param name="fb"> Feedback委托对象的引用 </param>
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
// 遍历所有整数
for (Int32 val = from; val <= to; val++)
{
// 如果指定了任何回调,则调用它们
// 并将val值传给回调方法
if (fb != null)
fb(val);// 可以写成显式调用 fb.Invoke(val);
}
}
fb的null检查必不可少, fb(val);
看上去是调用了方法并传递了一个参数,实际上没有名为fb的函数,而是编译器知道fb是引用了委托对象的变量,所以会生成代码调用该委托对象Invoke方法.生成的代码是fb.Invoke(val);
, 所以也可以显式的调用Invoke方法.
Invoke方法
被调用时,会使用私有字段_target
和_methodPtr
在指定的私有对象上调用包装好的回调方法.
Invoke方法的签名和委托的签名匹配.
用委托回调多个方法(委托链)
委托链
是委托对象
的集合.
private static void ChainDelegateDemo1(DelegateIntro di)
{
Console.WriteLine("----- Chain Delegate Demo 1 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(di.FeedbackToFile);
// 此变量委托变量用来引用委托链
Feedback fbChain = null;
// Delegate.Combine方法将公共静态方法FeedbackToConsole委托添加到委托链中
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
Delegate.Combine的过程
在第一次执行Delegate.Combine
方法时,发现试图合并null
和fb1
,在方法内部直接返回fb1
中的值,所以当前fbChain
变量引用fb1
变量所引用的委托对象.
第二次执行Delegate.Combine
方法时,在内部,发现fbChain
已经引用了一个委托对象,所以Combine会构造一个新的委托对象. 新的委托对象对它的私有字段_target
和_methodPtr
进行初始化. _invocationList
字段被初始化为引用一个委托对象的数组, 数组的第一个索引0是FeedbackToConsole
方法的委托(也就是fbChain
当前的引用的委托). 最后fbChain
被设为引用新建的委托对象.
注意之前新建的委托及其
_invocationList
会进行垃圾回收.
伪代码的形式,Feedback的Invoke方法基本上是像下面这样实现的:
public void Invoke(Int32 value)
{
Delegate[] delegateSet = _invocationList as Delegatep[];
if(delegateSet != null)
{
foreach(Feedback d in delegateSet)
d(value); // 调用每个委托
}else // 否则就不是委托链
{
// 该委托标识了要回调的当个方法
// 在指定的目标对象上调用这个回调方法
_methodPtr.Invoke(_target, value);
// 上面这行代码接近实际的代码,实际发生的事情用C#表示不出来的
}
}
Delegate.Remove的过程
Remove方法被调用时,它扫描fbChain
所引用的委托对象内部维护的委托数组(从末尾向索引0扫描).
查找的是其_target
和_methodPtr
字段与第二个实参中的字段匹配的委托,如果匹配到,并且在删除后数组中剩余一个数据项, 就返回那个数据项. 如果还有多个数据项,就新建一个委托对象, 创建并初始化_invocationList
引用剩余的数据项, 返回对这个新建委托对象的引用.
有返回值的委托
public delegate Feedback(Int32 value);
public void Invoke(Int32 value)
{
Int32 result;
Delegate[] delegateSet = _invocationList as Delegatep[];
if(delegateSet != null)
{
foreach(Feedback d in delegateSet)
result = d(value); // 调用每个委托
}else // 否则就不是委托链
{
// 该委托标识了要回调的当个方法
// 在指定的目标对象上调用这个回调方法
result = _methodPtr.Invoke(_target, value);
// 上面这行代码接近实际的代码,实际发生的事情用C#表示不出来的
}
return result;
}
返回值被保存到result变量中, 但是result变量只包含调用最后一个委托的结果(前面的返回值会被丢弃). result返回给调用Invoke的代码.
C#对委托链的支持(提供了+=和-=操作符)
C#编译器自动为委托类型的实例重载了+=
和-=
操作符.
+=
调用了Delegate.Combine
-=
调用了Delegate.Remove
生成的IL代码是一样的.
获取对委托链调用的更多控制
上述Invoke调用算法是依次遍历, 有 局限性:
- 除了最后一个返回值,其他所有回调方法的返回值都会被丢弃
- 如果被调用的委托中有一个抛出异常或者阻塞,链中后续的所有对象都调用不了.
这种时候Invoke里的算法就不胜其任, 所以MulticastDelegate类
提供了一个实例方法GetInvocationList
,用于显式调用链中的每一个委托,并允许你使用需要的任何算法:
public abstract class MulticastDelegate : Delegate
{
// 创建一个委托数组,其中每个元素都引用链中的一个委托
public sealed override Delegate[] GetInvocationList();
}
GetInvocationList
方法操作从MulticastDelegate
派生的对象, 返回包含Delegate引用
的一个数组. 在内部,GetInvocationList
构造并初始化一个数组,让它的每一个元素都引用链中的一个委托. 如果_invocationList
字段为null,返回的数组就只有一个元素,该元素引用链中唯一的委托(委托实例本身).
internal static class GetInvocationList
{
// 定义一个灯组件
internal sealed class Light
{
// 返回灯的状态
public String SwitchPosition()
{
return "灯光熄灭了";
}
}
// 定义一个风扇组件
internal sealed class Fan
{
// 返回风扇状态
public String Speed()
{
throw new InvalidOperationException("风扇过热而坏了");
}
}
// 定义一个扬声器
internal sealed class Speaker
{
// 返回扬声器的状态
public String Volume()
{
return "音量太大了";
}
}
// 定义一个委托查询组件的状态
private delegate String GetStatus();
public static void Go()
{
// 声明空的委托链
GetStatus getStatus = null;
// 构造3个组件,将他们的状态方法添加进委托链
getStatus += new GetStatus(new Light().SwitchPosition);
getStatus += new GetStatus(new Fan().Speed);
getStatus += new GetStatus(new Speaker().Volume);
// 显示整理好的状态报告,反应这三个组件的状态
Console.WriteLine(GetComponentStatusReport(getStatus));
}
// 查询组件并返回状态报告
private static String GetComponentStatusReport(GetStatus status)
{
// 如果委托对象为空,不执行任何操作
// 必要的null检查
if (status == null) return null;
// 使用StringBuilder来构造字符串,因为要添加删减
StringBuilder report = new StringBuilder();
// 将委托链中的委托放入数组
Delegate[] arrayOfDelegates = status.GetInvocationList();
// 遍历数组中的每一个委托
foreach (GetStatus getStatus in arrayOfDelegates)
{
try
{
// 获取组件的状态并附加在报告中
report.AppendFormat("{0}{1}{1}", getStatus(), Environment.NewLine);
}
// 如果有组件有错误异常
catch (InvalidOperationException e)
{
// (property) object System.Delegate.Target 获取类实例,当前委托将对其调用实例方法。
Object component = getStatus.Target;
// component.GetType() --> Fan
// getStatus.GetMethodInfo().Name --> Speed
report.AppendFormat(
"无法从 {1}{2}{0} 获得状态 错误: {3}{0}{0}",
Environment.NewLine,
((component == null) ? "" : component.GetType() + "."),
getStatus.GetMethodInfo().Name, e.Message);
}
}
// 把整理好的报告返回给调用者
// 灯光熄灭了
//
// 无法从 Fan.Speed 获得状态
// 错误 : 风扇过热而坏了
//
// 音量太大了
return report.ToString();
}
}
委托定义不要太多(委托泛型)
定义的委托 如果返回值和参数类型相同,就是一样的. 不需要重复定义不同名称的委托.
实例化该委托的多个实例,就可以实现一样的效果
现在,.NET Framewoke现在支持泛型,所以实际上只需要几个泛型委托就可以表示获取多达16个参数的方法:
// 无返回值的委托
public delegate void Action(); //这不是泛型
public delegate void Action<T>(T obj);
public delegate void Action<T1,T2>(T1 obj1,T2 obj2);
public delegate void Action<T1,T2,T3>(T1 obj1,T2 obj2,T3 obj3);
....
// 有返回值的委托
public delegate TResult Func<TResult>();
public delegate TResult Func<T,TResult>(T1 arg);
public delegate TResult Func<T1,T2,TResult>(T1 arg1,T2 arg2);
....
建议使用这些自带的委托定义,减少系统中的类型数量,同时简化编码.
需要自定义委托的情况
- 使用
ref
或out
关键字以传引用的方式传递参数.delegate void Bar(ref Int32 z);
- ref标记:调用方法前 必须先初始化被ref标记的参数, 被调用的方法 可以读写这个参数值.
- out标记:被传入调用方法的参数 不必要初始化, 被调用的方法不能读取参数值, 在返回前
必须向这个值写入.
- 需要委托通过C#的params关键字获取数量可变的参数
- 要为委托的任何参数指定默认值
- 对委托的泛型类型参数进行约束
要获取泛型实参并有返回值的委托支持逆变和协变.
“协变”->”和谐的变”->”很自然的变化”->string->object :协变。
“逆变”->”逆常的变”->”不正常的变化”->object->string :逆变。
C#为委托提供的简化语法
多开发人员认为和委托打交道很麻烦。因为它的语法很奇怪。例如以下代码:
// 向按钮空间登记button1_Click的地址,以便在按钮被单击时调用方法
button1.Click += new EventHandle(button1_Click);
// 其中的button1_Click是一个方法,它看起来像下面这样:
void button1_Click(Object sender, EventArgs e)
{
// 按钮单击后要做的事情....
}
仅仅为了指定button1_Click方法的地址,就要构造一个EventHandle委托对象,有点麻烦. 然而这时CLR要求的, 因为这个对象提供一个包装器,可确保被包装的方法只能以类型安全的方式调用. 这个包装器还支持调用实例方法和委托链. 所以更多程序喜欢像button1.Click += button1_Click;
这样写. C#提供的语法糖为程序员提供了一种更简单的方式生成CLR处理委托时所必须的IL代码.
简化语法1: 不需要构造委托对象
由于C#编译器能自己进行推断,所以可以省略构造WaitCallback委托对象的代码,但是实际上,C#编译器还是会生成IL代码来新建WaitCallback委托对象, 只是语法上进行了简化 .
public sealed class AClass
{
private static void CallbackWithoutNewingADelegateObject()
{
// QueueUserWorkItem 需要一个WaitCallback委托对象引用
// 由于C#编译器能自己进行推断,所以可以省略构造WaitCallback委托对象的代码
// 但是实际上,C#编译器还是会生成IL代码来新建WaitCallback委托对象
// 只是语法上进行了简化
ThreadPool.QueueUserWorkItem(SomeAsyncTask,5);
}
// 要和 public delegate void WaitCallback(object state);定义一致
private static void SomeAsyncTask(Object o)
{
Console.WriteLine(o);
}
}
简化语法2: 不需要定义回调方法(lambda表达式)
C#允许以内联(直接嵌入)的方式写回调方法的代码,不必在它自己的方法写.
private static void CallbackWithoutNewingADelegateObject()
{
// lambda表达式
ThreadPool.QueueUserWorkItem(obj => Console.WriteLine(obj),5);
}
lambda表达式可以在编译器预计会看到一个委托的地方使用. 编译器在看到这个lambda表达式之后,会在本类中自动定义一个新的私有方法, 这个新方法称为 匿名函数. 因为你不知道这个函数的名称,但可以利用ILDasm.exe看到C#将该方法命名为<CallbackWithoutNewingADelegateObject>b_0
,它获取一个Object返回void.
CLR能用<
符号开头命名,C#则不允许标识符包含<
, 这就不会让匿名函数和你的函数命名重复. 但是可以通过反射来访问方法,但是C#语言规范指出,编译器生成名称的方式是没有任何保证的,每次编译代码可能生成一个不同的名称.
C#编译器向匿名函数应用了ComplierGeneratedAttribute
特性, =>
操作符右侧的代码被放入编译器生成的方法中.
写lambda表达式时没有办法向编译器生成的方法应用定制特性. 不能向方法应用任何修饰符比如unsafe,这一般不会有什么问题,因为编译器生成的方法总是私有方法,要么是静态的要么是非静态的, 这取决于方法是否访问了任何实例成员. 没必要向方法应用public,property,internal,virtual,sealed,override,abstract之类的修饰符.
以下是C#编译器改写上段代码的结果:
internal sealed class AClass
{
// 创建该私有字段视是为了缓存委托对象
// 优点: CachedAnonymousMethodDelegatel不会每次调用都新建一个对象
// 缓存的对象永远不会被垃圾回收
[ComplierGenerated]
private static WaitCallback <>9_CachedAnonymousMethodDelegatel;
private static void CallbackWithoutNewingADelegateObject()
{
if(<>9_CachedAnonymousMethodDelegatel != null)
{
// 第一次调用时,创建委托对象并缓存它
<>9_CachedAnonymousMethodDelegatel = new WaitCallback(<CachedAnonymousMethodDelegatel>b__0);
}
ThreadPool.QueueUserWorkItem(<>9_CachedAnonymousMethodDelegatel,5);
}
[ComplierGenerated]
private static void <CachedAnonymousMethodDelegatel>b__0(Object obj)
{
Console.WriteLine(obj);
}
}
lambda表达式必须匹配WaitCallback委托: 获取Object并返回void.
- 匿名函数被标记为private,禁止非类型内定义的代码访问(反射能揭示出方法确实存在).
- 匿名函数被标记为static, 因为代码没有访问任何实例成员.
- 代码可以引用类中定义的任何静态字段或静态方法.
- 如果CallbackWithoutNewingADelegateObject本身不是静态的,匿名函数则可以包含对实例成员的引用.
- 如果没有引用,编译器还是会生成静态匿名函数,因为它的效率高于实例方法. 因为不需要额外的this参数.
lambda表达式的一些规则
=>
操作符左侧是指定传给lambda表达式的 参数的名称.
// 如果委托不获取任何参数, 就是用()
Func<String> f = () => "Jeff";
// 如果委托获取1个或更多参数,可显式指定类型
Func<Int32,String> f2 = (Int32 n) => n.ToString();
Func<Int32,Int32,String> f3 = (Int32 n1, Int32 n2)=>(n1 + n2).ToString();
// 如果委托获取1个或更多参数,编译器可推断类型
Func<Int32,String> f4 = (n) => n.ToString();
Func<Int32,Int32,String> f5 = (n1,n2)=>(n1 + n2).ToString();
// 如果委托获取1个参数, 可以省略()
Func<Int32,String> f6 = n => n.ToString();
// 如果委托有ref/out参数, 必须显示指定ref/out和类型
// Bar定义 delegate void Bar(out Int32 z);
Bar b = (out Int32 n) => n = 5;
// 如果 =>右侧主题由两个或多个语句构成, 必须用大括号将语句封闭.
// 并且,如果委托期待返回值,还必须在主体中添加return语句
Func<Int32,Int32,String> f7 = (n1,n2) => { Int32 sum = n1 + n2; return sum.ToString(); };
lambda表达式的主要优势在于, 它从你的源码中移除了一个间接层
, 可以说是避免了迂回. 正常情况下必须写一个单独的方法, 命名该方法, 再在需要委托的地方传递这个方法名. 如果要在多个地方引用同一个代码主体, 单独写一个方法并命名确实是理想的方案.但如果只需要引用一次,那么用lambda表达式直接内联代码,不必为它分配名称,从而提高编程效率.
C# 2.0 引入的匿名方法功能 和C# 3.0引入的lambda表达式相似.
- 匿名方法描述的也是创建匿名函数的语法.
- 新规范建议开发人员使用新的lambda表达式语法,而不要使用旧的匿名方法语法.
- lambda表达式更简洁,代码更容易写,读和维护.
简化语法3: 局部变量不需要手动包装到类中即可传给回调方法
前面展示了回调代码如何引用类中定义的其他成员. 但是有时还希望回调代码引用存在于定义方法中的局部参数或变量.
internal sealed class AClass2
{
internal static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo)
{
// 两个局部变量
Int32[] squares = new Int32[numToDo];
AutoResetEvent done = new AutoResetEvent(false);
// 在其它线程上执行一系列任务
for (Int32 n = 0; n < squares.Length; n++)
{
ThreadPool.QueueUserWorkItem(
delegate(Object obj) // 可以写成 obj =>
{
Int32 num = (Int32)obj;
// 耗时任务
squares[num] = num * num;
// 如果是最后一个任务,则让主线程继续执行
if (Interlocked.Decrement(ref numToDo) == 0)
done.Set();
}, n);
}
// 等待其他所有线程执行完毕
done.WaitOne();
// 显示任务
for (Int32 n = 0; n < squares.Length; n++)
Console.WriteLine("Index {0}, Square={1}", n, squares[n]);
}
}
lambda表达式中的方法是在一个单独的方法中(CLR要求), 方法变量是如何传给这个单独的方法呢?
唯一的办法就是定义一个新的辅助类,
- 这个类要打算传给回调代码的每个值都定义一个字段.
- 此外回调代码还必须定义成辅助类中的实例方法,
UsingLocalVariablesInTheCallbackCode
方法必须构造辅助类的实例,- 用方法定义的局部变量的值来初始化该实例中的字段,
- 然后, 构造绑定到辅助对象/实例方法的委托对象.
当lambda表达式造成编译器生成了一个类, 而且参数/局部变量被转移到该类的字段后, 变量引用的对象的生存期被延长了, 正确情况下, 在方法中最后一次使用
参数/局部变量
之后,这个参数/局部变量
就会离开作用域,结束其生命周期, 但是,将变量变为字段后,只要包含字段的那个对象不死, 字段引用的对象也不会死,这个问题在大部分情况下不是大问题, 有时候需要注意一下.
上述代码,由C#自动改为完整代码(详细化语法糖):
C#的lambda表达式功能很容易被滥用, 使调试和单步执行变得比较有挑战性. 此书作者为自己设定了一个规则:
- 如果需要回调方法中包含3行以上的代码,就不使用lambda表达式, 建议手写一个方法
// 创建并初始化一个String数组
String[] names = {"Jeff", "Kristin", "Aidan"};
// 只获取含有小写字母a的名字
Char charToFind = 'i';
// 写法1
names = Array.FindAll(names, delegate(String name) { return (name.IndexOf(charToFind) >= 0); });
// 写法2
names = Array.FindAll(names, name => name.IndexOf(charToFind) >= 0);
// 将每个字符串的字符转换为大写
// 写法1
names = Array.ConvertAll<String, String>(names, delegate(String name) { return name.ToUpper(); });
// 写法2
names = Array.ConvertAll<String, String>(names, name => name.ToUpper());
// 排序名字
// 写法1
Array.Sort(names, delegate(String name1, String name2) { return String.Compare(name1, name2); });
// 写法2
Array.Sort(names, (String name1, String name2) => String.Compare(name1, name2));
// 显示结果
// 写法1
Array.ForEach(names, delegate(String name) { Console.WriteLine(name); });
// 写法2
Array.ForEach(names, Console.WriteLine);
委托和反射
目前使用委托要求开发人员事先知道回调方法的原型(要知道回调方法有多少个参数,以及具体类型). 因此System.Delegate.MethodInfo
提供了CreateDelegate
方法, 允许在 编译时不知道委托的所有必要信息 的前提下创建委托.
MethodInfo
为CreateDelegate
方法定义的重载:
public abstract class MethodInfo : MethodBase
{
// 构造包装了一个静态方法的委托
public virtual Delegate CreateDelegate(Type delegateType);
// 构造包装了一个实例方法的委托: target引用this实参
public virtual Delegate CreateDelegate(Type delegateType, Object target);
}
// 创建好委托后,用Delegate的DynamicInvoke方法调用它
public abstract class Delegate
{
// 调用委托并传递参数
public Object DynamicInvoke(param Object[] args);
}
使用反射API,首先
- 必须获取引用了回调方法的一个
MethodInfo
对象, - 然后调用
CreateDelegate
方法来构造一个由Delegate
派生类型的对象- 如果委托包含的是实例方法, 还要传递一个target参数, 指定作为this参数传给实例方法的对象.
System.Delegate
的DynamicInvoke
方法允许调用委托对象的回调方法, 传递一组在 运行时确定的参数.调用DynamicInvoke
时,它会在内部保证传递的参数与回调方法期望的参数兼容.
- 如果兼容就调用回调方法.
- 否则抛出异常
ArgumentException
class Program
{
public static void Main(string[] args)
{
// 第一个参数是委托类型 委托类型的String是 "Class+DelegateName" 例如 DelegateReflection+TwoInt32s
// 第二个参数是需要回调的方法 此名称方法要和对应委托匹配参数类型和个数
// 之后的参数时传递给回调方法的参数
DelegateReflection.Go(typeof(DelegateReflection.TwoInt32s).ToString(),"Add","123","321");
}
}
internal static class DelegateReflection
{
// 定义2个委托
internal delegate Object TwoInt32s(Int32 n1, Int32 n2);
internal delegate Object OneString(String s1);
public static void Go(params String[] args)
{
if (args.Length < 2)
{
// Usage:
// delType methodName [Arg1] [Arg2]
// where delType must be TwoInt32s or OneString
// if delType is TwoInt32s, methodName must be Add or Subtract
// if delType is OneString, methodName must be NumChars or Reverse
//
// Examples:
// TwoInt32s Add 123 321
// TwoInt32s Subtract 123 321
// OneString NumChars "Hello there"
// OneString Reverse "Hello there"
return;
}
// 将实参转化为委托类型 (委托是一个类)
Type delType = Type.GetType(args[0]);
if (delType == null)
{
Console.WriteLine("Invalid delType argument: " + args[0]);
return;
}
Delegate d;
try
{
// 将args[1]转换为方法
MethodInfo mi = typeof(DelegateReflection).GetTypeInfo().GetDeclaredMethod(args[1]);
// 创建包装了静态方法的委托对象
d = mi.CreateDelegate(delType);
}
catch (ArgumentException)
{
Console.WriteLine("Invalid methodName argument: " + args[1]);
return;
}
// 创建一个数组,其中包含要通过委托对象传给方法的参数
Object[] callbackArgs = new Object[args.Length - 2];
if (d.GetType() == typeof(TwoInt32s))
{
try
{
// 将String类型参数转换为Int32类型的参数
for (Int32 a = 2; a < args.Length; a++)
callbackArgs[a - 2] = Int32.Parse(args[a]);
}
catch (FormatException)
{
Console.WriteLine("Parameters must be integers.");
return;
}
}
if (d.GetType() == typeof(OneString))
{
// 只复制字符串
Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);
}
try
{
// 调用委托并显示结果
Object result = d.DynamicInvoke(callbackArgs);
Console.WriteLine("Result = " + result);
}
catch (TargetParameterCountException)
{
Console.WriteLine("Incorrect number of parameters specified.");
}
}
// 需要2个Int32参数的静态函数
private static Object Add(Int32 n1, Int32 n2)
{
return n1 + n2;
}
// 需要2个Int32参数的静态函数
private static Object Subtract(Int32 n1, Int32 n2)
{
return n1 - n2;
}
// 需要1个String参数的静态函数
private static Object NumChars(String s1)
{
return s1.Length;
}
// 需要1个String参数的静态函数
private static Object Reverse(String s1)
{
return new String(s1.Reverse().ToArray());
}
}
以上示例有几个注意的地方:
// 1.将实参转化为委托类型 (委托是一个类)
Type delType = Type.GetType(args[0]);
// 2.先获取当前回调方法所在的类,反射获得TypeInfo,再根据参数名称获取对应的方法.
MethodInfo mi = typeof(DelegateReflection).GetTypeInfo().GetDeclaredMethod(args[1]);
// 3.创建包含回调静态方法的委托对象
d = mi.CreateDelegate(delType);
// 4.用if-else判断委托类型是哪个,根据对应的委托,解析对应的参数
if (d.GetType() == typeof(OneString)) ...
callbackArgs[..] = Int32.Parse(args[a]);...
Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);....
// 5. 调用委托传入参数
Object result = d.DynamicInvoke(callbackArgs);