委托

什么是委托


可以认为委托是持有一个或多个方法的对象。当然,正常情况下你不想“执行”一个对象,但委托与典型对象不同。可以执行委托,这时委托会执行它所“持有”的方法。
我们从下面的示例代码开始。具体细节将在本章剩余内容介绍。

  • 代码开始部分声明了一个委托类型MyDel(没错,是委托类型不是委托对象)
  • Program类声明了3个方法:PrintLow、PrintHigh和Main。接下来要创建的委托对象将持有PrintLow或PrintHigh方法,但具体使用哪个运行时确定
  • Main声明了局部变量del,持有一个MyDel类型的委托对象的引用。这不会创建对象。只是创建持有委托对象引用的变量,在几行后便会创建委托对象,并将值赋给这个变量
  • Main创建了Random类的对象,这是个随机数生成器类。接着调用该对象Next方法,将99作为参数。这会返回介于0到99间的随机整数,并将这个值保存在局部变量randomValue中
  • 下面一行检查这个随机值是否小于50
    • 小于50,就创建一个MyDel委托对象并初始化,让它持有PrintLow方法的引用
    • 否则,就创建一个持有PrintHigh方法引用的MyDel委托对象
  • 最后,Main执行委托对象del,这将执行它持有的方法(PrintLow或PrintHigh)

如果你有C++背景,理解委托最快的方法是把它看成一个类型安全的、面向对象的C++函数指针

delegate void MyDel(int value);//声明委托类型
class Program
{
void PrintLow(int value)
{
Console.WriteLine("{0} - Low Value",value);
}
void PrintHigh(int value)
{
Console.WriteLine("{0} - High Value",value);
}
static void Main()
{
Program program=new Program();
MyDel del; //声明委托变量
var rand=new Random();
var randomValue=rand.Next();
del=randomValue<
?new MyDel(program.PrintLow)
:new MyDel(program.PrintHigh);
del(randomValue); //执行委托
}
}

委托概述


委托和类一样,是用户自定义类型。但类表示的是数据和方法的集合,而委托持有一个或多个方法,以及一系列预定义操作。
可以通过以下操作步骤来使用委托。

  1. 声明一个委托类型。委托声明看上去和方法声明相似,只是没有实现块
  2. 使用该委托类型声明一个委托变量
  3. 创建委托类型的对象,把它赋值给委托变量。新的委托对象包括指向某个方法的引用,这个方法和第一步定义的签名和返回类型一致
  4. 你可以选择为委托对象增加其他方法。这些方法必须与第一步中定义的委托类型有相同的签名和返回类型
  5. 在代码中你可以像调用方法一样调用委托。在调用委托时,其包含的每个方法都会被执行

你可以把delegate看做一个包含有序方法列表的对象,这些方法的签名和返回类型相同。

  • 方法的列表称为调用列表
  • 委托保存的方法可以来自任何类或结构,只要它们在下面两点匹配
    • 委托的返回类型
    • 委托的签名(包括ref和out修饰符)
  • 调用列表中的方法可以是实例方法也可以是静态方法
  • 在调用委托时,会执行其调用列表中的所有方法

声明委托类型


与类一样,委托类型必须在被用来创建变量以及类型的对象前声明。声明格式如下。

 关键字      委托类型名
↓ ↓
delegate void MyDel(int x);
↑ ↑
返回类型 签名

虽然委托类型声明看上去和方法声明一样,但它不需要在类内部声明,因为它是类型声明。

创建委托对象


委托是引用类型,因此有引用和对象。委托类型声明后,我们可以声明变量并创建类型的对象。
有两种创建委托对象的方法,一种是使用带new运算符的对象创建表达式,如下面代码所示。

delVar=new MyDel(myInstObj.MyM1);
dVar=new MyDel(SClass.OtherM2);

我们还可以使用快捷语法,它仅由方法说明符构成。这种快捷语法能够工作是因为在方法名称和其相应的委托类型间存在隐式转换。

delVar=myInstObj.MyM1;
dVar=SClass.OtherM2;


创建委托对象会为委托分配内存,还会把第一个方法放入委托调用列表。

给委托赋值


由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的引用。旧的委托对象会被GC回收。

MyDel delvar;
delVar=myInstObj.MyM1;
...
delVar=SClass.OtherM2;

组合委托


迄今为止,我们见过的所有委托在调用列表中都只有一个方法。委托可以使用额外的运算符来“组合”。这个运算符会创建一个新的委托,其调用列表连接了作为操作数的两个委托的调用列表副本。
例:创建3个委托,第3个委托由前两个组合而成。

MyDel delA=myInstObj.MyM1;
MyDel delB=SClass.OtherM2;
MyDel delC=delA+delB;

尽管术语组合委托(combining delegate)让我们觉得好想操作数委托被修改了,其实它们并没有被修改。事实上,委托是恒定的。委托对象被创建后不能再被改变。


为委托添加方法


尽管通过上一节我们知道委托是恒定的,不过C#提供了看上去可以为委托添加方法的语法,即使用+=运算符。
例:为委托的调用列表增加两个方法。

MyDel delVar=inst.MyM1;
delVar+=SCL.m3;
delvar+=X.Act;


当然,使用+=运算符时,实际发生的是创建了一个新的委托,其调用列表是左边的委托加上右边的组合。然后将这个新的委托赋值给delVar。

从委托移除方法


我们可以使用-=运算符从委托移除方法。

delVar-=SCL.m3;

与为委托增加方法一样,其实是创建了一个新的委托。新的委托是旧委托的副本–只是没有了已经被移除方法的引用。
移除委托时需要记住以下事项:

  • 如果在调用列表中有多个实例,-=运算符将从列表最后开始搜索,并且移除第一个与方法匹配的实例
  • 试图删除委托中不存在的方法没有效果
  • 试图调用空委托会抛出异常。我们可以通过把委托和null进行比较来判断委托列表是否为空。如果调用列表为空,则委托是null

调用委托


可以像调用方法一样简单地调用委托。调用委托的参数将会用于调用列表中的每个方法(除非有输出参数,我们稍后介绍)。
例:delVar委托接受一个整数值。使用参数调用委托会使用相同的参数值调用它调用列表中的每个成员

MyDel delVar=inst.MyM1;
delVar+=SCL.m3;
delVar+=X.Act;
...
delVar();


如果一个方法在调用列表中出现多次,当委托被调用时,每次在列表中遇到该方法时它都会被调用一次。

委托示例


如下代码定义并使用了没有参数和返回值的委托。有关代码的注意事项如下:

  • Test类定义了两个打印函数
  • Main方法创建了委托的实例并增加了3个方法
  • 程序随后调用委托,调用前检测了委托是否为null
delegate void PrintFunction();
class Test
{
public void Print1()
{
Console.WriteLine("Print1 -- instance");
}
public static void Print2()
{
Console.WriteLine("Print2 -- static");
}
}
class Program
{
static void Main()
{
var t=new Test();
PrintFunction pf;
pf=t.Print1;
pf+=Test.Print2;
pf+=t.Print1;
pf+=Test.Print2;
if(null!=pf)
{
pf();
}
else
{
Console.WriteLine("Delegate is empty");
}
}
}

调用带返回值的委托


如果委托有返回值并且调用列表中有一个以上方法,会发生下面的情况:

  • 调用列表中最后一个方法返回的值就是委托调用的返回值
  • 调用列表中其他返回值被忽略
delegate int MyDel();
class MyClass
{
int IntValue=;
public int Add2()
{
IntValue+=;
return IntValue;
}
public int Add3()
{
IntValue+=;
return IntValue;
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
MyDel mDel=mc.Add2;
mDel+=mc.Add3;
mDel+=mc.Add2;
Console.WriteLine("Value: {0}",mDel());
}
}


调用带引用参数的委托


如果委托有引用参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。
在调用委托列表中的下一个方法时,参数的新值会传给下一个方法。

delegate void MyDel(ref int X);
class MyClass
{
public int Add2(ref int x)
{
x+=;
}
public int Add3(ref int x)
{
x+=;
}
static void Main()
{
var mc=new MyClass();
MyDel mDel=mc.Add2;
mDel+=mc.Add3;
mDel+=mc.Add2;
int x=;
mDel(ref x);
Console.WriteLine("Value: {0}",x);
}
}


匿名方法


匿名方法(anonymous method)是在初始化委托时内联(inline)声明的方法。
例:第一个声明了Add20方法,第二个使用匿名方法。

class Program
{
public static int Add20(int x)
{
return x+=;
}
delegate int OtherDel(int InParam);
static void Main()
{
OtherDel del=Add20;
Console.WriteLine("{0}",del());
Console.WriteLine("{0}",del());
}
}
class Program
{
delegate int OtherDel(int InParam);
static void Main()
{
OtherDel del=delegate(int x)
{
return x+;
};
Console.WriteLine("{0}",del());
Console.WriteLine("{0}",del());
}
}

使用匿名方法

我们可以在如下地方使用匿名方法。

  • 声明委托变量时作为初始化表达式
  • 组合委托时在赋值语句的右边
  • 为委托增加事件(第14章)时在赋值语句的右边
匿名方法的语法

匿名方法表达式语法包含如下:

关键字     参数列表        语句块
↓ ↓ ↓
delegate(Parameters){ImplementationCode}

Lambda 表达式


在匿名方法的语法中,delegate关键字有点多余,因为编译器已经知道我们在将方法赋值给委托。我们可以很容易地通过如下步骤把匿名方法转换为Lambda表达式:

  • 删除delegate关键字
  • 在参数列表和匿名方法主体之间放Lambda运算符=>(读作goes to)。
MyDel del=delegate(int x)    {return x+;};//匿名方法
MyDel le1= (int x) => {return x+;};//Lambda表达式

术语Lambda表达式来源于数学家Alonzo Church等人在1920到1930年期间发明的Lambda积分。Lambda积分是用于表示函数的一套系统,它使用希腊字母Lambda(λ)来表示无名函数。近来,函数式编程语言(如Lisp及其方言)使用这个术语来表示可以直接用于描述函数定义的表达式,表达式不再需要有名字了。

除了这种简单的转换,通过编译器的自动推断,我们可以更进一步简化Lambda表达式。

  • 编译器还可以从委托的声明中知道委托参数的类型,因此Lambda表达式允许我们省略类型参数,如le2

    • 带有类型的参数列表称为显示类型
    • 省略类型的参数列表称为隐式类型
  • 如果只有一个隐式类型参数,我们可以省略周围的圆括号,如le3
  • 最后,Lambda表达式允许表达式的主体是语句块或表达式。如果语句块包含了一个返回语句,我们可以将语句块替换为return关键字后的表达式,如le4
MyDel del=delegate(int x)    {return x+;};
MyDel le1= (int x) => {return x+;};
MyDel le2= (x) => {return x+;};
MyDel le3= x => {return x+;};
MyDel le4= x => x+ ;

例:Lambda表达式完整示例

delegate double MyDel(int par);
class Program
{
static void Main()
{
MyDel del=delegate(int x) {return x+;};
MyDel le1= (int x) => {return x+;};
MyDel le2= (x) => {return x+;};
MyDel le3= x => {return x+;};
MyDel le4= x => x+ ;
Console.WriteLine("{0}",del());
Console.WriteLine("{0}",le1());
Console.WriteLine("{0}",le2());
Console.WriteLine("{0}",le3());
Console.WriteLine("{0}",le4());
}
}

有关Lambda表达式的参数列表的要点如下:

  • Lambda表达式参数列表中的参数必须在参数数量、类型和位置上与委托相匹配
  • 表达式的参数列表中的参数不一定需要包含类型(隐式类型),除非委托由ref或out参数–此时必须注明类型(显式类型)
  • 如果只有一个参数,并且是隐式类型的,周围的圆括号可以省略
  • 如果没有参数,必须使用一组空圆括号

C#图解教程 第十三章 委托的更多相关文章

  1. C#图解教程 第二十三章 预处理指令

    预处理指令 什么是预处理指令基本规则#define和#undef指令条件编译条件编译结构诊断指令行号指令区域指令#pragma warning 指令 预处理指令 什么是预处理指令 源代码指定了程序的定 ...

  2. python 教程 第十三章、 特殊的方法

    第十三章. 特殊的方法 1)    特殊的方法 __init__(self,...) 这个方法在新建对象恰好要被返回使用之前被调用. __del__(self) 恰好在对象要被删除之前调用. __st ...

  3. C#图解教程 第十七章 泛型

    泛型 什么是泛型 一个栈的示例 C#中的泛型 继续栈示例 泛型类声明泛型类创建构造类型创建变量和实例 使用泛型的栈的示例比较泛型和非泛型栈 类型参数的约束 Where子句约束类型和次序 泛型方法 声明 ...

  4. Flask 教程 第十三章:国际化和本地化

    本文翻译自The Flask Mega-Tutorial Part XIII: I18n and L10n 这是Flask Mega-Tutorial系列的第十三部分,我将告诉你如何扩展Microbl ...

  5. C#4.0图解教程 - 第24章 反射和特性 – 2.特性

    1.特性 定义 Attribute用来对类.属性.方法等标注额外的信息,贴一个标签(附着物) 通俗:给 类 或 类成员 贴一个标签,就像航空部为你的行李贴一个标签一样 注意,特性 是 类 和 类的成员 ...

  6. C# 图解教程 第三章 类型、存储和变量

    类型.存储和变量 C#程序是一组类型声明类型是一种模板实例化类型数据成员和函数成员预定义类类型用户定义类型栈和堆 栈堆 值类型和引用类型 存储引用类型对象的成员C#类型的分类 变量静态类型和dynam ...

  7. C# 图解教程 第五章 方法

    方法的结构方法体内部代码的执行本地变量    类型推断和var关键字    嵌套块中的本地变量本地常量控制流方法调用返回值返回语句和void方法参数    形参    实参值参数引用参数引用类型作为值 ...

  8. C#图解教程 第六章 深入理解类

    深入理解类 类成员成员修饰符的顺序实例类成员静态字段从类的外部访问静态成员 静态字段示例静态成员的生存期 静态函数成员其他静态类成员类型成员常量常量与静态量属性 属性声明和访问器属性示例使用属性属性和 ...

  9. C#图解教程 第七章 类和继承

    类和继承 类继承访问继承的成员所有类都派生自object类屏蔽基类的成员基类访问使用基类的引用 虚方法和覆写方法覆写标记为override的方法覆盖其他成员类型 构造函数的执行 构造函数初始化语句类访 ...

随机推荐

  1. HDU 1724 Ellipse [辛普森积分]

    Ellipse Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Sub ...

  2. ES6,Array.fill()函数的用法

    ES6为Array增加了fill()函数,使用制定的元素填充数组,其实就是用默认内容初始化数组. 该函数有三个参数. arr.fill(value, start, end) value:填充值. st ...

  3. 极大似然估计(MLE)

    基本思想 模型已定,参数未知 根据已存在的样本,挑选(求出)能让样本以最大概率发生的参数 极大似然估计和最小二乘法最大区别之一 极大似然需要知道概率密度函数(离散型叫分布律) 若总体X属离散型,其分布 ...

  4. oracle12c各个版本对其需要的依赖包及系统参数的修改

    本文来自我的github pages博客http://galengao.github.io/ 即www.gaohuirong.cn 以下是我在oracle官网上对oracle12c 各个版本的依赖包需 ...

  5. 微信小程序 sha1 实现密码加密

    在utils中的util.js 文件中增加 函数 实现 字符串转换为16进制加密后的字符串 function encodeUTF8(s) { var i, r = [], c, x; for (i = ...

  6. spring boot 使用java9上传到github其他人clone后报错

    错误原因: Java.lang.NoClassDefFoundError:javax/xml/bind/JAXBException jdk9存在版本兼容问题. 经过查找资料发现问题所在 大致意思是ja ...

  7. win7下通过easyBCD引导安装Ubuntu16.04(并处理遇到的坑)

    Ubuntu16.04作为目前最新版本的ubuntu系统,相信很多人都想在自己的电脑上安装一下,然而系统的安装方法各式各样,u盘法.grub引导法等等,这里我将介绍在win7系统下用easyBCD软件 ...

  8. 高性能javascript笔记

    ----------------------------------------------------------- 第一章 加载和执行 ------------------------------ ...

  9. MySQL之表的数据类型

    一 介绍 存储引擎决定了表的类型,而表内存放的数据也要有不同的类型,每种数据类型都有自己的宽度,但宽度是可选的 详细参考: http://www.runoob.com/mysql/mysql-data ...

  10. AssetBundle实现服务器下载并从本地读取

    废话不多说  直接上代码. 从服务器下载的, 很简单 private IEnumerator Start() { byte[] ab = null; int len = 0; WWW www =nul ...