撸·委托
委托是寻址方法的.NET版本,在C++中,函数指针只不过是一个指向内存位置的指针,它不是类型安全.我们无法判断这个指针实际指向什么,像参数和返回类型等项就更无从知晓了。而.NET委托完全不同,委托是类型安全的,它定义了返回类型和参数的类型.委托类不仅包含对方法的引用,也可以包含对多个方法的引用。
委托的本质 是class,可以查看IL代码
为什么要用委托
1. 可以将方法当做另一个方法的参数来进行传递,使得程序更容易扩展(比如策略模式--封装变化的部分,所有封装变化的代码都会减少编写代码的工作量)。 2. 把过程的调用转化为对象的调用,充分体现了委托加强了面向对象编程的思想。 3. 使得方法声明和方法实现的分离,充分体现了面向对象的编程思想。委托声明
委托声明决定了可由该委托引用的方法,委托可指向一个与其具有相同标签的方法。 例如,假设有一个委托: ``` public delegate int MyDelegate (string s); ``` 理解委托,需要理解以下几点: 1. 理解委托的一个要点就是它的类型安全非常高,在定义委托时,必须给出它所表示的方法的签名和返回类型等全部细节。 2. 理解委托的一种好方式是把委托当作这样一件事,它给方法的签名和返回类型指定名称。 3. 其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本是定义一个新类,所以可以在定义类的任何相同地方定义委托。 4. 也就是说,可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在名称空间中把委托定义为顶层对象。 5. 根据定义的可见性,和委托的作用域,可以在委托的定义上应用任意常见的访问修饰符:pubic,private,protected等。实例化委托:
给委托变量赋值时,千万不要将方法带括号,带括号意味赋值是方法的结果,所以直接赋值方法名。
系统预定委托
### Action Action:它表示引用一个void返回类型的方法。这个委托类存在不同的变体,可以传递至多16种不同的参数类型。- Acition 属于无参无返回值的函数类型
- Action< T>通过设置泛型,我们可以定义有多个参数, 无返回值的函数
- 当函数有多个重载的时候,系统会自动匹配
- Action是没有返回值的
- 参数也是0或者最多16个
Action | 说明 |
---|---|
Action | 调用没有参数的方法 |
Action<in T1,> | 调用带一个参数的方法, T1后面不带逗号,因为网页显示不出来,所以加上去 |
Action<in T1,in T2> | 调用带两个参数的方法, |
Action<in T1,in T2,in T3> | 调用了带3个参数的方法 |
...... | 直到调用了带16个参数的方法 |
Func
Func:它表示引用一个必有返回类型的方法。这个委托类存在不同的变体,可以传递至多16种不同的参数类型,但必须有一个返回值。
- Func只有带泛型的一种形式,Action有带泛型和不带的两种
- Func 委托必须要带有一个返回值
- 可以有0个或多达16个参数类型
- 最后一个泛型参数代表返回类型,前面的都是参数类型
- 参数类型必须跟指向的方法的参数类型按照顺序对应
Func | 说明 |
---|---|
Func<out TResult,> | 调用没有参数的方法,但是有返回值,TResult后面不带逗号,因为网页显示不出来,所以加上去 |
Action<in T,out TResult > | 调用带一个参数的方法,返回一个值 |
Action<in T1,in T2,out TResult> | 调用带两个参数的方法,返回一个值 |
Action<in T1,in T2,in T3,out TResult> | 调用了带3个参数的方法,返回一个值 |
...... | 直到调用了带16个参数的方法 |
Predicate
Predicate:表示定义一组条件并确定指定对象是否符合这些条件的方法。
此委托由 Array 和 List 类的几种方法使用,用于在集合中搜索元素。
public delegate bool Predicate<in T>(T obj);
类型参数:T
要比较的对象的类型。参数:obj 。要按照由此委托表示的方法中定义的条件进行比较的对象。
返回值:System.Boolean。如果 obj 符合由此委托表示的方法中定义的条件,则为 true;否则为 false。
继承:ObjectDelegatePredicate
示例:
using System;
using System.Drawing;
public class Example
{
public static void Main()
{
// Create an array of Point structures.
Point[] points = { new Point(100, 200),
new Point(150, 250), new Point(250, 375),
new Point(275, 395), new Point(295, 450) };
// Define the Predicate<T> delegate.
Predicate<Point> predicate = FindPoints;
// Find the first Point structure for which X times Y
// is greater than 100000.
Point first = Array.Find(points, predicate);
// Display the first structure found.
Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
}
private static bool FindPoints(Point obj)
{
return obj.X * obj.Y > 100000;
}
}
// The example displays the following output:
// Found: X = 275, Y = 395
下面的示例等同于上一示例中,只不过它使用 lambda 表达式来表示Predicate
using System;
using System.Drawing;
public class Example
{
public static void Main()
{
// Create an array of Point structures.
Point[] points = { new Point(100, 200),
new Point(150, 250), new Point(250, 375),
new Point(275, 395), new Point(295, 450) };
// Find the first Point structure for which X times Y
// is greater than 100000.
Point first = Array.Find(points, x => x.X * x.Y > 100000 );
// Display the first structure found.
Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
}
}
// The example displays the following output:
// Found: X = 275, Y = 395
多播委托
如果要调用多个方法,就需要多次显式调用这个委托。但是委托也可以包含多个方法,这种委托称为多播委托。 如果调用多播委托就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得到委托调用的最后一个方法的结果。 例如: * Action operations=方法一;operartion+=方法二; * 也可以 委托1=方法1;委托二=方法2;委托3=委托1+委托2。所以当调用委托3时,委托1和委托2会同时执行,与第一种实现多播委托一个道理。
但是多播委托还有一个缺陷,一旦一个委托发生异常,其他委托都会停止。为了避免这个问题,应自己迭代方法列表。Delegate类定义GetInvocationList()方法,它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代。(.net core 中间件)
匿名方法
到目前为止,要想使委托工作,方法名必须已经存在。但还有另外一种使用委托的方式:匿名方法。匿名方法是用作委托的参数的一段代码,用匿名方法定义委托的语法与前面的定义并没有区别,但在实例化委托时,就有了区别。下面是一个非常简单的代码,它说明了如何使用匿名方法: ```` Func匿名方法语法
delegate (parameters ){implementationcode};
关键字 参数 方法体
匿名方法不会声明返回值类型。但是匿名方法返回值类型必须和委托返回值一样。
进阶
我们可以使圆括号为空,或省略圆括号来简化匿名方法的参数列表。但是仅在下面两项都为真的情况下才可以这么做。
- 委托的参数列表不包含任何out参数的委托。
- 匿名方法不使用任何参数。
例如:
class Program
{
delegate int otherdel(int param);
public static void Main()
{
otherdel del = delegate
{
cleanup();
printMessage();
};
}
}
params参数
如果委托参数包含params参数,那么params关键字就会被匿名方法的参数列表忽略。如下:
delegate int otherdel(int x,params int y);
otherdel del = delegate(int x,int y)
{
};
Lambda表达式
从c#3.0开始,就可以使用一种新语法把实现代码赋予委托:Lambda表达式 只要有委托参数类型的地方,就可以使用Lambda表达式。前面使用匿名方法的例子可以改为使用Lambda表达式。 Lambda表达式的语法比匿名方法简单.如果所调用的方法有参数,且不需要参数,匿名方法的语法就比较简单,因为这样不需要提供参数定义:Labmda表达式有几种定义参数的方式.
- 如果只一个参数,只写出参数名就足够了。下面的Lambda表达式使用了参数s,因为委托类型定义了一个参数string,所以s的类型就是string。实现代码调用String.Format()方法来返回一个字符串,在调用该委托时就把字符串写到控制台上:
Func<string,string> oneParam=s=>string.Format("change uppercase{0}",s.ToUpper());
Console.WriteLine(oneParam("test"));
- 如果使用多个参数,就把参数名放在花括号中,这里参数x和y的类型是double,由Func<double,double,double>委托定义。例如:
Func<double,double,double> twoParams=(x,y)=>x*y;
console.WriteLine(twoParams(3,2));
- 当然,为了方便,也可以给花括号中的变量添加参数类型,这样如果编译器不能匹配重载后的版本,那么使用参数类型可以帮助找到匹配的委托:
func<double,double,double> twoParamsWithTypes=(double x,double y)=>x*y ;
Console.Write(twoParamsWithTypes(2,3));
- 如果有多行代码.就得添加花括号,return和分号了,而且这比不添加更容易阅读
Func<double,double> square=x=>{ return x*x };
闭包
通过Lambda表达式可以访问表达式外部的变量,这称为闭包。闭包是一个非常好的功能,但如果未正确使用,也会非常危险。 多线程使用时就会出兮兮,因为不知道此时外部变量是什么。
方法泛型集合定义:List<Func<int>>();
Expression
lambda表达式是被视为一个对象的代码(表达式或语句块)的块。它可以作为参数传递给方法,也可以通过方法调用返回。Lambda表达式广泛用于:
- 将要执行的代码传递给异步方法,例如Task.Run(Action)。
- 编写LINQ查询表达式。
- 创建表达式树。
lambda表达式除了可以分配给委托类型外,还可以分配给表达式树类型,如:
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
或者可以直接将其作为方法参数传递:
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
当您使用基于方法的语法来调用System.Linq.Enumerable类中的Enumerable.Select方法时(就像在LINQ to Objects和LINQ to XML中那样),参数是委托类型System.Func <T,TResult>。lambda表达式是创建该委托的最便捷方式。
当您在System.Linq.Queryable类中调用Queryable.Select方法时(就像在LINQ to SQL中那样),参数类型是表达式树类型。同样,lambda表达式只是构造表达式树的一种非常简洁的方式。lambda允许调用看起来类似,但实际上从lambda创建的对象类型是不同的。Expression<Func<TSource,TResult>>Select
表达lambda
在=>运算符右侧具有表达式的lambda表达式称为表达式lambda。表达式lambda广泛用于构建表达树。表达式lambda返回表达式的结果,并采用以下基本形式:
(inputParameters) => expression
只有当lambda有一个输入参数时,括号才是可选的。否则他们是必需的。
使用空括号指定零输入参数:
Action line = () => Console.WriteLine();
两个或多个输入参数用括在括号中的逗号分隔:
Func<int, int, bool> testForEquality = (x, y) => x == y;
有时,编译器无法推断输入类型。您可以显式指定类型,如以下示例所示:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
输入参数类型必须全部显式或全部隐式; 否则,会发生CS0748编译器错误。
表达式lambda的主体可以包含方法调用。但是,如果要创建在.NET公共语言运行库的上下文之外计算的表达式树(例如在SQL Server中),则不应在lambda表达式中使用方法调用。这些方法在.NET公共语言运行库的上下文之外没有任何意义。
声明lambda
语句lambda类似于表达式lambda,除了语句括在括号中:
(input-parameters) => { statement; }
语句lambda的主体可以包含任意数量的语句; 然而,在实践中通常不超过两个或三个。
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
语句lambda与匿名方法一样,不能用于创建表达式树。
异步lambda
您可以使用async和await关键字轻松创建包含异步处理的lambda表达式和语句。例如,以下Windows窗体示例包含一个调用和等待异步方法的事件处理程序ExampleMethodAsync。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}
private async void button1_Click(object sender, EventArgs e)
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
可以使用异步lambda添加相同的事件处理程序。要添加此处理程序,请async在lambda参数列表之前添加修饰符,如以下示例所示:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
Lambda表达式和元组
从C#7.0开始,C#语言为元组提供内置支持。你可以提供一个元组作为lambda表达式的参数,你的lambda表达式也可以返回一个元组。在某些情况下,C#编译器使用类型推断来确定元组组件的类型。
您可以通过在括号中包含逗号分隔的组件列表来定义元组。下面的示例使用带有三个组件的元组将一系列数字传递给lambda表达式,该表达式将每个值加倍并返回包含三个包含乘法结果的组件的元组。
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)
Lambda与标准查询运算符
除了其他实现之外,LINQ to Objects还有一个输入参数,其类型是Func
当参数类型是Expression
以下示例使用Count标准查询运算符:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");
编译器可以推断输入参数的类型,也可以显式指定它。这个特殊的lambda表达式计算那些整数(n),当除以2时,余数为1。
以下示例生成一个序列,其中包含numbers数组中位于9之前的所有元素,因为这是序列中不符合条件的第一个数字:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3
在lambda表达式中键入推断
编写lambda时,通常不必为输入参数指定类型,因为编译器可以根据lambda正文,参数类型和C#语言规范中描述的其他因素推断类型。对于大多数标准查询运算符,第一个输入是源序列中元素的类型。如果要查询IEnumerable
customers.Where(c => c.City == "London");
lambdas类型推断的一般规则如下:
- lambda必须包含与委托类型相同数量的参数。
- lambda中的每个输入参数必须可隐式转换为其对应的委托参数。
- lambda的返回值(如果有的话)必须可以隐式转换为委托的返回类型。
请注意,lambda表达式本身没有类型,因为公共类型系统没有“lambda表达式”的内在概念。然而,有时非常方便地谈论lambda表达式的“类型”。在这些情况下,类型引用lambda表达式转换为的委托类型或表达式类型。
lambda表达式中的变量范围
Lambda可以引用外部变量(请参阅匿名方法),这些变量位于定义lambda表达式的方法的范围内,或者在包含lambda表达式的类型的范围内。以这种方式捕获的变量被存储用于lambda表达式,即使变量否则将超出范围并被垃圾收集。必须明确赋值外部变量,然后才能在lambda表达式中使用它。以下示例演示了以下规则:
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int> updateCapturedLocalVariable;
internal Func<int, bool> isEqualToCapturedLocalVariable;
public void Run(int input)
{
int j = 0;
updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
Console.WriteLine($"Local variable before lambda invocation: {j}");
updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}
public static void Main()
{
var game = new VariableCaptureGame();
int gameInput = 5;
game.Run(gameInput);
int jTry = 10;
bool result = game.isEqualToCapturedLocalVariable(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");
int anotherJ = 3;
game.updateCapturedLocalVariable(anotherJ);
bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}
以下规则适用于lambda表达式中的变量作用域:
- 捕获的变量将不会被垃圾收集,直到引用它的委托才有资格进行垃圾回收。
- lambda表达式中引入的变量在封闭方法中不可见。
- lambda表达式不能直接从封闭方法中捕获in,ref或out参数。
- 一回在lambda表达式语句不会导致封闭方法返回。
如果该跳转语句的目标位于lambda表达式块之外,则lambda表达式不能包含goto,break或continue语句。如果目标在块内,则在lambda表达式块之外有一个跳转语句也是错误的。
表达式树
定义 :将强类型lambda表达式表示为表达式树形式的数据结构。这个类不能被继承。
场景: 目前被大量运行在linq to sql 中。将表达式树转换成表达式,然后转换成SQL。
public sealed class Expression<TDelegate> : System.Linq.Expressions.LambdaExpression
下面的代码示例演示如何将lambda表达式表示为委托形式的可执行代码和表达式树形式的数据。它还演示了如何使用Compile方法将表达式树转换回可执行代码。
// Lambda expression as executable code.
Func<int, bool> deleg = i => i < 5;
// Invoke the delegate and display the output.
Console.WriteLine("deleg(4) = {0}", deleg(4));
// Lambda expression as data in the form of an expression tree.
System.Linq.Expressions.Expression<Func<int, bool>> expr = i => i < 5;
// Compile the expression tree into executable code.
Func<int, bool> deleg2 = expr.Compile();
// Invoke the method and print the output.
Console.WriteLine("deleg2(4) = {0}", deleg2(4));
/* This code produces the following output:
deleg(4) = True
deleg2(4) = True
*/
表达式树是lambda表达式的内存数据表示。表达式树使lambda表达式的结构透明和显式。您可以像处理任何其他数据结构一样与表达式树中的数据进行交互。
将表达式视为数据结构的能力使API能够以可以自定义方式检查,转换和处理的格式接收用户代码。例如,LINQ to SQL数据访问实现使用此工具将表达式树转换为可由数据库评估的Transact-SQL语句。
Queryable类中定义的许多标准查询运算符都有一个或多个Expression
所述的NodeType一个的表达
使用Lambda
方法