C#教程 – 委托类型(Delegate Type)


更新记录
转载请注明出处:https://www.cnblogs.com/cqpanda/p/16690892.html
2022年9月16日 发布。
2022年9月10日 从笔记迁移到博客。

Delegate Type说明

Delegate实例是一个对象,是一种可调用的对象

Delegate类型定义委托实例可以调用的方法类型

通过调用委托类型的实例,委托类型实例再调用对应的目标方法

委托调用相当于间接(indirection)调用

委托类型和类类型一样是一种用户自定义的类型

可以认为委托是持有一个或多个方法的对象

但委托对象是可以执行的,即执行委托对象持有的方法

委托类似C/C++中的函数指针(Function Pointers)的概念,但更加安全更好调试

委托类似JavaScript中回调函数(Callback)的概念

委托类似是一种可以指向方法的引用类型,可以理解为一种函数指针,是类型安全的

委托可以是泛型的

Delegate结构图

image

Delegate常见使用场景

Delegates are especially used for implementing events and the call-back methods.

Delegate缺点

Delegate会导致紧耦合

Delegate使用不当会导致可读性下降、Debug难度增加

Delegate、异步调用、多线程混合后,导致代码难以维护

Delegate使用不当会导致内存泄漏和性能下降

将方法当作变量使用

实例:委托与插件方法(Delegates with Plug-in Methods)

可以把方法做成像变量一样调用

public delegate int Transformer (int x);
class Util
{
    public static void Transform (int[] values, Transformer t)
    {
        for (int i = 0; i < values.Length; i++)
        values[i] = t (values[i]);
    }
}

class Test
{
    static void Main()
    {
        int[] values = { 1, 2, 3 };
        Util.Transform (values, Square); // Hook in the Square method
        foreach (int i in values)
        Console.Write (i + " "); // 1 4 9
    }
    static int Square (int x) => x * x;
}

委托类型与类类型对比

image

可以把委托看作一个包含有序方法列表的对象

方法的列表成为调用列表

委托保存的方法来自各种类或结构,方法需要与委托类型的签名和返回值匹配

调用列表中的方法可以是实例方法也可以是静态方法

在调用委托对象时,委托对象会执行调用列表中所有方法
image

使用委托的步骤

声明一个委托类型

声明一个委托类型的变量

实例化该委托类型的变量,然后给它赋值

继续赋值

调用执行委托

声明委托类型

声明委托类型
image

提示:声明委托类型类似声明一个方法,即:delegate + 方法签名
image

注意:不可以在方法内声明委托,只可以在全局声明或声明为类成员

创建委托对象

委托对象是引用类型,所以有引用和对象

声明委托变量

image

实例化委托变量

实例化委托变量,方法一:使用new
image

实例化委托变量,方法二:使用直接快捷赋值
image

声明并实例化委托变量

image

image

示意图:
image

委托实例引用的实例方法和静态方法(Instance Versus Static Method Targets)

委托实例使用Target属性 指向 其引用的实例方法的实例

注意:如果是静态方法,则Target为null

委托实例使用Method属性 指向 其引用的方法

实例:

public delegate void ProgressReporter (int percentComplete);
class X
{
 public void InstanceProgress (int percentComplete)
                => Console.WriteLine (percentComplete);
}
class Test
{
    static void Main()
    {
        X x = new X();
        ProgressReporter p = x.InstanceProgress;
        p(99); // 99
        Console.WriteLine (p.Target == x); // True
        Console.WriteLine (p.Method); // Void InstanceProgress(Int32)
    }
}

切换委托值

委托类型是引用类型,重新给委托赋值会改变委托变量中的引用

新委托值赋值给委托变量后,旧的委托值会被垃圾回收器回收
image

示意图:
image

多播委托(组合委托)(Multicast Delegate)

可以将多个委托组成一个委托成为组合委托

组合委托可以执行多个方法

所有委托实例都具有多播功能,所以可以引用多个方法

注意:

​ 组合委托并不修改原有的委托

实际上委托是恒定的,委托对象创建后不可以被修改

组合委托只是将多个委托进行组合在一起
image

image

为委托对象添加方法

为委托添加方法使用+=运算符
image

注意:委托必须先初始化再使用+=或-=

示意图:
image
实例:更新进度

//进度报告委托
public delegate void ProgressReporter (int percentComplete);

public class Util
{
    //具体进行的任务
    public static void HardWork (ProgressReporter p)
    {
        for (int i = 0; i < 10; i++)
        {
            p (i * 10); // 调用委托,报告进度
            // 模拟需要花时间的任务
            System.Threading.Thread.Sleep (100); 
        }
    }
}

class Test
{
    static void Main()
    {
        //新建委托变量
        ProgressReporter p = WriteProgressToConsole;
        p += WriteProgressToFile;
        //进行具体的任务
        Util.HardWork (p);
    }

    //报告进度-输出到Console
    static void WriteProgressToConsole (int percentComplete)
                => Console.WriteLine (percentComplete);
    //报告进度-输出到文件
    static void WriteProgressToFile (int percentComplete)
                => System.IO.File.WriteAllText ("progress.txt",percentComplete.ToString());
}

从委托移除方法

从委托移除方法使用-=运算符
image

示意图:
image

注意:

​ 试图移除空委托变量的方法会抛出异常,使用前记得和null比较

​ 试图删除委托中不存在的方法是没有效果的

​ 删除方法从委托的调用列表最后开始搜索,移除第一个匹配到的方法

调用委托

说明:

调用委托变量类似于调用方法

用于委托的参数将传递给调用列表中的每一个方法

image

注意:

​ 调用空委托会跳出异常,使用前应检查null

​ 参数是值类型,将返回最后一个函数的返回值.其他返回值将被丢弃

​ 调用组合委托变量时,方法是无序的

​ 参数是引用类时,传递的是引用,会连续改变引用参数

​ 委托除了使用DelegateName()方式调用,还可以使用DelegateName.Invoke()调用

using System;

namespace ConsoleApp3
{
    //定义委托类型
    delegate void Panda();
    class Program
    {
        static void Main(string[] args)
        {
            //定义委托变量
            Panda panda = delegate ()
            {
                //使用局部变量
                Console.WriteLine("Panda666");
            };

            //调用委托
            panda.Invoke();

            //wait
            Console.ReadKey();
        }
    }
}

调用带返回值的委托

如果委托有返回值并且调用列表有一个以上的方法,那么:

调用列表的最后一个方法的返回值将作为委托调用的返回值

调用列表中的其他方法的返回值将被忽略

注意:因为调用顺序是不确定的,所以不要依赖委托返回值

调用带引用参数的委托

如果委托有引用参数,在调用委托列表中的下一个方法时,参数的新值(不是初始值)会传给下一个方法

image

示意图:
image

委托内方法列表异常处理

委托变量内有多个方法,其中一个方法抛出异常,委托方法列表将不会继续执行

为了处理这个方法,可以使用GetInvocationList()方法进行自己来遍历调用方法

实例:

using System;

namespace Test
{
    class TestClass
    {
        //定义一个委托类型
        delegate void Panda();
        public static void Main()
        {
            //新建并实例化委托变量
            Panda panda = new Panda(TestClass.Method1);
            panda += TestClass.Method2;

            //自定义遍历委托方法列表
            foreach (Panda item in panda.GetInvocationList())
            {
                try
                {
                    item();
                }
                catch(Exception e)
                {
                    Console.WriteLine(e.Message);
                }
            }

            Console.ReadKey();
        }

        /// <summary>
        /// 测试用的方法一
        /// </summary>
        public static void Method1()
        {
            throw new Exception("Panda Exception");
        }

        /// <summary>
        /// 测试用的方法二
        /// </summary>
        public static void Method2()
        {
            Console.WriteLine("Method2");
        }
    }
}

使用Invoke方法调用委托

using System;
using System.Threading;
using System.Threading.Tasks;

namespace PandaTestDemo
{
    //定义委托类型
    delegate void PandaDelegate();

    class Program
    {
        /// <summary>
        /// 做某事
        /// </summary>
        static void DoSomething()
        {
            Console.WriteLine("DoSomething");
        }

        static void Main(string[] args)
        {
            //定义委托实例
            PandaDelegate pandaDelInstance = DoSomething;
            //调用委托
            pandaDelInstance.Invoke();

            //wait
            Console.WriteLine("Success");
            Console.ReadKey();
        }
    }
}

使用BeginInvoke调用委托

BeginInvoke会在底层使用多线程实现异步调用

注意:.NET 5已不支持该方法

using System;
using System.Threading;

namespace PandaTestDemo
{
    //定义委托类型
    delegate void PandaDelegate();

    class Program
    {
        /// <summary>
        /// 做某事1
        /// </summary>
        static void DoSomething1()
        {
            Console.WriteLine("DoSomething1-Begin");
            Thread.Sleep(TimeSpan.FromSeconds(3));
            Console.WriteLine("DoSomething1-End");
        }

        /// <summary>
        /// 做某事2
        /// </summary>
        static void DoSomething2()
        {
            Console.WriteLine("DoSomething2-Begin");
            Thread.Sleep(TimeSpan.FromSeconds(3));
            Console.WriteLine("DoSomething2-End");
        }

        static void Main(string[] args)
        {
            //定义委托实例
            PandaDelegate pandaDelInstance1 = new PandaDelegate(DoSomething1);
            PandaDelegate pandaDelInstance2 = new PandaDelegate(DoSomething2);

            //异步调用委托
            pandaDelInstance1.BeginInvoke(null, null);
            pandaDelInstance2.BeginInvoke(null, null);

            //wait
            Console.WriteLine("Success");
            Console.ReadKey();
        }
    }
}

委托捕获变量

委托使用了外部作用域的变量称为捕获变量

当代码离开了作用域后,变量仍然存在,委托仍然可以使用,类似闭包

using System;

namespace ConsoleApp3
{
    //定义委托类型
    delegate void Panda();
    class Program
    {
        static void Main(string[] args)
        {
            //局部变量
            int localValue = 666;
            //定义委托变量
            Panda panda = delegate ()
            {
                //使用局部变量
                Console.WriteLine(localValue);
            };

            //wait
            Console.ReadKey();
        }
    }
}

获得委托变量关联的对象和方法

如果委托对象存储的是实例的方法,则可以使用Target属性获得实例的引用

使用委托变量的Method属性获得关联的方法

使用委托变量的GetInvocationList ()方法获得关联的多个方法

using System;

namespace ConsoleApp1
{
    //测试使用的委托
    delegate void PandaDelegate();

    //测试类
    class PandaClass
    {
        public void DoSomething()
        {
            Console.WriteLine("PandaClass DoSomething");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            //新建实例
            PandaClass pandaClass = new PandaClass();
            //委托变量赋值
            PandaDelegate pandaDelegate = pandaClass.DoSomething;
            pandaDelegate += pandaClass.DoSomething;
            //输出目标
            Console.WriteLine(pandaDelegate.Target);
            //输出方法
            Console.WriteLine(pandaDelegate.Method);
            //输出所有方法
            foreach (PandaDelegate item in pandaDelegate.GetInvocationList())
            {
                Console.WriteLine(item.Method);
            }
            //wait
            Console.ReadKey();
        }
    }
}

委托本质

委托对象的本质

委托实例本质是一个包含其他对象方法地址的对象

委托本质是一种特别的类

委托类型派生自System.MulticastDelegate(这个类型又派生自System.Delegate)

但自定义委托不允许直接显式派生自这两个类型

这2个类不能直接在代码中使用

委托对象包含方法:

​ Invoke() //调用方法列表

​ BeginInvoke() //异步调用方法列表

​ EndInvoke() //异步调用方法列表

​ GetInvocationList() //获得委托对象中的方法列表

​ Target

​ Method

委托变量赋值的本质

当给一个委托变量赋值的时候,本质是实例化对象

所以本质上委托变量的赋值是发生在运行时

注意:A delegate variable is assigned a method at runtime

实例:

Transformer t = Square;
//as same
Transformer t = new Transformer (Square);

委托变量调用的本质

当调用一个委托变量的时候,本质是调用其的Invoke方法

实例:

t(3)
//as same
t.Invoke(3)

委托运算符本质

编译器最终会将+, -, +=,-=运算符编译为System.Delegate类型的Static的Combine方法和Remove方法

委托命名规范

Handler 作为普通委托的后缀

EventHandler 作为事件委托的后缀

实例:

public delegate void BeeHandler(string str)
public delegate void ClickEventhandler(object sender, EventArg args)

委托的不可变性

委托类型的实例是不可变的(immutable)

当对委托类型的实例调用+=或-=,本质是创建新的委托类型实例

委托兼容性(Delegate Compatibility)

不同的委托类型不相互兼容

但可以在创建委托时,进行赋值

如果相同类型的委托实例具有相同的方法目标,则认为它们相等

委托赋值兼容性

实例:委托赋值兼容性

delegate void D1();
delegate void D2();

D1 d1 = Method1;
D2 d2 = d1;            // 不可以,编译错误Compile-time error
//这是允许的
D2 d2 = new D2 (d1);

实例:委托类型声明的签名相同,但不相互兼容

delegate void D1();
delegate void D2();
D1 d1 = Method1;
D2 d2 = d1; // Compile-time error

实例:指向相同方法的委托是相等的

delegate void D();
D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2); // True

委托的返回类型兼容(Return type variance)

实例:

delegate object ObjectRetriever();
static void Main()
{
  ObjectRetriever o = new ObjectRetriever (GetString);
  object result = o();
  Console.WriteLine (result);      // hello
}
static string GetString() => "hello";

委托的比较

如果相同类型的委托变量指向相同的方法并且顺序相同,则委托变量相同

实例:

using System;
using System.Threading;

namespace ConsoleApp1
{
    //测试使用的委托
    delegate void PandaDelegate();

    class Program
    {
        static void Main(string[] args)
        {
            PandaDelegate a1 = Test;
            PandaDelegate a2 = Test;

            Console.WriteLine(a1 == a2); //true

            //wait
            Console.ReadKey();
        }

        static void Test()
        {
            //
        }
    }
}

泛型委托类型(Generic Delegate Types)

委托类型可以定义泛型参数

泛型委托最佳实践:

​ 将只用于返回类型的类型参数标记为协变(out)

​ 将只用于参数的任意类型参数标记为逆变(in)

实例:

public delegate T Transformer<T> (T arg);

实例:

using System;

namespace ConsoleApp3
{
    //定义泛型委托类型
    delegate void PandaDelegate<in T>(T arg1);
    class Program
    {
        public static void DoSomething(int arg1)
        {

        }
        static void Main(string[] args)
        {
            PandaDelegate<int> pandaDelegate = new PandaDelegate<int>(DoSomething);

            //wait
            Console.ReadKey();
        }
    }
}

.NET预定义泛型委托

使用.NET预定义的泛型委托可以减少自己定义委托的麻烦

Action:表示不带参数,返回类型是void。的委托

Action<>:表示带参数(最多8个),返回类型是void,的委托

Func<>:表示带参数(最多16个),返回类型不是void,的委托

Predicate<>:表示带1个泛型参数,返回类型是bool,的委托

无返回值泛型委托(Action Delegates)

The Action Generic Delegate in C# is also present in the System namespace. It takes one or more input parameters and returns nothing. This delegate can take a maximum of 16 input parameters of the different or same type

Note: Whenever your delegate does not return any value, whether by taking any input parameter or not, then you need to use the Action Generic delegate in C#.

Action
Action<>

实例:

Action test1 = () => { };
Action<int> test2 = (int i) => { };
Action<int, string> test3 = (int i, string s) => { };
Action<bool, int> test4 = (bool b, int i) => { };

有返回值泛型委托(Func Delegates)

The Func Generic Delegate in C# is present in the System namespace. This delegate takes one or more input parameters and returns one out parameter. The last parameter is considered as the return value. The Func Generic Delegate in C# can take up to 16 input parameters of different types. It must have one return type. The return type is mandatory but the input parameter is not.

Note: Whenever your delegate returns some value, whether by taking any input parameter or not, you need to use the Func Generic delegate in C#.

注意:最后一个类型参数是返回类型

Func<>

实例:

Func<int> test1 = () => { return 1; };
Func<int, string> test2 = (int i) => { return i.ToString(); };
Func<bool, int, int> test3 = (bool b, int i) => { return 1; };

返回布尔类型的泛型委托(Predicate Generic Delegate)

The Predicate Generic Delegate in C# is also present in the System namespace. This delegate is used to verify certain criteria of the method and returns the output as Boolean, either True or False. It takes one input parameter and always returns a Boolean value which is mandatory. This delegate can take a maximum of 1 input parameter and always return the value of the Boolean type.

public delegate bool Predicate<in T>(T obj)

实例:

Predicate<int> pp = (int i) => i > 0;

委托与接口对比(Delegates Versus Interfaces)

委托能实现的功能一般用接口一样能实现

以下情况优先选择委托:

The interface defines only a single method

Multicast capability is needed

The subscriber needs to implement the interface multiple times

实例:

public interface ITransformer
{
    int Transform (int x);
}
public class Util
{
    public static void TransformAll (int[] values, ITransformer t)
    {
        for (int i = 0; i < values.Length; i++)
        values[i] = t.Transform (values[i]);
    }
}
class Squarer : ITransformer
{
    public int Transform (int x) => x * x;
}

static void Main()
{
    int[] values = { 1, 2, 3 };
    Util.TransformAll (values, new Squarer());
    foreach (int i in values)
    Console.WriteLine (i);
}

委托实例

基本实例

using System;

namespace PandaNamespace
{
    class PandaClass
    {
        //定义委托
        public delegate void test();
        static void Main(string[] args)
        {
            //声明并初始化委托
            test test = someMethod;
            //执行委托
            test();

            Console.WriteLine("Task Process Over");
            Console.ReadKey();
        }

        public static void someMethod()
        {
            Console.WriteLine("test");
        }
    }
}

复合委托实例

using System;

namespace PandaNamespace
{
    class TestClass
    {
        //声明委托类型
        public delegate void TestDelegate();

        //定义委托成员变量
        public TestDelegate someAction;

        //添加方法到委托变量中
        public void AddAction(TestDelegate someAction)
        {
            if(this.someAction == null)
            {
                this.someAction = someAction;
            }
            else
            {
                this.someAction += someAction;
            }
        }

        //删除委托中的方法
        public void DeleteAction(TestDelegate someAction)
        {
            if(this.someAction != null)
            {
                this.someAction -= someAction;
            }
        }

        //执行委托
        public void RunAction()
        {
            if(this.someAction != null)
            {
                this.someAction();
            }
        }

        public void SomeMethod1()
        {
            Console.WriteLine("SomeMethod1");
        }

        public void SomeMethod2()
        {
            Console.WriteLine("SomeMethod2");
        }

        public void SomeMethod3()
        {
            Console.WriteLine("SomeMethod3");
        }
    }

    class PandaClass
    {
        static void Main(string[] args)
        {
            TestClass testObj = new TestClass();
            testObj.AddAction(testObj.SomeMethod1);
            testObj.AddAction(testObj.SomeMethod2);
            testObj.AddAction(testObj.SomeMethod3);
            testObj.RunAction();

            testObj.DeleteAction(testObj.SomeMethod2);
            testObj.RunAction();

            Console.WriteLine("Task Process Over");
            Console.ReadKey();
        }
    }
}

委托作为方法参数

using System;

namespace PandaNamespace
{
    class TestClass
    {
        //声明委托类型
        public delegate void TestDelegate(int arg1, int arg2);

        //执行委托
        public void RunAction(TestDelegate method, int arg1, int arg2)
        {
            method(arg1,arg2);
        }

        public void SomeMethod1(int arg1, int arg2)
        {
            Console.WriteLine($"SomeMethod1 arg1 = {arg1}, arg2 = {arg2}");
        }

        public void SomeMethod2(int arg1, int arg2)
        {
            Console.WriteLine($"SomeMethod2 arg1 = {arg1}, arg2 = {arg2}");
        }
    }

    class PandaClass
    {
        static void Main(string[] args)
        {
            TestClass testObj = new TestClass();
            testObj.RunAction(testObj.SomeMethod1, 123, 456);
            testObj.RunAction(testObj.SomeMethod2, 666, 888);
            Console.WriteLine("Task Process Over");
            Console.ReadKey();
        }
    }
}

使用委托输出任务进度

using System;
using System.Threading;

namespace ConsoleApp1
{
    //测试使用的委托
    delegate void ProgressDelegate(int percent);

    class Program
    {
        static void Main(string[] args)
        {
            //定义委托
            ProgressDelegate progressDelegate = PrintProgressToConsole;

            //模拟的任务
            for (int i = 0; i <= 100; i++)
            {
                progressDelegate(i);
                Thread.Sleep(100);
            }

            //wait
            Console.ReadKey();
        }

        static void PrintProgressToConsole(int percent)
        {
            Console.WriteLine($"Action {percent}%");
        }
    }
}

匿名方法(Anonymous Methods)

说明

一些方法只会使用一次,如果从零开始创建委托并引用该方法,会很麻烦

使用匿名方法(anonymous method)在初始化委托时就声明方法

image

可以在以下地方使用匿名方法:

​ 声明委托变量时直接作为赋值

​ 组合委托时赋值

​ 为委托增加事件时赋值

实现

image

注意:

​ 不需要在匿名方法中声明返回值,因为委托已经声明了返回值

​ 如果委托声明中包含params参数,在匿名方法中不需要params修饰

​ 如果委托声明中包含ref和out参数,在匿名方法中必须带上修饰

​ 如果没有参数,参数列表可以省略

​ 在匿名方法中可以访问外部变量,甚至可以形成闭包

​ 匿名方法中的变量作用域只在方法内
image

匿名方法的限制(Limitations of Anonymous Methods )

  1. An anonymous method in C# cannot contain any jump statement like goto, break or continue.

  2. It cannot access the ref or out parameter of an outer method.

  3. The Anonymous methods cannot have or access the unsafe code.

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/289809.html

(0)
上一篇 2022年9月16日
下一篇 2022年9月16日

相关推荐

发表回复

登录后才能评论