返回

委托

认识委托

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方法时,发现试图合并nullfb1,在方法内部直接返回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);
....

建议使用这些自带的委托定义,减少系统中的类型数量,同时简化编码.

需要自定义委托的情况

  • 使用refout关键字以传引用的方式传递参数.

    • 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#将该方法命名为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方法, 允许在 编译时不知道委托的所有必要信息 的前提下创建委托.

MethodInfoCreateDelegate方法定义的重载:

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.DelegateDynamicInvoke方法允许调用委托对象的回调方法, 传递一组在 运行时确定的参数.调用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);
Licensed under CC BY-NC-SA 4.0
0