<NET CLR via c# 第4版>笔记 第17章 委托

17.1 初识委托

  • .net 通过委托来提供回调函数机制.
  • 委托确保回调方法是类型安全的.
  • 委托允许顺序调用多个方法.

17.2 用委托回调静态方法

  • 将方法绑定到委托时,C# 和 CLR 都允许引用类型的 协变性(covariance)逆变性(contravariance).
  • 协变性是指方法能返回从委托的返回类型派生的一个类型.
  • 逆变性是方法获取的参数可以是委托的参数类型的基类.
  • 只有引用类型才支持协变性与逆变性,值类型或 void 不支持
    delegate object MyCallback(FileStream s);

    //可以应用于委托 MyCallback
    string SomeMethod(Stream s);

    //返回值是值类型,不能应用于委托 MyCallback
    int SomeOtherMethod(Stream s)

17.3 用委托回调实例方法

17.4 委托揭秘

  • 编译器看到委托后,会生成一个同名的类,并继承于 System.MulticastDelegate
  • System.MulticastDelegate 派生自 System.Delegate
  • System.MulticastDelegate的三个重要的非公共字段:
    • target System.Object 当委托对象包装一个静态方法时,这个字段为 null .当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象.
    • methodPtr System.IntPtr 一个内部的整数值, CLR 用它标识要回调的方法.
    • invocationList System.Object 该字段通常为 null, 构造委托链时它引用一个委托数组.

17.5 用委托回调多个方法( 委托链 )

  • 使用 Delegate 类的公共静态方法 Combine 将委托添加到链中:
    fbChain=(Feedback) Delegate.Combine(fbChain, fb1);
  • 使用 Delegate 类的公共静态方法 Remove 从链中删除委托. 每次 Remove 方法调用只能从链中删除一个委托.
    fbChain=(Feedback) Delegate.Combine(fbChain, fb1);
  • 委托public delegate int Feedback(int value)Invoke 的伪代码:
    public int Invoke(int value) {
        int result;
        Delegate[] delegateSet = _invocationList as Delegate[];
        if (delegateSet != null)
        {
            // 这个委托数组指定了应该调用的委托
            foreach (Feedback d in delegateSet)
                result = d(value);  //调用每个委托
        }
        else {  //否则就不是委托链
            //该委托标识了要回调的单个方法,
            //在指定的目标对象上调用这个回调方法
            result = _methodPtr.Invoke(_target,value);
            //上面这行代码接近实际的代码
            //实际发生的事情用C#是表示不出来的
        }
        return result;
    }

17.5.1 C# 对委托链的支持

  • c#编译器自动为委托类型的实例重载了+=和-=操作符.这些操作符分别调用 Delegate.CombineDelegate.Remove.
    fbChain += fb1;
    fbChain += fb2;
    fbChain -= fb1;

17.5.2 取得对委托链调用的更多控制

  • 可以通过实例方法 GetInvocationList 获取委托链中委托的集合,然后通过自定义算法,显式调用这些委托.

17.6 委托定义不要太多( 泛型委托 )

  • 尽量使用 Action<T>Func<T> 委托.需要使用 ref ,out ,params 关键字的地方除外.

17.7 C# 为委托提供的简化语法

  • 后面描述的这些只是 C# 的语法糖.

17.7.1 简化语法1: 不需要构造委托对象

  • 由于c#编译器能自己进行推断,所以可以省略构造 ThreadPool.QueueUserWorkItem 方法中 WaitCallback 委托对象的代码.
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(SomeAsyncTask, 5);
    }

    private static void SomeAsyncTask(object o) {
        Console.WriteLine(o);
    }

17.7.2 简化语法2: 不需要定义回调方法( lambda表达式 )

  • C# 允许使用 Lambda 表达式写回调代码,如:
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(obj =>Console.WriteLine(obj), 5);
    }
  • C# 编译器看到这个 lambda 表达式之后,会在类中自动定义一个新的私有方法,称为匿名函数,如下:
class Program
{
    //创建该私有字段是为了缓存委托对象,
    //优点: 不会每次调用都新建一个对象
    //缺点: 缓存的对象永远不会被垃圾回收
    [CompilerGenerated]
    private static WaitCallback <>9_xxxDelegate1;

    static void Main(string[] args)
    {
        if(<>9_xxxDelegate1==null)
            <>9_xxxDelegate1 = new WaitCallback(<Main>.b_0);

        ThreadPool.QueueUserWorkItem(<>9_xxxDelegate1,5);
    }

    [CompilerGenerated]
    private static void <Main>.b_0(object obj) {
        Console.WriteLine(obj);
    }
}
  • 如果调用方法不是静态的,但其内部的 匿名函数 不包含实例成员引用,编译器仍会生成静态匿名函数,因为它的效率比实例方法高; 但如果 匿名函数 的代码确实引用了实例成员,编译器就会生成非静态匿名函数.
  • 下面是一些 lambda 表达式的使用范例:
    //如果委托不获取任何参数,就使用()
    Func<string> f = () => "Jeff";

    //如果委托获取一个或更多参数,可显式指定类型
    Func<int, string> f2 = (int n) => n.ToString();
    Func<int, int, string> f3 = (int n1, int n2) => (n1 + n2).ToString();

    //如果委托获取一个或更多参数,编译器可推断类型
    Func<int, string> f4 = (n) => n.ToString();
    Func<int, int, string> f5 = (n1, n2) => (n1 + n2).ToString();

    //如果委托获取一个参数,可省略 ( 和 )
    Func<int, string> f6 = n => n.ToString();

    //如果委托有 ref/out 参数,必须显式指定 ref/out 和类型
    Bar b = (out int n) => n = 5;

    //Bar的定义
    delegate void Bar(out int z);
  • 如果主体由两个或多个语句构成,必须用大括号将语句封闭.在用了大括号的情况下,如果委托期待返回值,还必须在主体中添加 return 语句.

17.7.3 简化语法3: 局部变量不需要手动包装到类中即可传给回调方法

示例代码:

    static void Main(string[] args)
    {
        //一些局部变量
        int numToDo = 20;
        int[] squares = new int[numToDo];
        AutoResetEvent done = new AutoResetEvent(false);
        //在其他线程上执行一系列任务
        for (int n = 0; n < squares.Length; n++)
        {
            ThreadPool.QueueUserWorkItem(obj =>
            {
                int num = (int)obj;
                //该任务通常更耗时
                squares[num] = num * num;
                //如果这是最后一个任务,就让主线程继续运行
                if (Interlocked.Decrement(ref numToDo) == 0)
                    done.Set();
            }, n);
        }

        done.WaitOne();

        for (int n = 0; n < squares.Length; n++)
        {
            Console.WriteLine("Index {0} ,Square={1}", n, squares[n]);
        }
    }
  • 对于上面代码,C#编译器会定义一个新的辅助类,这个类要为打算传给 回调代码 的每个值都定义一个字段.
  • 此外,回调代码 还必须定义成辅助类中的实例方法.
    C#编译器会像下面这样重写代码:
    class Program
    {
        static void Main(string[] args)
        {
            //一些局部变量
            int numToDo = 20;
            WaitCallback callback1 = null;

            //构造辅助类的实例
            <>c_DisplayClass2 class1 = new <>c_DisplayClass2();

            //初始化辅助类的字段
            class1.numToDo = numToDo;
            class1.squares = new int[class1.numToDo];
            class1.done = new AutoResetEvent(false);

            //在其他线程上执行一系列任务
            for (int n = 0; n < class1.squares.Length; n++) {
                if (callback1 == null) {
                    //新建的委托对象绑定到辅助对象及其匿名实例方法
                    callback1 = new WaitCallback(class1.<Main>b_0);
                }

                ThreadPool.QueueUserWorkItem(callback1,n);
            }

            //等待其他所有线程结束运行
            class1.done.WaitOne();

            //显示结果
            for (int n = 0; n < class1.squares.Length; n++) {
                Console.WriteLine("Index {0} ,Square={1}", n, class1.squares[n]);
            }
        }

        //为避免冲突,辅助类被指定了一个奇怪的名称.
        //而且被指定为私有的,禁止从Program类外部访问
        [CompilerGenerated]
        private sealed class <>c_DisplayClass2:Object{
            //回调代码要使用的每个局部变量都有一个对应的公共字段
            public int[] squares;
            public int numToDo;
            public AutoResetEvent done;

            //公共无参构造器
            public <>c_DisplayClass2{}
            
            //包含回调代码的公共实例方法
            public void <Main>b_0(object obj) {
                int num = (int)obj;
                squares[num] = num * num;
                if (Interlocked.Decrement(ref numToDo) == 0)
                    done.Set();
            }
        }
    }
  • 作者给自己定了个规则:如果需要在回调方法中包含3行以上的代码,就不使用 lambda 表达式,而是手写一个方法,并为其分配自己的名称.

17.8 委托和反射

可以通过 System.Reflection.MethodInfo 提供的 CreateDelegate 方法,创建一个 Delegate 对象,然后调用该对象的 DynamicInvoke 方法,以实现在运行时动态调用委托.

//下面是一些不同的委托定义
internal delegate object TwoInt32s(int n1, int n2);
internal delegate object OneString(String s1);
static class Program
{
    static void Main(string[] args)
    {
        //如果委托在命名空间下,第一个参数一定要是带命名空间的完全限定名
        //args = new[] { "TwoInt32s", "Add", "1", "11" };

        if (args.Length < 2)
        {
            Console.WriteLine("Usage: TwoInt32s Add 123 321");
            return;
        }

        //将delType实参转换为委托类型
        Type delType = Type.GetType(args[0]);
        if (delType == null)
        {
            Console.WriteLine("Invalid delType argument: " + args[0]);
            return;
        }

        Delegate d;
        try
        {
            //将Arg1实参转换为方法
            MethodInfo mi = typeof(Program).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 (int a = 2; a < args.Length; a++)
                    callbackArgs[a - 2] = int.Parse(args[a]);
            }
            catch (FormatException)
            {
                Console.WriteLine("Parameters must be integers.");
                return;
            }
        }
        if (d.GetType() == typeof(OneString))
        {
            //只复制String参数
            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.");
        }
    }


    private static object Add(int n1, int n2)
    {
        return n1 + n2;
    }
    private static object Subtract(int n1, int n2)
    {
        return n1 - n2;
    }
    private static object NumChars(string s1)
    {
        return s1.Length;
    }
    private static object Reverse(string s1)
    {
        return new String(s1.Reverse().ToArray());
    }
}
posted on 2017-08-21 10:50  Harry(悟秀)  阅读(359)  评论(0编辑  收藏  举报