C#基础篇——委托

前言

在本章中,主要是借机这个C#基础篇的系列整理过去的学习笔记、归纳总结并更加理解透彻。

在.Net开发中,我们经常会遇到并使用过委托,如果能灵活的掌握并加以使用会使你在编程中游刃有余,然后对于很多接触C#时间不长的开发者而言,较好的理解委托和事件并不容易。

本节主要是讲述对委托的定义、委托的使用、多播委托、泛型委托、匿名方法、Func和Action委托、Lambda委托,并对它们进行讨论。

说明

简单说它就是一个能把方法当参数传递的对象,而且还知道怎么调用这个方法,同时也是粒度最小的“接口”(约束了指向方法的签名)。

开始

1.定义委托

委托:是一种定义方法签名的类型。 当实例化委托时,可以将其实例与任何具有兼容签名的方法相关联。 可以通过委托实例调用方法。

这里引用一个网友的说法:

某人有三子,让他们各自带一样东西出门,并带回一头猎物。
上面一句话可以理解为父亲对儿子的委托:猎物 办法(工具 某工具)-->delegate 猎物(返回值) 带回猎物(委托名)(工具(参数类型) x)-->delegate int GetValue(int i)
三个人执行委托的方法各不相同
兔子 打猎(工具 弓)-public static int GetValue1(int i){ return i; }
野鸡 买(工具 钱)-public static int GetValue2(int i){ return i*2; }
狼 诱捕(工具 陷阱)-public static int GetValue3(int i){ return i*i; }

2.简单的使用

一个委托类型定义了该类型的实例化时能调用的一类方法,这些方法含有同样的返回类型和同样参数(类型和参数个数相同)

比如:定义一个委托

delegate int Calculator (int x);

此委托适用于有着int返回类型和一个int类型参数方法。

static int Double (int x) { return x * 2; }

创建一个委托实例,并将方法赋值给委托实例

Calculator c = new Calculator(Double);
//或者另一种写法
Calculator c = Double;

通过委托实例的调用

int result = c(2);

3.多播委托

在开发中,我们有时候会遇到要通过调用一个委托,同时可以执行多个方法的时候,就可以考虑用多播委托。调用多个委托需要多次显示调用这个委托。所有的委托实例都可以包含多个方法,实现多播功能。

这个打个比方:多播,就像一群程序员在瞬聘网填好了求职意向后,某天有个公司发布了一个和这些程序员求职意向刚好相匹配的工作,然后这些求职者都被通知了 - “有一份好工作招人啦,你们可以直接申请去上班了!”。
也就是说,一个委托实例不仅可以指向一个方法,还可以指向多个方法。
多播委托,提供了一种类似于流水线的钩子机制,只要加载到这条流水线上的委托,都会被顺序执行。因为所有的都继承自MulticastDelegate,因此所有的委托都具有多播特性
        //声明一个委托,委托返回值为void
        public delegate void Greetings(String name);

        public static void Hello(String name)
        {
            Console.WriteLine("您好, {0}!", name);
        }

        public static void GoodBye(String name)
        {
            Console.WriteLine("再见, {0}!", name);
        }

        public static void Main()
        {
            Greetings greetings = Hello;
            //使用+=给委托添加方法
            greetings += GoodBye;
            String name = "艾三元";
            Console.WriteLine("这是一种调用方法:");
            //第一种执行方式
            greetings(name);
            //第二种执行方式
            Console.WriteLine("这是另一种使用方法");
            //返回委托的调用列表。
            Delegate[] delegates = greetings.GetInvocationList();
            //注意这里的delegates列表中存储的是Greetings类型的委托
            foreach (Greetings greeting in delegates)
            {
                greeting(name);
            }
            Console.ReadKey();
        }

1748233452

说明:

  • 如果是多播委托,委托的签名就必须返回 void ,否则,返回值应送到何处?当委托只包含一个方法的时候,则可以通过所封装的方法发现其返回类型的声明,不一定必须是void。实际上,如果编译器发现某个委托返回 void ,就会自动假定这是一个多播委托。

  • “+=” 用来添加,“-=”用来从委托中删除方法调用

4.泛型委托

在之前的篇章中,我们已经学会了什么是泛型,因此,也方便我们理解泛型委托,简单的说,就是一种含有泛型参数的委托。

		public delegate T Calculator<T>(T arg);
        static int Double(int x) { return x * 2; }

        static class Utility
        {
            public static void Calculate<T>(T[] values, Calculator<T> c)
            {
                for (int i = 0; i < values.Length; i++)
                    values[i] = c(values[i]);
            }
        }
        static void Main(string[] args)
        {
            int[] values = { 11, 22, 33, 44 };

            Utility.Calculate(values, Double);
            foreach (int i in values)
                Console.Write(i + " "); // 22 44 66 88
            Console.ReadKey();
        }

5. 匿名方法

匿名方法,是在初始化委托时候内联声明的方法。

每次实例化一个委托时,都需要事先定义一个委托所要调用的方法。为了简化这个流程,C# 2.0开始提供匿名方法来实例化委托。这样,我们在实例化委托时就可以 “随用随写” 它的实例方法。

    static string GetNumber(string str)
    {
        return str;
    }
    delegate string DelNumber(string str);
    static void Main(string[] args)
    {
        //声明一个名称为GetNumber的具名方法
        DelNumber delNumber1 = GetNumber;
        Console.WriteLine(delNumber1("这是具名方法"));

        //匿名方法 ,未在别的地方定义方法,而是直接把方法写在实例化代码中
        DelNumber delNumber2 = delegate (string str)
        {
            return str;
        };
        Console.WriteLine(delNumber2("这是匿名方法调用"));
        Console.ReadKey();
    }

    #endregion

通过以上简单的示例看出:

匿名方法的语法:关键字delegate {参数列表}{语句块}

delegte { Paramters} {ImplementationCode}

		delegate (string str)
        {
            return str;
        };

使用的格式是:

委托类名 委托实例名 = delegate (args) {方法体代码} ;

		 delegate string DelNumber(string str); //委托类型的返回类型
        //匿名方法 ,未在别的地方定义方法,而是直接把方法写在实例化代码中
        DelNumber delNumber2 = delegate (string str)
        {
            return str;                         //根据返回类型,返回一个string类型
        };

这样就可以直接把方法写在实例化代码中,不必在另一个地方定义方法。当然,匿名委托不适合需要采用多个方法的委托的定义。需要说明的是,匿名方法并不是真的“没有名字”的,而是编译器为我们自动取一个名字。

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

  • 声明委托变量时为初始化表达式。
  • 组合委托时在赋值语句的右边。
  • 为委托增加事件时在赋值语句的右边。

6.Func 和 Action 委托

在之前,我们在使用委托的时候,都是自定义一个委托类型,再使用这个自定定义的委托定义一个委托字段或变量。而在后续的编程语言中又新加入了一种特性,C#语言预先为我们定义了两个常用的委托,一个是Func,一个是Action,还带来了Lambda,这使得委托的定义和使用变得简单起来, 在以后进行C#程序编写中引入委托更加灵活。

Action委托

C#中与预定义了一个委托类型Action,基本特点就是可以执行一个没有返回值,没有参数的方法。是一类没有输出参数的委托,但是输入参数可以为C#中的任意类型,即可以进行委托执行形式的方法。

    static void printString()
    {
        Console.WriteLine("Hello World");
    }
    static void printNumber(int x)
    {
        Console.WriteLine(x);
    }
    static void Main(String[] args)
    {
        //Action基本使用
        Action a = printString;
        a(); // 输出结果  Hello World
        //Action指向有参数的方法
        Action<int> b = printNumber; // 定义一个指向 形参为int的函C#数
        b(5); // 输出结果  5
    }

Action可以通过泛型来指定,指向的方法有 0 – 16个参数

Action<int, int, string, bool 等等>

Func委托

Func同样也是预定的委托,是一种由返回值的委托,传递0-16个参数,其中输入参数和返回值都用泛型表示。

  		static int GetNumber()
        {
            return 1;
        }
        static int GetNumber(string str)
        {
            return 1;
        }
        static void Main(string[] args)
        {
            Func<int> a = GetNumber; // 定义一个Func 委托,  指向一个返回int类型的 方法
            Console.WriteLine(a());
            Func<string, int> b = GetNumber; // 泛型中最后一个参数表示返回值类型。
            Console.WriteLine(b("Hello"));
        }	

注意:Func<string, int> 最后一个参数表示返回值类型,前面的都是形参类型。

7. Lambda表达式

江山代有才人出,纵然匿名方法使用很方便,可惜她很快就成了过气网红,没能领多长时间的风骚。如今已经很少见到了,因为delegate关键字限制了她用途的扩展。自从C# 3.0开始,她就被Lambda表达式取代,而且Lambda表达式用起来更简单。Lambda表达式本质上是改进的匿名方法。

在匿名方法中,delegate关键字有点多余,因为编译器已知将我们的方法赋值给委托。因此,我们很容易的将匿名方法的步骤转换为Lambda表达式:1. 删除delegate关键字。2.在参数列表和匿名方法主体之间放lambda运算符=>。

DelNumber delNumber2 = delegate (string str){ return str;}; //匿名方法

DelNumber delNumber2 =  (string str) =>{ return str;}; //Lambda方法

Lambda表达式的灵感来源于数学中的Lambda积分函数表达式,例如下图:

1270751451

Lambda表达式把其中的箭头用 => 符号表示。

上面的对比例子中,Lambda还可以进一步简化

delegate string DelNumber(string str); //委托类型的返回类型
DelNumber delNumber2 =  (string str) =>{ return str;}; //Lambda方法
DelNumber delNumber3 =         (str) =>{ return str;}; //省略类型参数
DelNumber delNumber4 =           str =>{ return str;}; //省略类型参数( 如果只有一个隐式类型参数,可以省略周围的圆括号)
DelNumber delNumber5 =           str =>  str; //语句块替换为return关键字后的表达式 ( 如果只有一个返回语句,可以将语句块替换为return关键字后的表达式)

如今Lambda表达式已经应用在很多地方了,例如方法体表达式(Expression-Bodied Methods)、自动只读属性表达式等等。

Lambda表达式形式上分为两种:

1.表达式Lambda
当匿名函数只有一行代码时,可采用这种形式。例如:

DelNumber delNumber= (s4, s5) => s4.Age <= s5.Age;

其中=>符号代表Lambda表达式,它的左侧是参数,右侧是要返回或执行的语句。参数要放在圆括号中,若只有一个参数,为了方便起见可省略圆括号。有多个参数或者没有参数时,不可省略圆括号。

相比匿名函数,在表达式Lambda中,方法体的花括号{}和return关键字被省略掉了。

用的也是表达式Lambda,这是Lambda表达式的推广, 是C# 6 编译器提供的一个语法糖。

2.语句Lambda
当匿名函数有多行代码时,只能采用语句Lambda。例如,上面的表达式Lambda可改写为语句Lambda:

DelNumber delNumber= (s4, s5) => 
{
    //此处省略其他代码
    return s4.Age <= s5.Age;
};

语句Lambda不可以省略{}和return语句。

完整示例

        delegate string DelNumber(string str); //委托类型的返回类型
        static void Main(string[] args)
        {

            DelNumber delNumber2 = (string str) => { return str; }; //Lambda方法
            DelNumber delNumber3 = (str) => { return str; }; //省略类型参数
            DelNumber delNumber4 = str => { return str; }; //省略类型参数( 如果只有一个隐式类型参数,可以省略周围的圆括号)
            DelNumber delNumber5 = str => str; //语句块替换为return关键字后的表达式 ( 如果只有一个返回语句,可以将语句块替换为return关键字后的表达式)

            Console.WriteLine(delNumber2("lambda"));

            Console.WriteLine(delNumber3("lambda"));
            Console.WriteLine(delNumber4("lambda"));
            Console.WriteLine(delNumber5("lambda"));
            Console.ReadKey();
        }

注意:一个参数可以省略圆括号,多个参数必须圆括号,但是没有参数,必须使用一组空的圆括号

如: (参数,参数)=>{语句} 或者 表达式
       (参数)  =>{语句} 或者 表达式
        参数   =>{语句} 或者 表达式
        ()    =>{语句} 或者 表达式

总结

  1. 委托相当于用方法作为另一方法参数,同时,也可以实现在两个不能直接调用的方法中做桥梁,如在多线程中的跨线程的方法调用就得用委托。
  2. 熟悉在什么情况使用委托,在使用事件设计模式时,当需要封装静态方法时,当需要方便的组合时等多种情况下,可以加以使用。
  3. 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。
  4. 在下一节中,将对事件进行简单介绍,并总结归纳。

参考 文档 《C#图解教程》

注:搜索关注公众号【DotNet技术谷】–回复【C#图解】,可获取 C#图解教程文件