撸·委托

C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量,引用可在运行时被改变。 委托(Delegate)特别用于实现事件和回调方法,所有的委托(Delegate)都派生自 System.Delegate 类。
  委托是寻址方法的.NET版本,在C++中,函数指针只不过是一个指向内存位置的指针,它不是类型安全.我们无法判断这个指针实际指向什么,像参数和返回类型等项就更无从知晓了。而.NET委托完全不同,委托是类型安全的,它定义了返回类型和参数的类型.委托类不仅包含对方法的引用,也可以包含对多个方法的引用。
> Lambda表达式与委托直接相关,当参数是委托类型时,就可以使用Lambda表达式引用方法.

委托的本质 是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委托。 每个元素points数组传递给 lambda 表达式,该表达式查找符合搜索条件的元素之前。 在这种情况下,lambda 表达式返回true是否大于 100,000 的 X 和 Y 字段。

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 anonDel=delegate(string param) { param+=mid; param+=" and this was added to the string."; return param; } ```` 使用匿名方法的规则很明显,它前面是关键字delegate,后面是一个字符串参数. > 使用匿名方法 1. 声明委托变量时候作为初始化表达式。 2. 组合委托时在赋值语句的右边。 3. 为委托增加事件时在赋值语句的右边。

匿名方法语法

delegate (parameters ){implementationcode};
关键字  参数        方法体

匿名方法不会声明返回值类型。但是匿名方法返回值类型必须和委托返回值一样。

进阶

我们可以使圆括号为空,或省略圆括号来简化匿名方法的参数列表。但是仅在下面两项都为真的情况下才可以这么做。

  1. 委托的参数列表不包含任何out参数的委托。
  2. 匿名方法不使用任何参数。
    例如:
 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表达式有几种定义参数的方式.

  1. 如果只一个参数,只写出参数名就足够了。下面的Lambda表达式使用了参数s,因为委托类型定义了一个参数string,所以s的类型就是string。实现代码调用String.Format()方法来返回一个字符串,在调用该委托时就把字符串写到控制台上:
Func<string,string> oneParam=s=>string.Format("change uppercase{0}",s.ToUpper());

Console.WriteLine(oneParam("test"));
  1. 如果使用多个参数,就把参数名放在花括号中,这里参数x和y的类型是double,由Func<double,double,double>委托定义。例如:
Func<double,double,double> twoParams=(x,y)=>x*y;

console.WriteLine(twoParams(3,2));
  1. 当然,为了方便,也可以给花括号中的变量添加参数类型,这样如果编译器不能匹配重载后的版本,那么使用参数类型可以帮助找到匹配的委托:
func<double,double,double> twoParamsWithTypes=(double x,double y)=>x*y ; 

Console.Write(twoParamsWithTypes(2,3));
  1. 如果有多行代码.就得添加花括号,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 系列泛型委托之一。这些委托使用类型参数来定义输入参数的数量和类型,以及委托的返回类型。Func委托对于封装应用于一组源数据中的每个元素的用户定义表达式非常有用。例如,考虑Func <T,TResult>委托类型:

当参数类型是Expression 时,您还可以提供lambda表达式,例如在Queryable类型中定义的标准查询运算符中。指定Expression 参数时,lambda将编译为表达式树。
以下示例使用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,则输入变量被推断为Customer对象,这意味着您可以访问其方法和属性:

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。

使用Lambda (Expression,IEnumerable )或Lambda (Expression,ParameterExpression [])方法创建Expression 对象。

方法

posted @ 2019-05-24 11:25  天空的湛蓝  阅读(341)  评论(0编辑  收藏  举报