您的位置:首页 > 编程语言 > C#

【C#】委托基础

2017-03-03 21:06 190 查看

【关于委托】

概述

  委托是表示对具有特定参数列表和返回类型的方法的引用的类型。简单说,委托是一种引用类型,它引用的对象是方法,实例化委托对象时,需要赋值与委托标签相同的方法名或是null值,关联后,可以通过委托实例来调用方法。补充一点,这里的标签与重载概念中的标签定义几乎是相同的,即当方法的返回值类型,参数个数,参数类型(有序)与委托类型的返回值类型,参数个数,参数类型完全一致时(除了变体形式),该方法可赋值给该委托,我们又称作是委托关联了一个方法。通过这段描述,也可以得知,委托是一种类型安全的特性。

  那么变体是什么呢?变体是协变性抗变性的总称。抗变性即参数类型可以从一个类的类型更换为该类派生类的类型;协变性即返回值类型可以从一个类的类型更换为该类基类的类型。其本质是派生类对象可以转换为基类对象,而基类对象无法隐式或者强制转换为派生类对象。(至于不清楚原因的读者可以查阅“C#继承与多态”的相关资料)定义委托与方法如下所示(此处为简写,【使用委托】模块中有完整的例子)

//定义一个委托
public delegate object MyDelegate(string str);
//声明一个方法
public string MyMethod(object obj);
//实例委托并关联方法
MyDelegate md = MyMethod;
//以调用委托的形式调用方法
md("Hello");


  过程是这样的:当我们传参时,方法本身是需要object类型的参数,而我们定义的委托是string类型的参数,string是object的派生类,可以隐式转换,所以我们可以将参数类型更换为string类型;同样的,方法本身输出string类型的返回值,但我们定义的是object类型返回值的委托,当我们取返回值时,方法返回值string类型也会隐式转换为我们委托返回值object返回值。当我们用委托实例md来调用方法时,书写时编译器会有如下图提示:



  提示我们当前需要输入string类型的参数,返回object类型的返回值。注:协变性与抗变性只应用于引用类型,不适用于void和值类型。

特性

1.委托类似于C++函数指针,但它比·函数指针更加安全,主要体现在类型安全;

2.委托允许将方法作为参数传递;

3.委托可用于定义回调方法;

4.委托可以链接在一起;

5.方法不必与委托类型完全匹配,可使用变体;

6.可使用匿名函数(匿名方法与Lambda表达式)。

【委托语法与使用】

定义委托

1.定义如下委托,匹配int型参数1个且无返回值的方法,该委托定义在命名空间内,该命名空间下的任意类与方法均可创建该委托的实例:

namespace DelegateCSDN
{
public delegate void MyDelegate(int num);

class Program
{
static void Main(string[] args) { }
}
}


2.当定义在类中时,private修饰符表示该委托只能在该类内部实例;public修饰符表示该委托可以在该命名空间下的任意类中实例,可将其视为类类型,在其他类创建该委托方式如下:

namespace DelegateCSDN
{
class Program
{
public delegate void MyDelegate(int num);

static void Main(string[] args) { }
}

class MyClass
{
//Program.MyDelegate类型的使用与Program的访问级无关
void MyMain()
{
Program.MyDelegate md = new Program.MyDelegate(MyMethod);
}

void MyMethod(int number)
{
//code......
}
}
}


使用委托

基本形式

  实例委托可以有以下几种形式:

namespace DelegateCSDN
{
class Program
{
public delegate void MyDelegate(int num);

static void Main(string[] args)
{
// 1 标准形式
MyDelegate md1 = new MyDelegate(MyMethod);

// 2 简写成如下形式,编译器会推断出委托类型
MyDelegate md2 = MyMethod;

// 3 也可以赋值为null值
MyDelegate md3 = null;
}

static void MyMethod(int num)
{
//code......
}
}
}


重载方法

  当含有多个同名方法的重载方法时,委托实例会关联与自身相匹配的那一重载方法。注:委托只依靠参数类型与参数个数来进行自动匹配,若存在重载方法参数类型与参数个数均相同,仅返回值类型不同,则编译器会报错。示例如下:

namespace DelegateCSDN
{
class Program
{
public delegate void MyDelegate(int num1, int num2);

static void Main(string[] args)
{
//委托实例md关联2方法,会自动关联与委托相匹配的重载方法
MyDelegate md = new MyDelegate(MyMethod);
}

//
4000
1
static void MyMethod(int num)
{
//code......
}

// 2
static void MyMethod(int num1, int num2)
{
//code......
}

//不可定义如下重载方法,由于参数个数及类型均与2方法相同
//只有返回值不同,无法自动判断委托关联的是哪一重载方法
//static int MyMethod(int num1, int num2)
//{
//    return num1 + num2;
//}
}
}


链接方法

  委托还可以同时关联多个方法,称为链接方法。这种模式常用于观察者模式中(事件),当调用委托实例时,该委托关联的所有方法均会被执行。当关联多个方法或移除多个方法中的某一方法时,利用“+=”和“-=”运算符。委托必须关联方法或者赋值为null(即初始化)后才可链接或移除方法。示例如下:

namespace DelegateCSDN
{
class Program
{
public delegate void MyDelegate();

static void Main(string[] args)
{
//形式1
MyDelegate md1 = null;
md1 += new MyDelegate(MyMethod1);
md1 += new MyDelegate(MyMethod2);
md1 += new MyDelegate(MyMethod3);
md1 -= new MyDelegate(MyMethod2);
//只会执行方法1和方法3
md1();

//形式2 可以省略委托类型直接链接
MyDelegate md2 = MyMethod1;
md2 += MyMethod2;
md2 += MyMethod3;
md2 -= MyMethod1;
//只会执行方法2和方法3
md2();
}

// 1
static void MyMethod1()
{
Console.WriteLine("Method1 is called!");
}

// 2
static void MyMethod2()
{
Console.WriteLine("Method2 is called!");
}

// 3
static void MyMethod3()
{
Console.WriteLine("Method3 is called!");
}
}
}


回调方法

  回调机制简单来说就是把方法1作为参数传给方法2,由方法2决定什么时候来调用方法1:

namespace DelegateCSDN
{
class Program
{
public delegate void MyDelegate(string str);

static void Main(string[] args)
{
//md关联PrintInfo方法
MyDelegate md = new MyDelegate(PrintInfo);
//调用SetName方法,将委托实例md作为参数传递给该方法
SetName("Eazey", md);
}

static void SetName(string name, MyDelegate myDelegate)
{
if (myDelegate != null)
{
string content = "My name is " + name;
//由SetName方法在适时的情况下调用传进来的委托,
//此示例中表示调用的实际是PrintInfo方法
myDelegate(content);
}
else
{
Console.WriteLine("myDelegate is null!");
}
}

//PrintInfo方法可以说是被间接的调用了
//此例中,PrintInfo方法被称为回调方法
static void PrintInfo(string content)
{
Console.WriteLine(content);
}
}
}


变体形式

  指的是参数的抗变性与返回值的协变性。委托不单单可以关联与其标签匹配的方法,还可以以变体的形式关联该方法。下述代码是概述中的完整形式:

namespace DelegateCSDN
{
delegate object MyDelegate(string str);

class Program
{
static void Main(string[] args)
{
MyDelegate md = new MyDelegate(MyMethod);
object obj = md("Hello");
//打印 Hello
Console.WriteLine(obj.ToString());
}

static string MyMethod(object obj)
{
string str = obj as string;
return str;
}
}
}


匿名方法

  匿名方法,顾名思义,不需要名字的方法,即没有真正被定义的方法。开发时常常想要代码简化再简化,有的方法在开发中只用作回调而不会直接调用,这样的方法定义出来实在是又占地方又占名字的,所以可以在实例委托时为其指定方法的内容。此时我们不需要中规中矩的定义方法的访问级返回值类型名称之类的。使用关键字delegate,示例如下:

namespace DelegateCSDN
{
class Program
{
public delegate string MyDelegate(string str);

static void Main(string[] args)
{
MyDelegate md = new MyDelegate(delegate(string str)
{
string newStr = "Hello " + str;
return newStr;
});
string returnStr = md("World");
//打印Hello World
Console.WriteLine(returnStr);

//写成如下形式也可以
MyDelegate md1 = delegate(string str)
{
return "Hello " + str;
};
}
}
}


  并没有定义什么方法,方法体的内容直接在delegate关键字后完成,传入参数并返回一个值。在为委托赋值时也不需要指定方法的返回值,编译器可以通过委托的定义来推断返回值,开发者需要return并return与委托相同类型的返回值,否则编译器会报错。

Lambda

  总是希望代码能简洁简洁更简洁,所以C#后续又推出了Lambda表达式,它源自于匿名方法,但看起来更加简洁易用。除无参无返形式外,其余均用.Net中内置的Func与Action泛型委托来进行实例,免去声明好多委托的麻烦。

[b]1.无参无返[/b]

namespace DelegateCSDN
{
class Program
{
delegate void MyDelegate();

static void Main(string[] args)
{
//一句语句  简写成如下形式
MyDelegate md1 = () => Console.WriteLine("Hello World");
//打印Hello World
md1();
//多句语句  需用大括号
MyDelegate md2 = () =>
{
Console.Write("Hello ");
Console.WriteLine("World");
};
//打印Hello World
md2();
//小结:无参无返的“方法”部分只能是语句
}
}
}


[b]2.有返回值时[/b]

namespace DelegateCSDN
{
class Program
{
static void Main(string[] args)
{
//只有返回值语句一般简写成以下形式
//即“方法”内只有值而非语句
Func<string> func1 = () => "Hello World";
//print 'Hello World'
Console.WriteLine(func1());

//也可以写成这样 是上例的完整形式
Func<string> func2 = () => { return "Hello World"; };
//print 'Hello World'
Console.WriteLine(func2());

//多行语句依旧需要大括号
Func<string> func3 = () =>
{
string str1 = "Hello";
string str2 = " ";
string str3 = "World";
return str1 + str2 + str3;
};
//print 'Hello World'
Console.WriteLine(func3());
}
}
}


[b]3.含有单个参数[/b]

namespace DelegateCSDN
{
class Program
{
static void Main(string[] args)
{
//单参数时,可以简化成以下形式,即不带括号
//由于委托已经指明参数为string型,则不需指定参数类型
Action<string> action1 = str => Console.WriteLine(str);
//print 'Hello World'
action1("Hello World");

//标准形式
Action<string> action2 = (string str) => Console.WriteLine(str);
//print 'Hello World'
action2("Hello World");
}
}
}


[b]4.含有多个参数[/b]

namespace DelegateCSDN
{
class Program
{
static void Main(string[] args)
{
//与单参不同的是  最简形式也要带括号
Action<string, int> action1 =
(str, num) => Console.WriteLine("Num:" + num + "  " + str);
//print 'Num:0  Hello World'
action1("Hello World", 0);

//当然也可以写成这种形式
Action<string, int> action2 =
(string str, int num) => Console.WriteLine("Num:" + num + "  " + str);
//print 'Num:1  Hello World'
action2("Hello World", 1);
}
}
}


[b]5.匿名函数都可以使用外部成员[/b]

namespace DelegateCSDN
{
class Program
{
static void Main(string[] args)
{
string str1 = "Hello";

Func<string> func = delegate
{
string str2 = "World";
//使用外部成员str1 所以其实匿名函数并不是封闭的方法
//而是一条或多条组成的语句体
return str1 + " " + str2;
};

Console.WriteLine(func());
}
}
}


【把方法当对象用】

  演示了这么多例子,其实委托就是提供了一种将方法当做对象来使用的机制。例如string类型的对象的值都是string类型的,而MyDelegate类型的对象都是与MyDelegate委托标签匹配的方法。就像是给string str对象赋予各种stirng类型值,每次赋值后只需要Console.WriteLine(str);这一句相同的代码即可打印不同的值,同样的,为MyDelegate md关联与其标签匹配但是各种不同的方法,只需要md();(假设是无参数的委托类型)即可在每次复制后调用不同的方法。

  很重要的一件事是,当方法可以被当成对象时,可作为方法的参数传给带有委托参数的方法,这便引出了下面要重点提及的——回调方法。

【回调方法(CallBack)】

是什么?

  上文提到,回调机制简单来说就是把方法1作为参数传给方法2,由方法2决定什么时候来调用方法1。不简单的说,基于委托的回调机制使开发层与底层产生了联动效应。

怎么用?

  下图是维基百科给出的图例:



  该图表示在开发层中有主程和回调方法,在底层含有库方法,当开发人员在主程中调用API提供的底层库方法时,库方法会调用回调方法。一般调用方向都是开发层调用底层;当开发层的某方法需要被底层回过头来调用,那么此方法被称为回调方法。亦泽个人觉得这个图表示的并不完全,它只表示出开发层需要调用底层,而未表示出在调用的过程中要将回调方法的委托传给底层。完整过程应如下图所示:



  (1)创建回调函数的委托;

  (2)调用库方法,将回调函数以委托形式传递给库方法;

  (3)库方法适机调用回调方法(以调用委托的形式)

为什么要这么用?

  解释了回调方法是什么,应用流程是怎样的,剩下的就是为什么要用回调方法,以及回调方法的意义。传统的编程模式是开发人员来调用底层封装好的库方法来实现各种软件开发,这种调用方向是单一的;而一些比较复杂的逻辑功能实现会被底层封装成库方法,这个库方法具有极高的复用性,但对应的便是它的功能并不会完整,需要开发层人员定制自己的方法来补充这个功能,来实现自身独特的需求,所以需要将自己定制的方法交给库方法,使其变得完整,达到期望的功能实现,这种调用是双向的,由开发层发起,底层回调。

  举个例子,库方法就相当于电脑的主板,而回调函数相当于CPU,装机的过程便是主程,我们并不能脱离主板而单独使用CPU,必须把CPU给主板,让主板与CPU一起运作,才能展现CPU的性能。

再多的字不如敲几行代码

  将上文中回调方法一栏的示例改写一下,我们创建类MyLibraryClass来表示底层类,该类中有个静态方法SetName,用于设置姓名;我们的主程运行在Program类的Main方法中,该类还有个UpdateInfo方法用于打印更新后的信息。示例如下:

namespace DelegateCSDN
{
public delegate void MyDelegate(string str);

//开发层
class Program
{
static void Main(string[] args)
{
MyDelegate md = UpdateInfo;
MyLibraryClass.SetName("Eazey", md);
}

static void UpdateInfo(string str)
{
Console.WriteLine("Print:" + str);
}
}
}


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.RegularExpressions;

namespace DelegateCSDN
{
//底层
public class MyLibraryClass
{
public static void SetName(string name, MyDelegate md)
{
string str = "My name is ";
if (md != null)
{
//使用正则表达式来匹配长度至少为3的纯英文字符串
string parttern = @"^(([a-z]|[A-Z]){3})$";
if (Regex.IsMatch(name, parttern))
//符合条件才会回调输出方法
md(str + name);
else
Console.WriteLine("The name is useless!");
}
else
{
Console.WriteLine("Instense of MyDelegate is null!");
}
}
}
}


【补充与总结】

1.通常用new关键字创建委托对象实例并使用需要关联的方法做参数来实例化该委托,而不是直接md = MyMethod;这种形式,在大多情况下(尤其是在定义了多种委托类型的情况),这种形式的可读性很差,复读程序时很难直观的看出这是何种委托。尽量使用标准形式,灵活使用最简形式。

2.匿名方法和Lambda表达式采用方法的形式,但实际上并不是方法,他可以获取到它外部的成员。

3.回调机制使开发变得更简单,通过与底层的联动性来降低开发难度,使开发人员更多的关注整体的架构而不是某个功能模块的实现;

4.什么叫做一切皆为对象?当有了委托后,真的一切都是对象。

【参考资料】

[1]《Untiy3D脚本编程》 陈嘉栋 著

[2]https://msdn.microsoft.com/zh-cn/library/ms173171.aspx C#编程指南-委托

[3]https://zh.wikipedia.org/wiki/%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0 维基百科-回调方法

[4]https://www.zhihu.com/question/19801131 知乎-回答:桥头堡
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息