C#8.0本质论第十三章--委托和Lambda表达式

C#8.0本质论第十三章--委托和Lambda表达式

13.1委托概述

C#使用委托提供类似C++里函数指针的功能。委托允许捕捉对方法的引用。

13.1.1背景
13.1.2委托数据类型

13.2声明委托类型

使用delegate关键字声明委托类型

13.2.1常规用途的委托类型:System.Func和System.Action

System.Func系列委托代表有返回值的方法,而System.Action系列代表返回void的方法。

c# 复制代码
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(
    T1 arg1, T2 arg2);
public delegate void Action<in T1, in T2, in T3>(
    T1 arg1, T2 arg2, T3 arg3);
public delegate void Action<in T1, in T2, in T3, in T4>(
    T1 arg1, T2 arg2, T3 arg3, T4 arg4);
 
 ...
 
public delegate void Action<
    in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8,
    in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16>(
        T1 arg1, T2 arg2, T3 arg3, T4 arg4,
        T5 arg5, T6 arg6, T7 arg7, T8 arg8,
        T9 arg9, T10 arg10, T11 arg11, T12 arg12,
        T13 arg13, T14 arg14, T15 arg15, T16 arg16);
 
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(
    T1 arg1, T2 arg2);
public delegate TResult Func<in T1, in T2, in T3, out TResult>(
    T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<in T1, in T2, in T3, in T4,
    out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
 
 ...
 
public delegate TResult Func<
    in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8,
    in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16,
    out TResult>(
        T1 arg1, T2 arg2, T3 arg3, T4 arg4,
        T5 arg5, T6 arg6, T7 arg7, T8 arg8,
        T9 arg9, T10 arg10, T11 arg11, T12 arg12,
        T13 arg13, T14 arg14, T15 arg15, T16 arg16);
 
public delegate bool Predicate<in T>( T obj)

Func的最后一个类型参数是TResult,即返回值的类型。

清单最后一个委托是Predicate< in T >。若用一个Lambda返回bool,则该Lambda称为谓词(predicate)通常用谓词筛选或标识集合中的数据项。

13.2.2实例化委托

委托是引用类型,但不需要用new实例化。从C#2.0开始,从方法组(为方法命名的表达式)向委托类型的转换会自动创建新的委托对象。

委托实际是特殊的类。虽然C#标准没有明确说明类的层次结构,但委托必须直接或间接派生自System.Delegate。

第一个属性是System.Reflection.MethodInfo类型,描述方法签名,包括名称、参数和返回类型。除了MethodInfo,委托还需要一个对象实例来包含要调用的方法。这正是第二个属性Target的作用。在静态方法的情况下,Target为null。

所有委托都不可空(immutable)。换言之,委托创建好后无法更改。

13.3Lambda表达式

C#2.0引入了匿名方法 ,C#3.0引入了Lambda表达式 。这两种语法统称为匿名函数。这两种都合法,但应该优先使用Lambda表达式。

Lambda表达式的目的是在需要基于很简单的方法生成委托时,避免声明全新成员的麻烦。

Lambda表达式分成两种:语句Lambda表达式Lambda

13.3.1语句Lambda

语句Lambda由形参列表、Lambda操作符=>和代码块构成。

c# 复制代码
BubbleSort(items, (int first, int second) =>
    {
        return first < second;
    }
);

假如只有一个参数,且类型可以推断,Lambda表达式就可拿掉围绕参数列表的圆括号。但假如无参,或参数不止一个,或者包含显示指定了类型的单个参数,Lambda表达式就必须将参数列表放到圆括号中。

c# 复制代码
// ...
    IEnumerable<Process> processes = Process.GetProcesses().Where(
        process => { return process.WorkingSet64 > 1000000000; });
    // ...
c# 复制代码
Func<string> getUserInput =
    () =>
    {
        string? input;
        do
        {
            input = Console.ReadLine();
        }
        while(!string.IsNullOrWhiteSpace(input));
        return input!;
    };
//...
13.3.2表达式Lambda

只需要包含要返回的表达式,完全没有语句块。

c# 复制代码
//...
DelegateSample.BubbleSort(items, (first, second) => first < second);
//...

13.4匿名方法

匿名方法必须显式指定每个参数的类型,而且必须有代码块。在参数列表前添加delegate关键字

c# 复制代码
//...
DelegateSample.BubbleSort(items,
    delegate(int first, int second)
    {
        return first < second;
    }
);
//...

13.5委托没有结构相等性

.NET委托类型不具有结构相等性(structural equality),也就是说,不能将一个委托类型的对象引用转换成一个不相关的委托类型,即使两者的形参和返回类型完全一致。唯一的办法就算创建新委托并让它引用旧委托的Invoke方法。

13.6外部变量

在Lambda表达式外部声明的局部变量称为该Lambda的外部变量。如果Lambda表达式主体使用一个外部变量,那么就说该变量被该Lambda表达式捕获。被捕捉的变量的生存期被延长了。

外部变量的CIL实现

c# 复制代码
public class Program
{
    // ...
    private sealed class __LocalsDisplayClass_00000001
    {
        public int comparisonCount;
        public bool __AnonymousMethod_00000000(
            int first, int second)
        {
            comparisonCount++;
            return first < second;
        }
    }
 
    public static void Main()
    {
        __LocalsDisplayClass_00000001 locals = new();
        locals.comparisonCount = 0;
        int[] items = new int[5];
 
        for (int i = 0; i < items.Length; i++)
        {
            Console.Write("Enter an integer: ");
            string? text = Console.ReadLine();
            if (!int.TryParse(text, out items[i]))
            {
                Console.WriteLine($"'{text}' is not a valid integer.");
                return;
            }
        }
 
        DelegateSample.BubbleSort
            (items, locals.__AnonymousMethod_00000000);
        for (int i = 0; i < items.Length; i++)
        {
            Console.WriteLine(items[i]);
        }
 
        Console.WriteLine("Items were compared {0} times.",
            locals.comparisonCount);
    }
}

注意,被捕捉的局部变量永远不会被"传递"或"拷贝"到别的地方。相反,作为实例字段实现,从而延长了生存期。

生成的__LocalsDisplayClass类称为闭包(closure),它是一个数据结构(一个C#类),其中包含一个表达式以及对表达式进行求值所需的变量(C#中的公共字段)。

在C#5.0中捕捉循环变量:

c# 复制代码
    public static void Main()
    {
        var items = new string[] { "Moe", "Larry", "Curly" };
        var actions = new List<Action>();
        foreach(string item in items)
        {
            actions.Add(() => { Console.WriteLine(item); });
        }
        foreach(Action action in actions)
        {
            action();
        }
    }

在C#4.0中输出的是

Curly
Curly
Curly

Lambda表达式捕捉变量并总是使用其最新的值--而不是捕捉并保留变量在委托创建时的值。这正是你希望的行为。

捕捉循环变量时,每个委托都捕捉同一个循环变量。循环变量变化时,捕捉它的每个委托都看到了变化。所以无法指责C#4.0的行为。

C#5.0对此进行了更改,认为每一次循环迭代,foreach循环变量都应该是"新"变量。所以,每次创建委托,捕捉的都是不同的变量,不再共享同一个变量。但注意,这个更改不适用于for循环。

在C#5.0之前的版本中应使用下面的模式:

c# 复制代码
    public static void Main()
    {
        var items = new string[] { "Moe", "Larry", "Curly" };
        var actions = new List<Action>();
        foreach(string item in items)
        {
            string _item = item;
            actions.Add( () => { Console.WriteLine(_item); } );
        }
        foreach(Action action in actions)
        {
            action();
        }
    }

13.7表达式树

表达式Lambda还能转换成表达式树,表达式树也是对象,允许传递编译器对Lambda表达式的分析。

13.7.1Lambda表达式作为数据使用
c# 复制代码
persons.Where( person => person.Name.ToUpper() == "INIGO MONTOYA");

假定persons是Person的一个数组,和Lambda表达式实参对应的Where方法形参具有委托类型Func< Person,bool >。编译器生成方法来包含Lambda表达式主体代码,再创建委托实例来代表所生成的方法,并将委托传给Where方法。Where方法返回一个查询对象,一旦执行查询,就将委托应用于数组的每个成员来判断查询结果。

现在假定persons是代表远程数据库表的对象,表中有数百万人的数据,客户端如何请求查询结果?

一个技术是将几百万数据从服务器传输到客户端,每一行都创建Person对象,根据Lambda创建委托,再针对每个Person执行委托。但代价过于高昂。

第二个技术则要好很多,它是将Lambda的含义发送给服务器。服务器只将符合条件的少数几行传输到客户端,而不是创建几百万个对象。但怎样将Lambda的含义发送给服务器呢?

这正是在语言中添加表达式树的原因。转换成表达式树的Lambda表达式对象代表的是对Lambda表达式进行描述的数据,而不是编译好的、用于实现匿名函数的代码。

c# 复制代码
EXPRESSION TREE:
    persons.Where(person => person.Name.ToUpper() == "INIGO MONTOYA");
       
SQL WHERE CLAUSE:
    select * from Person where upper(Name) = 'INIGO MONTOYA';
13.7.2表达式树作为对象图使用
13.7.3比较委托和表达式树
13.7.4检查表达式树
相关推荐
坚硬果壳_1 分钟前
《硬件架构的艺术》笔记(一):亚稳态
笔记·学习
清风fu杨柳23 分钟前
centos7 arm版本编译qt5.6.3详细说明
开发语言·arm开发·qt
醉颜凉25 分钟前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
糊涂君-Q27 分钟前
Python小白学习教程从入门到入坑------第三十一课 迭代器(语法进阶)
python·学习·程序人生·考研·职场和发展·学习方法·改行学it
_小柏_28 分钟前
C/C++基础知识复习(20)
开发语言
qiyi.sky30 分钟前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
程序员小明z37 分钟前
基于Java的药店管理系统
java·开发语言·spring boot·毕业设计·毕设
dal118网工任子仪1 小时前
web安全漏洞之ssrf入门
笔记·学习·计算机网络·网络安全
键盘敲没电1 小时前
【iOS】知乎日报前三周总结
学习·ios·objective-c·xcode
Lyqfor1 小时前
云原生学习
java·分布式·学习·阿里云·云原生