C# 委托

委托是类型安全的类,它定义了返回类型和参数的类型,委托类可以包含一个或多个方法的引用。可以使用lambda表达式实现参数是委托类型的方法。

委托

当需要把一个方法作为参数传递给另一个方法时,就需要使用委托。委托是一种特殊类型的对象,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是一个或多个方法的地址。

声明委托类型

声明委托类型就是告诉编译器,这种类型的委托表示的是哪种类型的方法。语法如下:

delegate void delegateTypeName[<T>]([参数列表]);

声明委托类型时指定的参数,就是该委托类型引用的方法对应的参数。

 //声明一个委托类型
private delegate void IntMethodInvoker(int x);
//该委托表示的方法有两个long型参数,返回类型为double
protected delegate double TwoLongsOp(double first, double second);
//方法不带参数的委托,返回string
public delegate string GetString();
public delegate int Comparison<in T>(T left, T right);

(注:我们把上述定义的Comparison<in T>IntMethodInvoker等统称为委托类型。)

在定义委托类型时,必须给出它要引用的方法的参数信息和返回类型等全部细节。声明委托类型的语法和声明方法的语法类似,但没有方法体,并且需要指定delegate关键字。

委托实现为派生自基类System.MulticastDelegate的类,System.MulticastDelegate有派生自基类System.Delegate。因此定义委托类型基本上是定义一个新类,所以可以在定义类的任何相同地方定义委托类型。(可以在类的内部定义委托类型,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象)。

我们从“delegate”关键字开始,因为这是你在使用委托时会使用的主要方法。 编译器在你使用 delegate 关键字时生成的代码会映射到调用 Delegate 和 MulticastDelegate 类的成员的方法调用。

可以在类中、直接在命名空间中、甚至是在全局命名空间中定义委托类型。

建议不要直接在全局命名空间中声明委托类型(或其他类型)。

使用委托

定义委托类型之后,可以创建该类型的实例。 为了便于说明委托是如何将方法进行传递的,针对上述的三个委托类型,分别定义三个方法:

static void ShowInt(int x)
{
Console.WriteLine("这是一个数字:"+x);
}
static double ShowSum(double first,double second)
{
return first + second;
}
//最后一个委托,直接可以使用int.ToString()方法,所以此处不再定义

调用委托有两种形式,一种形式是实例化委托,并在委托的构造函数中传入要引用的方法名(注意仅仅是方法名,不需要带参数),另一种形式是使用委托推断,即不需要显式的实例化委托,而是直接指向要引用的方法名即可,编译器将会自动把委托实例解析为特定的类型。具体示例如下:

public static void Run()
{
int a = 10;
//调用委托形式一
IntMethodInvoker showIntMethod = new IntMethodInvoker(ShowInt);
showIntMethod(a); //调用委托形式二
TwoLongsOp showSumMethod = ShowSum;
double sum= showSumMethod.Invoke(1.23, 2.33);
Console.WriteLine("两数之和:"+sum); //由于int.Tostring()不是静态方法,所以需要指定实例a和方法名ToString
GetString showString = a.ToString;
string str=showString();
Console.WriteLine("使用委托调用a.ToString()方法:"+str);
}

在使用委托调用引用的方法时,委托实例名称后面的小括号需要传入要调用的方法的参数信息。实际上,给委托实例提供圆括号的调用和使用委托类的Invoke()方法完全相同。委托实例showSumMethod最终会被解析为委托类型的一个变量,所以C#编译器会用showSumMethod.Invoke()代替showSumMethod()

委托实例可以引用任何类型的任何对象上的实例方法或静态方法,只要方法的签名匹配委托的签名即可。(所谓签名,指的是定义方法或委托时,指定的参数列表和返回类型)

简单的委托示例

后面的内容将会基于此示例进行扩展,首先定义一个简单的数字操作类MathOperations,代码如下:

internal class MathOperations
{
//显示数值的2倍结果
public static double MultiplyByTwo(double value)
{
double result = value * 2;
Console.WriteLine($"{value}*2={result}");
return result;
}
//显示数值的乘方结果
public static double Square(double value)
{
double result = value * value;
Console.WriteLine($"{value}*{value}={result}");
return result;
}
}

然后定义一个引用上述方法的委托:

delegate double DoubleOp(double x);

如果要使用该委托的话,对应的代码为:

DoubleOp op = MathOperations.MultiplyByTwo;
op(double_num);// 假设double_num为一个double类型的变量

但是很多时候,我们并不是直接这样使用,而是将委托实例作为一个方法(假设该方法为A)的参数进行传入,并且将委托实例引用的方法的参数 作为另一个参数传递给该方法A。将上述代码进行封装转换:

static void ShowDouble(DoubleOp op, double double_num)
{
double result = op(double_num);
Console.WriteLine("值为:"+result);
}

调用该方法:

ShowDouble(MathOperations.MultiplyByTwo, 3);

使用委托一个好的思路就是,先定义普通方法,然后针对该方法定义一个引用该方法的委托,然后写出对应的委托使用代码,接着再将使用的代码用一个新定义的方法进行封装转换,在新的方法参数中,需要指明委托实例和将要为委托实例引用的方法传入的参数(也就是上述示例中的op和double_num),接着就可以在其他地方调用该方法了。

完整的实例代码如下:

delegate double DoubleOp(double x);
static void ProcessAndDisplayNumber(DoubleOp action, double value)
{
double result = action(value);
Console.WriteLine($"Value is {value },result of operation is {result}");
}
public static void Run()
{
DoubleOp[] operations = {
MathOperations.MultiplyByTwo,
MathOperations.Square
};
for (int i = 0; i < operations.Length; i++)
{
Console.WriteLine($"Using operations[{i}]:");
ProcessAndDisplayNumber(operations[i], 2);
ProcessAndDisplayNumber(operations[i], 3);
ProcessAndDisplayNumber(operations[i], 4);
}
}

Action<T>Func<T>Predicate<T>委托

泛型Action<T>委托表示引用一个void返回类型的方法。 该委托类最多可以为将要引用的方法传递16种不同的参数类型。

泛型Func<T>委托表示引用一个带有返回值类型的方法。该委托类最多可以为将要引用的方法传递16中不同的参数类型,其中最后一个参数代表的是将要引用的方法的返回值类型。

泛型Predicate<T> 用于需要确定参数是否满足委托条件的情况。 也可将其写作 Func<T, bool> 。例如:

Predicate<int> pre = b => b > 5;

此处只对Action<T>Func<T>做详细说明。

有了这两个委托类,在定义委托时,就可以省略delegate关键字,采用新的形式声明委托。

Func<double,double> operations = MathOperations.MultiplyByTwo;
Func<double, double>[] operations2 ={
MathOperations.MultiplyByTwo,
MathOperations.Square
};
static void ProcessAndDisplayNumber(Func<double, double> action, double value)
{
double result = action(value);
Console.WriteLine($"Value is {value },result of operation is {result}");
}

下面使用一个示例对委托的用途进行说明,首先定义一个普通的方法,该方法是冒泡排序的另一种写法:

public static void Sort(int[] sortArray)
{
bool swapped = true;
do
{
swapped = false;
for (int i = 0; i < sortArray.Length - 1; i++)
{
if (sortArray[i] > sortArray[i + 1])
{
int temp = sortArray[i];
sortArray[i] = sortArray[i + 1];
sortArray[i + 1] = temp;
swapped = true;
}
}
} while (swapped);
}

上述方法中,接收的参数局限于数值,为了扩展 使其支持对其他类型的排序,并且不仅仅是升序,对该方法进行泛型改写,并使用泛型委托。

internal class BubbleSorter
{
public static void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison)
{
bool swapped = true;
do
{
swapped = false;
for (int i = 0; i < sortArray.Count - 1; i++)
{
if (comparison(sortArray[i + 1], sortArray[i]))
{
T temp = sortArray[i];
sortArray[i] = sortArray[i + 1];
sortArray[i + 1] = temp;
swapped = true;
}
}
} while (swapped);
}
}

上述方法中的参数comparison是一个泛型委托,将要引用的方法带有两个参数,类型和T相同,值可以来自于sortArray,并返回bool类型值,因此实际调用该委托时,不用单独的为泛型类型传入参数,直接使用sortArray中的项即可。

为了更好的调用该方法,定义如下类:

internal class Employee
{
public string Name { get; set; }
public decimal Salary { get; private set; } public override string ToString() => $"{Name},{Salary:C}"; public Employee(string name, decimal salary)
{
this.Name = name;
this.Salary = salary;
}
//为了匹配Func<T,T,bool>委托,定义如下方法
public static bool CompareSalary(Employee e1, Employee e2) => e1.Salary < e2.Salary;
}

使用该类:

Employee[] employees = {
new Employee("小明",8000),
new Employee("小芳",9800),
new Employee("小黑",4000),
new Employee("小米",13000),
new Employee("小马",12000)
};
//调用排序
BubbleSorter.Sort(employees, Employee.CompareSalary);
ForeachWrite(employees); //输出结果,该方法的定义如下:
public static void ForeachWrite<T>(T[] list)
{
foreach (T item in list)
{
Console.WriteLine(item.ToString());
}
}

多播委托

一个委托包含多个方法的调用,这种委托称为多播委托。多播委托可以识别运算符“+”和“+=“(在委托中添加方法的调用)以及”-“和”-=“(在委托中删除方法的调用)。

多播委托实际上是一个派生自 System.MulticastDelegate的类,而System.MulticastDelegate又派生自基类System.DelegateSystem.MulticastDelegate的其他成员允许把多个方法调用链接为一个列表。

internal class MathOperations_V2
{
public static void MultiplyByTwo(double value)
{
double result = value * 2;
Console.WriteLine($"{value}*2={result}");
} public static void Square(double value)
{
double result = value * value;
Console.WriteLine($"{value}*{value}={result}");
}
}

针对上述方法定义一个带有泛型委托的方法:

private static void ProcessAndDisplayNumber(Action<double> action, double value)
{
Console.WriteLine("调用ProcessAndDisplayNumber方法:value=" + value);
action(value);
}

使用多播委托的形式进行调用:

Action<double> operations = MathOperations_V2.MultiplyByTwo;
operations += MathOperations_V2.Square; ProcessAndDisplayNumber(operations, 3);
ProcessAndDisplayNumber(operations, 4);
ProcessAndDisplayNumber(operations, 5);

上述在调用方法时,会依次执行MathOperations_V2.MultiplyByTwoMathOperations_V2.Square

注意:在使用多播委托时,多播委托包含一个逐个调用的委托集合,一旦通过委托调用的其中一个方法抛出一个异常,整个迭代就会停止。

private static void One()
{
Console.WriteLine("调用One()方法");
throw new Exception("Error in one");
}
static void Two()
{
Console.WriteLine("调用Two()方法");
}
public static void Run()
{
Action d1 = One;
d1 += Two;
try
{
d1();
}
catch (Exception)
{
Console.WriteLine("调用d1出错了");
}
}

上述使用了多播委托,一旦One出现了异常,Two并不能够继续执行。因为第一个方法抛出了一个异常,委托迭代就会停止,不再调用Two()方法。为了避免这个问题,应自己迭代方法列表。Delegate类定义GetInvocationList()方法,返回Delegate对象数组,可以迭代这个数组进行方法的执行:

public static void Run2()
{
Action d1 = One;
d1 += Two;
Delegate[] delegates = d1.GetInvocationList();
foreach (Action d in delegates)
{
try
{
d();
}
catch (Exception)
{
Console.WriteLine("调用出错了!!");
}
}
}

上述迭代,即使第一个方法出错,依然就执行第二个方法。

匿名方法和Lambda表达式

匿名方法是用作委托的参数的一段代码。

string start = "厉害了,";
Func<string, string> print = delegate (string param)
{
return start + param;
};
Console.WriteLine(print("我的国!"));

在该示例中,Func<string,string>委托接受一个字符串参数,返回一个字符串。print是这种委托类型的变量。不要把方法名赋予这个变量,而是使用一段简单的代码:前面是关键字delegate,后面是一个字符串参数。

匿名方法的优点是减少了要编写的代码,但代码的执行速度并没有加快。

使用匿名方法时,在匿名方法中不能使用跳转语句(breakgotocontinue)调到该匿名方法的外部,也不能在匿名方法的外部使用跳转语句调到匿名方法的内部。并且不能访问在匿名方法外部使用的refout参数。

实际使用中,不建议使用上述的方式定义匿名方法,而是使用lambda表达式。

只要有委托参数类型的地方,就可以使用lambda表达式,将上述示例改为lambda表达式,代码如下:

//使用Lambda表达式进行匿名方法的定义
string start = "厉害了,";
Func<string, string> lambda = param => start + param;
Console.WriteLine(lambda("我的C#!!!"));

使用lambda表达式规则:

参数

只有一个参数时,可以省略小括号

Func<string, string> oneParam = s => $"将{s}转换为大写:" + s.ToUpper();
//调用
Console.WriteLine(oneParam("abc"));

没有参数或者有多个参数时必须使用小括号

//无参数
Action a = () => Console.WriteLine("无参数");
a();
//多个参数,在小括号中指定参数类型
Func<double, double, double> twoParamsWithTypes = (double x, double y) => x + y;
//调用
Console.WriteLine("2.3+1.3=" + twoParamsWithTypes(2.3, 1.3));

多行代码

如果lambda表达式只有一条语句,在方法块内就不需要花括号({})和return语句,因为编译器会添加一条隐式的return语句。如果lambda表达式有多条语句,必须显式的添加花括号或return语句。例如:

Func<string, string, string> joinString = (str1, str2) =>
{
str1 += str2;
return str1.ToUpper();
};
Console.WriteLine(joinString("abc", "def"));

闭包

在lambda表达式的内部使用表达式外部的变量,称为闭包。使用闭包需要注意的一点就是 ,如果在表达式中修改了闭包的值,可以在表达式的外部访问已修改的值 。

委托和 MulticastDelegate 类

System.Delegate 类及其单个直接子类 System.MulticastDelegate 可提供框架支持,以便创建委托、将方法注册为委托目标以及调用注册为委托目标的所有方法。

有趣的是,System.Delegate 和 System.MulticastDelegate 类本身不是委托类型。 它们为所有特定委托类型提供基础。 相同的语言设计过程要求不能声明派生自 Delegate 或 MulticastDelegate 的类。 C# 语言规则禁止这样做。

相反,C# 编译器会在你使用 C# 语言关键字声明委托类型时,创建派生自 MulticastDelegate 的类的实例。

要记住的首要且最重要的事实是,使用的每个委托都派生自 MulticastDelegate。 多播委托意味着通过委托进行调用时,可以调用多个方法目标。

C#基础提升系列——C#委托的更多相关文章

  1. C#基础提升系列——C#异步编程

    C#异步编程 关于异步的概述,这里引用MSDN的一段文字: 异步编程是一项关键技术,使得能够简单处理多个核心上的阻塞 I/O 和并发操作. 如果需要 I/O 绑定(例如从网络请求数据或访问数据库),则 ...

  2. C#基础提升系列——C#任务和并行编程

    C#任务和并行编程 我们在处理有些需要等待的操作时,例如,文件读取.数据库或网络访问等,这些都需要一定的时间,我们可以使用多线程,不需要让用户一直等待这些任务的完成,就可以同时执行其他的一些操作.即使 ...

  3. C#基础提升系列——C#任务同步

    C#任务同步 如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态.如果不注意同步,就会出现争用条件和死锁. 不同步导致的线程问题 如果两个或多个线程访问相同的对象,并且对共享 ...

  4. C#基础提升系列——C#文件和流

    C#文件和流 本文主要是对C#中的流进行详细讲解,关于C#中的文件操作,考虑到后期.net core跨平台,相关操作可能会发生很大变化,所以此处不对文件系统(包括目录.文件)过多的讲解,只会描述出在. ...

  5. C#基础提升系列——C# 泛型

    C# 泛型(Generics) 泛型概述 泛型是C#编程语言的一部分,它与程序集中的IL(Intermediate Language,中间语言)代码紧密的集成.通过泛型,我们不必给不同的类型编写功能相 ...

  6. C#基础提升系列——C#集合

    C#集合 有两种主要的集合类型:泛型集合和非泛型集合. 泛型集合被添加在 .NET Framework 2.0 中,并提供编译时类型安全的集合. 因此,泛型集合通常能提供更好的性能. 构造泛型集合时, ...

  7. C# 基础知识系列- 11 委托和事件

    0. 前言 事件和委托是C#中的高级特性,也是C#中很有意思的一部分.出现事件的地方,必然有委托出现:而委托则不一定会有事件出现.那为什么会出现这样的关系呢?这就需要从事件和委托的定义出发,了解其中的 ...

  8. C#基础提升系列——C# LINQ

    C# LINQ LINQ(Language Integrated Query,语言集成查询).在C# 语言中集成了查询语法,可以用相同的语法访问不同的数据源. 命名空间System.Linq下的类En ...

  9. C# 基础知识系列- 12 任务和多线程

    0. 前言 照例一份前言,在介绍任务和多线程之前,先介绍一下异步和同步的概念.我们之间介绍的知识点都是在同步执行,所谓的同步就是一行代码一行代码的执行,就像是我们日常乘坐地铁通过安检通道一样,想象我们 ...

随机推荐

  1. RabbitMQ核心概念和AMQP协议(二)

    RabbitMQ是什么? RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议,在完全不同的应用之间共享数据,RabbirMQ是使用Erlang语言来编写的,并且RabbitMQ是基于A ...

  2. WINDOWS2008server安全策略设置

    一.防止黑客或恶意程序暴力破解我的系统密码 答: 暴力破解Windows密码实质上是通过穷举算法来实现,尤其是密码过于简单的系统,暴力破解的方法还是比较实用的.有一点需要我们注意,这个问题的关键在于W ...

  3. cookie字段属性解析

    一个域名下面可能存在着很多个cookie对象.如果我们用selenium的get_cookies方法,可以得到当前浏览器的多个cookie,比如: {'name': 'QCARJSESSIONID', ...

  4. What code you will get when you create a wcf library

    创建wcf服务库的时候,系统自动生成的代码 // 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的接口名“IService1”. [ServiceContract] publ ...

  5. Jsoup代码示例、解析网页+提取文本

    使用Jsoup解析HTML 那么我们就必须用到HttpClient先获取到html 同样我们引入HttpClient相关jar包 以及commonIO的jar包 我们把httpClient的基本代码写 ...

  6. 使用Flask+nginx+uwsgi+Docker部署python应用

    https://www.cnblogs.com/vh-pg/p/11731637.html

  7. 《图解设计模式》读书笔记2-2 Factory Method模式

    目录 类图 代码 角色介绍 思想 类图 代码 //产品类,任意可"use"的产品都可继承该类 public abstract class Product { public abst ...

  8. Mac010--IDEA安装及应用

    Mac--IDEA安装及应用 应用IDEA,首先确保已安装如下环境: JDK:JDK是整个java开发的核心,它包含了JAVA的运行环境,JAVA工具和JAVA基础的类库(安装 & 配置环境变 ...

  9. jQuery基础--音乐视频操作

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  10. 贪吃蛇大作战canvas实现(手机触屏操作)--地图逻辑

    //html部分 <!DOCTYPE html><html><head lang="en"> <meta charset="UT ...