• 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的方法。

    public delegate void Action();
    public delegate void Action(T arg);
    public delegate void Action(
        T1 arg1, T2 arg2);
    public delegate void Action(
        T1 arg1, T2 arg2, T3 arg3);
    public delegate void Action(
        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();
    public delegate TResult Func(T arg);
    public delegate TResult Func(
        T1 arg1, T2 arg2);
    public delegate TResult Func(
        T1 arg1, T2 arg2, T3 arg3);
    public delegate TResult Func(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( T obj)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    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操作符=>和代码块构成。

    BubbleSort(items, (int first, int second) =>
        {
            return first < second;
        }
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    // ...
        IEnumerable processes = Process.GetProcesses().Where(
            process => { return process.WorkingSet64 > 1000000000; });
        // ...
    
    • 1
    • 2
    • 3
    • 4
    Func getUserInput =
        () =>
        {
            string? input;
            do
            {
                input = Console.ReadLine();
            }
            while(!string.IsNullOrWhiteSpace(input));
            return input!;
        };
    //...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    13.3.2表达式Lambda

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

    //...
    DelegateSample.BubbleSort(items, (first, second) => first < second);
    //...
    
    • 1
    • 2
    • 3

    13.4匿名方法

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

    //...
    DelegateSample.BubbleSort(items,
        delegate(int first, int second)
        {
            return first < second;
        }
    );
    //...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    13.5委托没有结构相等性

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

    13.6外部变量

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

    外部变量的CIL实现

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

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

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

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

        public static void Main()
        {
            var items = new string[] { "Moe", "Larry", "Curly" };
            var actions = new List();
            foreach(string item in items)
            {
                actions.Add(() => { Console.WriteLine(item); });
            }
            foreach(Action action in actions)
            {
                action();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在C#4.0中输出的是

    Curly
    Curly
    Curly
    
    • 1
    • 2
    • 3

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

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

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

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

        public static void Main()
        {
            var items = new string[] { "Moe", "Larry", "Curly" };
            var actions = new List();
            foreach(string item in items)
            {
                string _item = item;
                actions.Add( () => { Console.WriteLine(_item); } );
            }
            foreach(Action action in actions)
            {
                action();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    13.7表达式树

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

    13.7.1Lambda表达式作为数据使用
    persons.Where( person => person.Name.ToUpper() == "INIGO MONTOYA");
    
    • 1

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

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

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

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

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

    EXPRESSION TREE:
        persons.Where(person => person.Name.ToUpper() == "INIGO MONTOYA");
           
    SQL WHERE CLAUSE:
        select * from Person where upper(Name) = 'INIGO MONTOYA';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    13.7.2表达式树作为对象图使用
    13.7.3比较委托和表达式树
    13.7.4检查表达式树
  • 相关阅读:
    有哪些常用的压力测试工具?软件压力测试流程
    模拟实现跨平台方案原理之双线程架构方案
    BSV 上的点对点结算衍生品:远期合约
    pandas是什么以及pandas的三种数据结构Series、DataFrame、MultiIndex的创建方式函数代码
    Java中interrupt的理解(个人)
    ElasticSearch学习笔记-Vector向量搜索记录
    KMP算法
    方舟开服教程win
    Kotlin 协程 - 协程调度器 CoroutineDispatcher
    测试日常工作中需要具备哪些知识和能力,在需求评审时需要考虑哪些方面,在技术方面评审时需要考虑哪些方面,从什么方面进行设计测试用例
  • 原文地址:https://blog.csdn.net/Story_1419/article/details/134457955