(23)认识Linq:序列与集合,延迟与缓存,链式与查询式,性能提升技巧

    本文参考https://www.bilibili.com/video/BV1rx41157DS/?p=6&spm_id_from=333.880.my_history.page.click&vd_source=2a0404a7c8f40ef37a32eed32030aa18
    
    难度:中级


一、Enumerables与IEnumerable


    1、什么是Enumerables?
    
        (1)Enumerables是一个用于处理集合数据的接口或者类型。
            
            它是System.Collections命名空间下提供的一组功能强大的接口和类的集合。通过使用Enumerables,你可以遍历、过滤、转换和操作各种集合,比如列表(List)、数组(Array)以及其他实现了"IEnumerable"接口的类型。
            
            Enumerables提供了一种方便的方法来处理集合数据,它包含了许多扩展方法(Extension Methods),使得对集合的操作变得更加简单和高效。你可以使用Linq查询表达式或者方法链(Method Chaining)来使用Enumerables,以实现各种常用的操作,如过滤、映射、排序等。总之,Enumerables在C#中是一个非常有用的工具,它简化了集合数据的处理和操作。
        
        (2)Enumerables与Enumerable表达的意思是一样的。
            它们都是用来表示可枚举序列的概念。
            说Enumerable时,通常指的是Enumerable类及其提供的扩展方法。
            而说Enumerables时,通常指的是一组可枚举序列的集合。
    
    
    2、Enumerables与IEnumerable区别?
        
        可以将 Enumerable 视为包含了丰富集合操作的工具箱,提供了许多可用于操作和处理集合的扩展方法。这些方法提供了丰富的功能,包括筛选、映射、排序、分组等等,使得对集合进行操作变得更加方便和灵活。

        而IEnumerable 接口只是 Enumerable 工具箱中的一项功能,它定义了一个最基本的功能:允许对集合进行迭代。IEnumerable 接口提供了 GetEnumerator() 方法,该方法返回一个 IEnumerator 对象,用于遍历集合中的元素。在 C# 中,foreach 循环就是利用这个接口来迭代集合的。        所以,Enumerable 提供了 Enumerable.Range()、Enumerable.Where()、Enumerable.OrderBy() 等丰富的扩展方法,而 IEnumerable 接口只提供了最基本的迭代功能。
        
        总结:IEnumerable 是用于表示一个可循环迭代的集合的基本接口;
            而 Enumerables 则是提供了一整套功能丰富的工具方法,用于操作和处理实现了 IEnumerable 接口的集合。

        private static void Main(string[] args)
        {
            List<int> list = new List<int>() { 1, 2, 3, 4, 5 };
            IEnumerable<int> filter = Enumerable.Where<int>(list, x => x % 2 == 0);//a
            IEnumerable<int> map = Enumerable.Select<int>(list, x => 2 * x);//b

            Console.WriteLine("filter:" + string.Join(",", filter.ToArray()));//c
            Console.WriteLine("filter:" + string.Join(",", filter.ToArray()));
        }


        上面使用Enumerable类的静态方法Where和Select,对numbers集合进行操作。
        可以看到,Enumerable是一个静态类,提供了一系列扩展方法,用于对IEnumerable类型的对象进行操作。
        
        问:a处Where<int>是否可以省略int?
        答:可以。因为可以由后面的第二参数自动推荐Where<T>中T为int类型。
        
        问:b处Select<int>,省略后是正确的,但加上后反而错误了,为什么?
        答:因为a处只需要一个参数where<Tresult>,所以加上<int>可省略都有道理。
            而b处需要两个参数Select<Tsource,Tresult>,省略时可以正确。但加上时,必须加两个。
            
            加上: IEnumerable<int> map = Enumerable.Select<int,int>(list, x => 2 * x); 
            省略:IEnumerable<int> map = Enumerable.Select(list, x => 2 * x);
            上面两个都是正确的。
            
        
    3、一组元素序列,说的是集合还是数组?
        
        一组元素序列可以指代一个集合,也可以指代IEnumerable<T>接口的实现对象。
        在C#中,集合是一种常见的数据结构,用于存储和操作一组元素。常见的集合类型包括List<T>、HashSet<T>、Dictionary<TKey, TValue>等。

        另一方面,IEnumerable<T>接口是C#中用于表示可枚举序列的接口。它定义了一个方法GetEnumerator(),该方法返回一个IEnumerator<T>对象,用于遍历序列中的元素。IEnumerable<T>接口的实现对象可以是集合,也可以是其他类型的序列,比如数组或者自定义的序列类。 另外一个,说遍历常对应的是集合或数组(已经存在),说枚举一般对的是序列,因为序列是延后执行,自己也不清楚自己有多少元素,直到用时它才执行。但平时我们都会混合这种概念,因为有时没有必须纠结,随着学习深入自然会明白两者的区别。

二、IEnumerator枚举器


    1、IEnumerator 是一个接口,它定义了用于遍历集合的方法。
    
        IEnumerator 接口提供了两个主要的方法:MoveNext() 和 Reset(),以及一个属性 Current。
        通过实现 IEnumerator 接口,可以创建一个可枚举的对象,用于在集合上进行迭代。

        public interface IEnumerator
        {
            bool MoveNext();//
            void Reset();
            object Current { get; }
        }   

 
        MoveNext() 将枚举器推进到集合的下一个元素,成功返回 true,否则false。
        Reset() 将枚举器重置到集合的起始位置。
        Current 属性用于获取集合中当前位置的元素。
    
    
    2、yield
    
        用于定义迭代器方法,它可以将一个方法转换为一个可以生成序列的特殊方法。
        
        IEnumerator 是一个标准接口,用于实现迭代器对象。yield 可以简化迭代器方法的实现,而 IEnumerator 则需要手动实现和维护迭代状态。
    
        yield与IEnumerator的关系,有点类似Action与Delegate的关系,特殊化与标准化的关系。
        
        
        问:下面为什么不能输出"最终"两个字符?

        private static void Main(string[] args)
        {
            IEnumerable<int> list = CreatColl();
            IEnumerator<int> cr = list.GetEnumerator();
            for (int i = 0; i < 10; i++)
            {
                if (cr.MoveNext())
                    Console.WriteLine(cr.Current);
                else
                    break;
            }
            Console.ReadKey();
        }

        private static IEnumerable<int> CreatColl()
        {
            try
            {
                for (int i = 0; i < int.MaxValue; i++)
                { yield return i; }
            }
            finally
            {
                Console.WriteLine("最终");
            }
        }


        答:当使用 yield return 语句返回元素时,迭代器方法会在每次迭代请求时执行,然后在下一次迭代请求之前暂停。一直就在迭代与返回之间执行,也就是一直在迭代器方法的执行流程中,程序并没有“结束”,除非整个迭代流程执行完成,才会进入到finally中执行“最终”.
            修改上面int.MaxValue(这个数太大,一般迭代不会终结),改为5,于是结果为:
            0
            1
            2
            3
            4
            最终
        重要:上面可以看到返回的结果并不是一次性的,而是按需要返回.
            
            当使用yield return语句时,它可以暂停并返回给调用者一个值,然后继续执行代码直到下一个yield return语句。这种行为类似于暂时跳转到主调函数,然后在下一个迭代中恢复执行。
            
            在使用yield return时,方法的栈并没有出栈,而是在每次迭代后保留其状态。只有当所有yield return执行完成后,方法的栈才会清空,才会真正地返回到主调函数。这使得yield return非常适用于处理大量数据并且不需要一次性加载全部数据到内存中的情况。
            
        注意:一个方法中yield return与return不能同时出现。
    
    
    3、流
    
        可以看到,上面返回的并不是整个集合,而是一个一个追加的序列。
        
        问:序列和集合有什么区别?
        答:集合类似为湖中的一部分水,或许是全部,它已经存在(缓存);
            序列类比为湖中的这部分水的输送流(不存在,需逐个枚举才能形成湖水)。与文件流的概念类似。

            集合是指一组对象的容器,它可以存储和操作多个元素。
            集合通常具有添加、删除、查找、排序等操作,可以随机访问集合中的元素。
            集合可以是有序的(如列表、数组)或无序的(如集、散列表),并且可以包含重复的元素。
            集合通常是静态的,一旦创建,其内容和大小可以改变,但集合本身的结构不会改变。

            序列是一种按顺序排列的元素的集合,它可以是有限的或无限的。
            序列可以是静态的,也可以是动态的。
            序列通常提供了一系列操作,如过滤、映射、排序、聚合等,可以对序列进行转换和处理。
            序列是按需生成的,只有在需要时才会生成下一个元素,这使得序列可以处理无限大的数据集或延迟加载的数据。
            序列还可以进行迭代,按照顺序逐个访问序列中的元素。

            因此,集合和序列之间的主要区别在于:
            
            结构:
            集合通常是静态的,一旦创建,其内容和大小可以改变,但集合本身的结构不会改变;
            而序列是按需生成的,只有在需要时才会生成下一个元素,序列本身可以是静态的或动态的。

            访问方式:
            集合可以随机访问元素,可以通过索引或键来访问元素;
            而序列是按顺序排列的,只能按照顺序逐个访问元素。

            大小:
            集合可以包含任意数量的元素,可以是有限的或无限的;
            而序列可以是有限的或无限的,可以处理无限大的数据集或延迟加载的数据。

            总结:集合更适合于存储和操作多个元素,而序列更适合于按顺序处理元素。
            集合可以通过索引或键来访问元素,而序列只能按顺序逐个访问元素。
        
        
    4、为什么前面说一大堆的序列,有什么用?
        
        前面都是为Linq的基础铺路。
        
        LINQ 选择使用序列的概念,而不是集合的概念,是因为序列是一种更通用的数据结构。
        序列是按顺序排列的元素的集合,可以是有限的或无限的,可以是静态的或动态的。
        序列提供了一种统一的方式来处理和操作元素,不管数据源是什么类型。这使得 LINQ 可以适用于各种数据源,包括集合、数组、数据库、XML、JSON 等。

        另外,序列的概念更加符合 LINQ 的查询语义。LINQ 强调的是查询和转换操作,而不是集合的增删改查。
        序列提供了一系列操作符,如过滤、映射、排序、聚合等,可以方便地对数据进行转换和处理。
        序列还支持延迟加载和按需生成,这使得 LINQ 可以处理大数据集或延迟加载的数据。
        因此,使用序列的概念更符合 LINQ 的设计目标和使用场景。        虽然 LINQ 主要使用序列的概念,但它仍然可以与集合一起使用。事实上,大多数 LINQ 查询操作都可以应用于集合,因为集合实现了 IEnumerable 接口。因此,你可以将集合作为 LINQ 查询的数据源,并使用 LINQ 查询语法和操作符来操作集合中的元素。
        
        
    5、使用LINQ的序列可以使操作不必返回整个集合,从而提高效率和节省资源。

        private static void Main(string[] args)
        {
            foreach (string s in GetStrings().Take(100))//b
            {
                Console.WriteLine(s);
            }
            Console.ReadKey();
        }

        private static IEnumerable<string> GetStrings()
        {
            int i = 0;
            while (i++ < int.MaxValue)//a
                yield return i.ToString();
        }


        使用了yield return语句来创建一个迭代器方法GetStrings(),该方法在每次循环中返回一个字符串。通过使用迭代器方法,可以在需要的时候逐个返回字符串,而不必等待整个集合的计算完成(若返回整个int.MaxValue将是灾难)。

        在foreach循环中,每次迭代都会调用GetStrings()方法,并且只获取一个字符串进行处理。这样,当需要的字符串被获取后,就可以立即停止迭代,而不必等待整个集合的计算完成。这样可以大大减少内存消耗和计算时间,使程序更加高效。


三、枚举与算法的分开


    尽管有了通过枚举序列,可以有效提高效率。但真正哪个需要枚举返回,涉及算法,而且算法才是我们需要的,比如上面已经在序列中枚举了,但我们通过take(10)返回最前面的10个,这个take就是算法。
    
    1、扩展不用string,用T

        private static void Main(string[] args)
        {
            int i = 0;
            //IEnumerable<int> sequence = GetStrings(() => i++);//e
            IEnumerable<string> sequence = GetStrings(() => i++.ToString());//d
            //i=50;//c
            foreach (string s in sequence.Take(10))//b
            {
                Console.WriteLine(s);
            }
            Console.ReadKey();
        }

        private static IEnumerable<T> GetStrings<T>(Func<T> itemGererator)
        {
            int i = 0;//f
            while (i++ < int.MaxValue)//a
                yield return itemGererator();//返回对委托的调用
        }


        重载原来调用方法,参数使用委托方法,返回我们需要的(这就是算法了)。
        b处需要返回序列的前10个;
        d处定义算法,一个委托,把主函数中的i++,然后对字串返回。
            注意:d处的i++是基于主函数的i进行自加,与调用方法中f处的i无法。调用方法中f处的i只是一个计数,并不进行委托的算法当中,你可以设置调用方法f处i=100,,实际对结果没有影响。
            
        e处是用int参数,因为调用方法使用了T,是可以通用一些类型的。
        c处是修改主函数中的i,将对算法起作用,d处只是使用算法,结果也没有出来,所以在b处前使用i=50是可以生效的。
    
    
    2、如果把调用方法改为:

        private static IEnumerable<T> GetStrings<T>(Func<T> itemGererator)
        {
            //int i = 100;
            //while (i++ < int.MaxValue)//a
            //    yield return itemGererator();//返回对委托的调用
            int i = 0;
            List<T> sequence = new List<T>();
            while (i++ < int.MaxValue)
                sequence.Add(itemGererator());
            return sequence;
        }   

 
        运行时可以看到没有反应,因为这样的话,返回的不是序列,而是整个集合,这个集合元素有int.MaxValue个,将是一个可怕数字,程序需要很长的时间反应。
        所以,用序列而不用集合,能大大提高反应速度。
    
    
    3、随处可以更改算法
        上面再次修改:

        private static void Main(string[] args)
        {
            int i = 0;
            IEnumerable<string> sequence = GetStrings(() => i++.ToString());//a
            foreach (string s in sequence.Take(10))//c
            {
                i += 10;//b
                Console.WriteLine(s);//a
            }
            Console.ReadKey();
        }


        注意:c处take(10)的本质,就是限制整个迭代的次数,所以在处理完前10个元素后,迭代就会停止,因为序列象处理流一样处理,10次后就结束了。相当于总共有10个元素。
        
        而b处就是把原委托a处的i再次进行运算(闭包原理),所以委托实际上是i++后,然后i+=10,所以整个输出的结果就是0,11,22,33,44,....
        
        
        问:为什么第一个数字是0而不是11?
        答:因为第一次产生序列是0,1,2,3,第一次迭代取0,此时再进入循环内(i++后为1),由于i+=10,此时i=11。尽管i=11,但前面的序列已经产生即0,1,2,3....。所以第二次产生序列时从i=11开始,后面类推。
    
        警告:算法修改提供便利性的同时,也为随意修改制造了坑,象上面无意中修改了i,可能导致结果不对而不自知。
    
        
        问:不是说foreach不能修改元素,上面似乎修改了元素?
        答:虽然在foreach循环中对i进行了修改,但实际上这只是修改了迭代变量i的值,并不会影响序列中的元素。foreach循环是通过迭代器来遍历集合或序列的,而迭代器会在每次迭代时返回一个新的元素,因此对迭代变量的修改不会影响到原始序列。
    
    
    4、BCL是什么?
        
        C# BCL(Base Class Library)是C#语言的基础类库,它是.NET Framework(.NET框架)的一部分。
        BCL提供了一组常用的类和方法,用于开发和执行C#应用程序。BCL包含了大量的命名空间,涵盖了各种常见的开发任务,如文件操作、网络通信、数据访问、图形界面、多线程编程等。

        BCL是C#开发人员的重要工具之一,它提供了许多常用的功能和工具,使开发人员能够更加高效地编写代码。
        通过使用BCL,开发人员可以避免从头开始编写重复的代码,而是直接使用已经实现好的类和方法,从而节省时间和精力。

        除了BCL,C#还有其他的类库和框架,如ASP.NET、Windows Forms、WPF等,它们提供了更专业和特定领域的功能和工具,用于开发Web应用程序、桌面应用程序等。
        这些类库和框架都是建立在BCL的基础上,为开发人员提供更丰富和强大的功能。


四、Filter、Map、Reduce


    1、Filter常对应Where;
        Map对应Select;
        Reduce对应Sum、Avg等
        
    
    2、reduce 聚合
    
        "reduce"在这里是指对序列中的元素进行聚合操作,将多个元素缩减为一个单一的结果。
        
        LINQ中的Sum和Average方法被称为"reduce"操作,这是因为它们对一个序列中的元素进行聚合操作,将序列中的多个元素“缩减”为一个单一的结果。

        "Reduce"一词在计算机科学中是一个常用的术语,表示将一个序列或集合中的元素通过某种操作进行聚合,最终得到一个单一的结果。这个操作可以是求和、求平均、求最大值、求最小值等等。

        在LINQ中,Sum方法用于计算序列中所有元素的总和,Average方法用于计算序列中所有元素的平均值。这两个方法都是将多个元素“缩减”为一个单一的结果,因此被称为"reduce"操作。
    
    
    3、标量值
        
        标量值在LINQ中表示一个单一的结果,通常用于对序列中的元素进行聚合操作,将多个元素缩减为一个单一的值。
        
        标量值(Scalar Value)是指一个单一的值,而不是一个集合或序列。
        在LINQ中,标量值通常是指一个单一的结果,例如使用Sum方法计算序列中所有元素的总和,或使用Average方法计算序列中所有元素的平均值。        标量值在LINQ中用于表示某个操作的结果,该操作将一个序列中的多个元素聚合为一个单一的结果。这个结果可以是一个数字、一个字符串、一个布尔值等等。标量值可以用于各种场景,例如计算统计数据、获取某个属性的总和、判断序列是否满足某个条件等等。

        int[] numbers = { 1, 2, 3, 4, 5 };
        int sum = numbers.Sum(); // 计算序列中所有元素的总和
        Console.WriteLine("Sum: " + sum);

        double average = numbers.Average(); // 计算序列中所有元素的平均值
        Console.WriteLine("Average: " + average);


        Sum方法和Average方法都返回一个标量值,即计算结果。这些标量值可以直接用于后续的计算、判断或输出等操作。
    
    
    4、管道操作符
    
        C#中的管道操作符(|>)是一种语法结构,用于将一个表达式的结果作为另一个表达式的输入,以实现链式的操作。它可以用于各种场景,提高代码的可读性和简洁性。
        
        管道操作符可以用于各种场景,例如对集合进行连续的筛选、转换和排序操作,或者对某个对象执行多个方法调用。它可以提高代码的可读性,减少临时变量的使用,并且使操作的顺序更加清晰。

        int[] numbers = { 1, 2, 3, 4, 5 };

        var result = numbers
            .Where(num => num % 2 == 0) // 筛选偶数
            .OrderByDescending(num => num) // 按降序排序
            .Select(num => num * 2); // 将每个偶数乘以2

        foreach (var num in result)
        {
            Console.WriteLine(num);
        }


        上面使用管道操作符将多个LINQ操作连接在一起。
        首先使用Where方法筛选出偶数,然后使用OrderByDescending方法按降序排序,最后使用Select方法将每个偶数乘以2。通过使用管道操作符,可以将这些操作连在一起,以实现更简洁和可读的代码。
    
    
        C#中的管道操作符的命名灵感来自于函数式编程中的管道概念。它的引入使得代码可以更加流畅和可读,通过将多个操作链接在一起形成一个操作流水线,提高代码的可读性和简洁性。
        
        这种链式操作类似工厂的流水线,上一个工序的结果是下一个工序的开始。
    
    
    5、如果在程序中,既有BCL的 where,又有自已定义的where,程序将使用哪个的where?
        
        编译器将首先查找当前命名空间下的where 声明,如果找到则使用该声明,如果没有找到则使用 BCL 中的where。这样可以确保在有多个重名的where 声明时,能够按照优先顺序选择正确的声明。

        编译器将会根据以下规则来确定使用哪个where:

        1. 如果自定义的命名空间中有一个where 声明,它将覆盖 BCL 中的where。
        2. 如果自定义的命名空间中没有定义where 声明,但是 BCL 中有,那么将使用 BCL 中的where。        简言之:我的地盘我作主,选择最近的自定义。
    
    
    6、自定义一个where方法.
        用扩展方法,并带有一个委托。

        public static class MyLinqImplementation
        {
            public static IEnumerable<T> Where<T>(this IEnumerable<T> souce, Func<T, bool> predicate)
            {
                foreach (var item in souce)//d
                    if (predicate(item))
                        yield return item;
            }
        }

        internal class Program
        {
            private static void Main(string[] args)
            {
                var sequence = GenerateSequence();
                sequence = sequence.Where(s => s.Length < 2);
                foreach (var s in sequence)//c
                    Console.WriteLine(s);
                var seqNum = GenerateNum().Where(n => n < 50 && n % 3 == 0);
                foreach (var s in seqNum)//d
                    Console.WriteLine(s);
                Console.ReadKey();
            }

            private static IEnumerable<string> GenerateSequence()
            {
                int i = 0;
                while (i++ < 100)//b
                    yield return i.ToString();
            }

            private static IEnumerable<int> GenerateNum()
            {
                int i = 0;
                while (i++ < 100) //a
                    yield return i;
            }
        }


        d处所处的方法是扩展方法。
        上运行时,会很快出结果。
        
        如果把a,b处的100改为int.MaxValue,那么第一个序列结果出来后就会好像卡住,为什么?
        答:因为用了int.MaxValue后,尽管不是会返回整个序列,但它会逐个返回序列,看上去第一个序列已经枚举并输出来,但实际上它内部仍然在逐个枚举,一直要枚举到int.MaxValue。所以耗时很长,以致于后面第二序列好像并没有输出,因为它一个序列它仍然在不停地枚举中(由于是逐个枚举,所以不会导致程序卡死)。
        
        下面我们用这个来查看,把b处的方法改为:

        private static IEnumerable<string> GenerateSequence()
        {
            try
            {
                int i = 0;
                while (i++ < 100)//b
                    yield return i.ToString();
            }
            finally
            {
                Console.WriteLine("第一序列整个枚举结束");
            }
        }


        可以看到第一序列的结果,很快出来,且显示“第一序列整个枚举结束”,然后第二个序列也出来了。
        但这并没显示出问题所在,再修改程序b处为while (i++ < int.MaxValue),再运行程序。先是第一序列出来,然后。。。就没有然后了。因为程序第一个序列一直枚举且没有结束,所以后面的“第一序列整个枚举结束”在没有枚举完时,是不会输出的。从而证明了这个程序一直在枚举中。
        
        
    7、自定义Select
        同样,可以在MyLinqImpletetation静态类中定义静态方法,达到扩展方法效果。

        public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
        {
            foreach (TSource item in source)
                yield return selector(item);
        }     

   
        另一个带有索引的select重载是:

        public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, TResult> selector)
        {
            int idx = 0;
            foreach (TSource item in source)
                yield return selector(item, idx++);
        }   

     
        因此可以这样使用:

        var sequence = GenerateNum()
            .Where(x => x % 10 == 0)
            .Select((n, idx) =>
            new { idx, formattedResult = n.ToString() });
        foreach (var s in sequence)//c
            Console.WriteLine(s);    


        结果是(前面代码a处100改为50):
        { idx = 0, formattedResult = 10 }
        { idx = 1, formattedResult = 20 }
        { idx = 2, formattedResult = 30 }
        { idx = 3, formattedResult = 40 }
        { idx = 4, formattedResult = 50 }    
    


五、Peduce 聚合


    1、Any 是否有元素,有则返回为True,无则false
    

        public static bool Any<T>(this IEnumerable<T> sequence)
        {
            return sequence.GetEnumerator().MoveNext();
        }
        //重载有限制条件时,是否有元素
        public static bool Any<T>(this IEnumerable<T> sequence, Func<T, bool> predicate)
        {
            return sequence.Where(predicate).GetEnumerator().MoveNext();
        }


        主程序中验证

        private static void Main(string[] args)
        {
            Console.WriteLine(SequenceFromConsole().Any(s => s.Contains("hi")));
            Console.ReadKey();
            return;
        }

        private static IEnumerable<string> SequenceFromConsole()
        {
            string text = Console.ReadLine();
            while (text != "done")
            {
                yield return text;
                text = Console.ReadLine();
            }
        }


        
        
    
    2、Count 返回(满足条件)元素个数
    

        public static int Count<T>(this IEnumerable<T> sequence)
        {
            int count = 0;
            foreach (var item in sequence)
                count++;
            return count;
        }
        //重载有限制条件时,元素个数
        public static int Count<T>(this IEnumerable<T> sequence, Func<T, bool> predicate)
        {
            int count = 0;
            foreach (var item in sequence.Where(predicate))
                count++;
            return count;
        }
        验证:
        Console.WriteLine(SequenceFromConsole().Count(s => s.Contains("hi")));


        
        
        问:在用linq验证是否有元素时,用Any而不用Count为什么?
        答:当我们只关心序列中是否存在满足条件的元素时,使用 Any() 方法更合适,因为它只会遍历序列直到找到第一个满足条件的元素为止,而不会计算整个序列的长度。这样可以提高性能并减少不必要的计算。
    
    
    3、Aggregate 累积值
    

        public static int Aggregate(this IEnumerable<int> sequence, Func<int, int, int> fun)
        {
            int sum = 0;
            foreach (var item in sequence)
                sum += fun(sum, item);//a
            return sum;
        }
        主程序:
        Console.WriteLine(SequenceFromConsole()
            .Select(s => int.Parse(s))
            .Aggregate((partialSum, n) => partialSum + n));//b
        Console.ReadKey();


        输入,1,2,3,4,done,结果是26.
        
        问:结果为什么是26,而不是10?
        答:a处调用的是b处的lambda表达式,因此a处实际为sum = sum + partialSum + n .
            当n=1时:sum=0+0+1,结果是sum=1
            当n=2时:sum=1+1+2,结果是sum=4
            当n=3时:sum=4+4+3,结果是sum=11
            当n=4时:sum=11+11+4,结果是sum=26
            上面partial实际就是每次的sum的值,两者是一样的。
            
        也可以b处的lambda处下断点,查看每看变化的值。
        
        
        问:表达式中的lambda表达式如何设置断点?
        答:鼠标定位到lambda中,按F9设置断点(该断点不是整行,而是专门针对lambda)。
            或者右击lambda表达式,选择断点->插入断点,这样断点就设置在lambda上而非整个语句上。
        
        
        问:为什么Aggregate与认识的累积值不一样呢?
        答:上面扩展方法中,a处是关键,将a处改为sum = fun(sum, item);就是正常的累积值,结果将变为10。
        
        
        将上面的改为正常累积值后,再重载一个种子基数。

        public static int Aggregate(this IEnumerable<int> sequence, int seed, Func<int, int, int> fun)
        {
            int sum = seed;
            foreach (var item in sequence)
                sum = fun(sum, item);
            return sum;
        }        
        再运行:
        Console.WriteLine(SequenceFromConsole()
            .Select(s => int.Parse(s))
            .Aggregate(10,(partialSum, n) => partialSum + n));    

    
        上面输入1,2,3,4,done后,结果为:20
    
        改上面扩展方法为泛型:

        public static T Aggregate<T>(this IEnumerable<T> sequence, Func<T, T, T> fun)
        {
            T sum = default;
            foreach (var item in sequence)
                sum = fun(sum, item);
            return sum;
        }

        public static T Aggregate<T>(this IEnumerable<T> sequence, T seed, Func<T, T, T> fun)
        {
            T sum = seed;
            foreach (var item in sequence)
                sum = fun(sum, item);
            return sum;
        }        
        验证:
        List<string> list = new List<string>() { "a", "b" };
        Console.WriteLine(list.Aggregate("k", (partialSum, n) => partialSum + n));


        
        
        问:default是什么意思?
        答:类型的缺省值,比如,数值类缺省为0,引用缺省为null,bool缺省为false。
            这里不能用null,0等,因为我们无法确定它的类型。
            在已经确定类型的情况下,比如int时,  int n=defalut;与int n=defalut(int);与var n=defalut(int);三者是等效的。
        


六、OrderBy 排序


    1、OrderBy与ThenBy是两个主要的排序。
        
        必须先有OrderBy才有ThenBy。
        
        ThenBy是在OrderBy的基础上进行排序,即在相同OrderBy的元素中再进行ThenBy排序。
        
        
    2、外面看似多步,内部实则一步。
        
        在外面可以看到sequence.OrderBy(A).Thenby(B).ThenBy(C)...ThenBy(H)
        但实际内部排序不是先A,再在A基础上进行B排序,再在前面两个基础上排序C。
        而是一次排序做成,即将ABC...H等多个排序在一个工序中进行排序。
        因为排序是一个很耗费资源的动作,如果逐个排序将浪费大量资源,为了节约资源,内部会一次性将多个排序在一个动作中完成,而并非安排对应的多个动作去做。
    
    


七、从查询表达式映射到方法


    
    1、skip 跳过
        
        Skip 方法用于跳过序列中指定数量的元素,然后返回剩余的元素。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        IEnumerable<int> skippedNumbers = numbers.Skip(5);
        foreach (int number in skippedNumbers)
            Console.WriteLine(number);


        上面跳过前面的 5 个数字后,得到了剩余的数字 6 到 10。

        注意,Skip 方法返回的是一个延迟执行的 IEnumerable<T> 类型,因此可以在需要时进行迭代。它不会修改原始的集合,而是返回一个新的序列。        
    
    
    2、take 先拾取
    
        Take 方法用于从序列的开头返回指定数量的元素。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        IEnumerable<int> takenNumbers = numbers.Take(5);
        foreach (int number in takenNumbers)
            Console.WriteLine(number);


        上面得到了列表的前面 5 个数字,即1,2,3,4,5.

        注意,Take 方法返回的是一个延迟执行的 IEnumerable<T> 类型,因此可以在需要时进行迭代。它不会修改原始的集合,而是返回一个新的序列。    
    
    3、请说出下面seq与seq1求序列的区别?
    

        List<int> list = new List<int>() { 1, 22, 3, 4, 5, 6, 7, 8, 9 };
        var seq = list
            .Where(n => n > 5)
            .Select(n => n);
        var seq1 = from n in list
                   where n > 5
                   select n;   

 
        答:上面方法是等效的,但更推荐第一种。
        
            seq使用了方法链式调用的方式,是一种常见的使用LINQ的方式。
        
            seq1使用了查询表达式语法,更类似于传统的SQL查询语法。
            
            seq可以更好地扩展和组合多个LINQ操作。这种方式使得代码更具可读性和可维护性,并且可以轻松地添加、删除或调整操作步骤。
    
    
    4、Join 两集合连接返回新集合
    

        int[] numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        string[] labels = new string[] { "0", "1", "2", "3", "4", "5" };
        var query = from num in numbers
                    join label in labels on num.ToString() equals label
                    select new { num, label };
        var query2 = numbers.Join(labels, num => num.ToString(), label => label,
                                (num, label) => new { num, label});

        foreach (var item in query)
            Console.WriteLine($"\t{item}");    


        Join方法的第一个参数是要连接的第二个集合(labels),第二个参数是连接的键(num => num.ToString()表示使用numbers中元素作为连接键,label => label表示使用labels中元素作为连接键,第三个参数是选择结果的委托(num, label) => new { num, labels })表示选择由num与lable组成的结果。
    
    
    5、GroupJoin 分组联接
    

        var departments = new List<Department>
            {
                new Department { ID = 1, Name = "HR" },
                new Department { ID = 2, Name = "Finance" },
                new Department { ID = 3, Name = "IT" }
            };

        var employees = new List<Employee>
            {
                new Employee { ID = 1, Name = "Alice", DepartmentID = 1 },
                new Employee { ID = 2, Name = "Bob", DepartmentID = 2 },
                new Employee { ID = 3, Name = "Charlie", DepartmentID = 1 },
                new Employee { ID = 4, Name = "David", DepartmentID = 3 }
            };

        var result = departments.GroupJoin(employees,
                    department => department.ID, employee => employee.DepartmentID, 
                    (department, employeeGroup) => new { department.Name, Employees = employeeGroup });

        foreach (var item in result)
        {
            Console.WriteLine($"Department: {item.Name}");
            foreach (var employee in item.Employees)
            {
                Console.WriteLine($"- {employee.Name}");
            }
            Console.WriteLine();
        }


        上面分组联接,可改为查询式: 

        var result = from department in departments
                     join employee in employees on department.ID equals employee.DepartmentID
                     into employeeGroup
                     select new { department.Name, Employees = employeeGroup };   

 
    
        技巧:上面的两个类没有定义,有提示红线,怎么办?
            不需要人工再输入,鼠标指向红线,提示“显示可能修复的程序”,然后直接点生成class XXX 或生成对应的属性,不必再用键盘一个一个地按,瞬间自动完成类及属性创建。
    
        
        问:为什么join on后面的条件一般用equal而不用其它?
        答:用其它是会报错的。
            通常在 join 操作中使用 equals 关键字来指定联接条件,而不能用如 ==、> 或 <。这是因为 join 操作的目的是在两个集合之间建立关联,而不是进行一般的比较操作。如果你需要进行其他类型的比较,可以使用 where 子句来过滤数据。
            
            如果需要进行比较操作,应该在 where 子句中进行过滤。
        
        
        问:join on 与join on into有什么区别?
        答:join on将两个或多个数据集按照指定的列进行连接操作。连接操作是根据连接条件将两个数据集中具有相同值的行组合在一起。
            
            用join on into在连接操作的基础上,将连接的结果存储到新的表或数据集中。
            
            两者返回的类型都是可以自定义的,注意的是,返回集合的元素中可能有组的存在。

            
            List<Student> students = new List<Student>
            {
                new Student { ID = 1, Name = "Alice", Age = 20 },
                new Student { ID = 2, Name = "Bob", Age = 21 },
                new Student { ID = 3, Name = "Charlie", Age = 19 }
            };
            List<Grade> grades = new List<Grade>
            {
                new Grade { ID = 1, Subject = "Math", Score = 90 },
                new Grade { ID = 2, Subject = "English", Score = 85 },
                new Grade { ID = 1, Subject = "Science", Score = 95 }
            };

            // 使用join子句和into关键字进行关联查询
            var result = from student in students
                         join grade in grades on student.ID equals grade.ID into studentGrades//b
                         select new
                         {
                             ID = student.ID,
                             Name = student.Name,
                             //Score = grade.Score,//a
                             Grades = studentGrades.ToList()
                         };

            // 输出结果
            foreach (var item in result)
            {
                Console.WriteLine($"Student ID: {item.ID}, Name: {item.Name}");
                Console.WriteLine("Grades:");
                foreach (var grade in item.Grades)
                    Console.WriteLine($"- Subject: {grade.Subject}, Score: {grade.Score}");
                Console.WriteLine();
            }    


        上面是一对多,所以用b处的studentGrades来代表任意一组,以便后面指代使用。
        
        
        问:为什么上面a处添加后会出错?
        答:因为grade在此处不可见。
        
            为什么不可见?
            因为grade是由join产生,因此grade只能在join grade in grades on student.ID equals grade.ID中可见。
            
        
        问:linq中的可见原则是什么?
        答:在LINQ查询中,每个子句都有其自己的范围和可见性规则。

            (1)from子句中声明的范围变量在整个查询中都是可见的。
            这意味着您可以在查询的任何子句中访问from子句中声明的范围变量。

            (2)join子句中声明的范围变量只在join子句及其后续子句中可见。
            这意味着您可以在join子句及其后续子句(例如where、select等)中访问join子句中声明的范围变量。

            (3)let子句中声明的范围变量在let子句之后的所有子句中都是可见的。
            这意味着您可以在let子句之后的所有子句(例如where、select等)中访问let子句中声明的范围变量。

            (4)在select子句中,您可以访问之前的所有范围变量、参数和属性。
            这意味着您可以在select子句中访问之前的from、join和let子句中声明的范围变量,以及查询的输入参数和属性。

            注意,如果在查询中引入了同名的范围变量,则后面的范围变量将隐藏前面的范围变量。例如,如果在from子句中声明了一个名为student的范围变量,并在后面的子句中再次声明了一个名为student的范围变量,则后面的范围变量将隐藏前面的范围变量。

        注意:join on into是基于左表的,如果右表没有,也会创建空值。上面运行结果:
                Student ID: 1, Name: Alice
                Grades:
                - Subject: Math, Score: 90
                - Subject: Science, Score: 95

                Student ID: 2, Name: Bob
                Grades:
                - Subject: English, Score: 85

                Student ID: 3, Name: Charlie
                Grades:
                
            同样写成链式:

        var result2 = from student in students
                      join grade in grades on student.ID equals grade.ID
                      select new { student.ID, grade.Subject };   

             
            输出的结果仍然是上面,所以都需要进行过滤:

        var result1 = students.GroupJoin(grades, student => student.ID, grade => grade.ID,
            (student, studentGrades) => new { student, studentGrades })
            .Where(x => x.studentGrades.Any())
            .Select(x => new { ID = x.student.ID, Name = x.student.Name, Grades = x.studentGrades.ToList() });


        结果显示为:
                Student ID: 1, Name: Alice
                Grades:
                - Subject: Math, Score: 90
                - Subject: Science, Score: 95

                Student ID: 2, Name: Bob
                Grades:
                - Subject: English, Score: 85                
        可以看到,没有匹配上的第三组空值就过滤了。
        
        
        问:上面select中的x能引用前面where中的x?
        答:不能。两者各是一个变量只在各自内部可见。
        
        
        问:上面ID = x.student.ID可以省略写成x.student.ID吗?
        答:可以.
            
            在LINQ查询中,如果您省略了select子句中的属性名称,它将默认使用源对象的属性名称。因此,在上面的查询中,如果您省略了select子句中的属性名称,它将直接使用原始属性名称。(在SQL称为字段)

        
         结果:join on与join on into,以及groupjoin都表达相同的意思,有细微的差异。主要以"join"来记忆它们是两个或多个集合的联接。
    
    
    6、group by分组
        
        group by子句用于按照指定的键对集合进行分组,并返回一个IEnumerable<IGrouping<TKey, TElement>>对象,其中TKey是键的类型,TElement是集合中的元素类型。

        var students = new List<Student>
        {
            new Student { Name = "Alice", Grade = 1 },
            new Student { Name = "Bob", Grade = 2 },
            new Student { Name = "Charlie", Grade = 1 },
            new Student { Name = "Dave", Grade = 2 },
            new Student { Name = "Eve", Grade = 3 }
        };
        var result = from student in students
                     group student by student.Grade;
        foreach (var group in result)
        {
            Console.WriteLine($"Grade: {group.Key}");
            foreach (var student in group)
                Console.WriteLine($"Student: {student.Name}");
        }


        结果:
            Grade: 1
            Student: Alice
            Student: Charlie
            Grade: 2
            Student: Bob
            Student: Dave
            Grade: 3
            Student: Eve
        上面不必用into,group by into用于将分组结果赋值给一个新的变量,并对该变量进行进一步处理。

        var students = new List<Student>
        {
            new Student { Name = "Alice", Grade = 1 },
            new Student { Name = "Bob", Grade = 2 },
            new Student { Name = "Charlie", Grade = 1 },
            new Student { Name = "Dave", Grade = 2 },
            new Student { Name = "Eve", Grade = 3 }
        };
        var result = from student in students
                     group student by student.Grade into gradeGroup
                     select new { Grade = gradeGroup.Key, Count = gradeGroup.Count() };

        foreach (var group in result)
            Console.WriteLine($"Grade: {group.Grade}, Count: {group.Count}");


        结果:
            Grade: 1, Count: 2
            Grade: 2, Count: 2
            Grade: 3, Count: 1
        至于最后的结果元素中是否还有多个值,取决自己定义select是单一值还是多个值。
    
        
        上面查询式可以改写为链式:
        var result1=students.GroupBy(student=>student.Grade)
            .Select(x=>new {Grade=x.Key,Count=x.Count()});        
    
    
        问:上面的分组排序后,再每组按Name排序?
        答:(1)链式:

        var result1 = students.GroupBy(student => student.Grade)
            .OrderBy(x => x.Key)
            .SelectMany(x => x.OrderBy(student => student.Name));


            先按分组序号排序,然后再把它扁平化后用每组里的Name排序。
            
            (2)链式

        var result = from student in students
                     group student by student.Grade into groups
                     orderby groups.Key
                     from s in groups.OrderBy(student => student.Name)
                     select s;


            在这段代码中,from s in groups.OrderBy(student => student.Name)只是对每一组进行再次排序。
            
            注意:总集合中的每个元素都按照组的形式存在。当对每个组内的元素进行排序时,不会干扰原来总集合中各组的相对位置。因为每个组内的元素是在 groups 变量中进行排序的,而 groups 变量只是一个临时的分组结果,不会对原来的总集合产生影响。
            
            通过对分组进行排序,然后再对每个分组内的元素进行排序,可以实现在保持总集合中组的相对位置的同时,对每个组内的元素进行排序。这种方式可以更加灵活地控制分组和排序的逻辑。
    
    
    7、SelectMany 扁平化(平铺)
        

        含义:select表示是映射,many表明多个,即将(一个集合中)多个嵌套进行映射操作,返回一个扁平化的结果集合。
        
        扁平化:把一个集合中含的有多个子集合(嵌套),进行平铺、压扁形成单一元素(再进行加工)的操作。
        
        例如:原集合{{1,2},{3,4},{6,7,8}}包含了三个子集合,分别是{1,2}、{3,4}和{6,7,8}。通过应用 SelectMany 方法,我们可以将这些子集合合并为一个扁平的集合{1,2,3,4,6,7,8}。

        var collection = new List<List<int>>
                        {
                            new List<int> {1, 2},
                            new List<int> {3, 4},
                            new List<int> {6, 7, 8}
                        };
        var flattenedCollection = collection.SelectMany(list => list);


        注意:SelectMany会将最后一个x返回的子集合平铺成一个单独的集合。
    
        (1)带一个参数
        以参数的形式表示每个元素,并返回一个集合。

        var list = new List<int> { 1, 2, 3, 4 };
        var res = list.SelectMany(x => new List<int> { x, x * 2 });//看似返回集合,同样压扁    

    
        上面是自己变化情况,还可以引用其它,而且不是linq内:

        var ones = new List<int> { 1, 2, 3 };
        var twos = new List<int> { 4, 5 };
        var res = ones.SelectMany(one => twos.Select(two => one + two));     

   
        上面由one对应一个子集合,数字变化一下。最后返回所有子集合平铺情况:{5,6,6,7,7,8}
    
        (2)带两个参数
        AAs.SelectMany(AA => BBs, (AA, BB) => ... })
        第一个参数用于选择源序列中的元素的集合(AA,它来自于AAs)
        第二个参数则表示在每个选定的元素上执行的操作来生成结果序列(BB,它来自于BBs)

        第一个参数可以是一个集合,也可以是一个返回集合的函数或表达式。对于源序列中的每个元素,SelectMany 将应用第一个参数指定的选择器,并将其结果扁平化为一个单一的序列。        第二个参数是一个转换函数或表达式,用于将每个选定的元素转换为结果序列的元素。这个函数接受两个参数:源序列中的元素和源序列中的索引(可选)。

        users.SelectMany(user => user.Roles, (user, role) => new { uname = user.Name, rname = role.Name });

        List<dynamic> list = new List<dynamic>();
        foreach (var user in users)
            foreach (var role in user.Roles)
                list.Add(new { uname = user.Name, rname = role.Name });     

   
        上面第一句,可以用它下面的语句解释,先遍历users,再遍历Roles,第二个参数就把这些单个元素加入到集合中。

        var employees = new List<Employee>
        {
            new Employee { Id = 1, Name = "Alice", DepartmentIds = new List<int> { 1, 2 } },
            new Employee { Id = 2, Name = "Bob", DepartmentIds = new List<int> { 1, 3 } },
            new Employee { Id = 3, Name = "Charlie", DepartmentIds = new List<int> { 2, 3, 4 } }
        };

        var departments = new List<Department>
        {
            new Department { Id = 1, Name = "HR" },
            new Department { Id = 2, Name = "Finance" },
            new Department { Id = 3, Name = "Marketing" },
            new Department { Id = 4, Name = "IT" }
        };

        var empDep = employees
            .SelectMany(emp => emp.DepartmentIds, (emp, deptId) => new { emp, deptId })
            .Join(departments, x => x.deptId, dept => dept.Id, (x, dept) => new { x.emp, dept.Name });
        foreach (var item in empDep)
            Console.WriteLine(item.emp.Name + "," + item.Name);


    
        另一个就是用于字符串的拆分:

        var input = "Hello World";
        var splitLetters = input.SelectMany(c => c.ToString(), (c, letter) => letter);   

     
        上面感觉有些多余了,因为后面创造新的元素中,直接使用了letter,所以整个集合返回的是单一的letter。

        var ones = new List<int> { 1, 2, 3 };
        var twos = new List<int> { 4, 5, 6 };
        var threes = new List<int> { 7, 8, 9 };

        var combined = ones.SelectMany(one => twos, (one, two) => one + two)
            .SelectMany(sum => threes, (sum, three) => sum + three);
        foreach (var item in combined)
            Console.WriteLine(item);


            
        细心发现,里面使用了叉积即笛卡尔积。如:

        var combined = ones.SelectMany(one => twos, (one, two) => one + two);


        结果就是{5,6,7,6,7,8,7,8,9}共3X3=9个元素。也等效于:

        var combined1 = from one in ones
                        from two in twos
                        select one + two;


    
        变换一下:

        var ones = new List<int> { 1, 3, 5, 7 };
        var twos = new List<int> { 2, 4, 6, 8 };
        var combined1 = from one in ones
                        from two in twos
                        where one > two
                        select one + two;    

    
        先产生叉积共4X4=16个,然后where过滤,3+2,5+2,5+4,7+2,7+4,7+6,结果为:5,7,9,9,11,13.等效的链式表达:

        var res = ones.SelectMany(one => twos, (one, two) => new { one, two })
            .Where(pair => pair.one > pair.two)
            .Select(pair => pair.one + pair.two);


八、Linq设计与性能


    
    1、什么是IQueryable?
        
        IQueryable是一个泛型接口,用于支持查询和操作可查询数据源的对象。它是LINQ(Language Integrated Query,语言集成查询)技术的核心之一。

        IQueryable接口继承自IEnumerable接口,它拥有IEnumerable接口提供的遍历和查询功能,并添加了更多的查询能力。
        它与IEnumerable的主要区别在于,IQueryable支持基于表达式树的查询,这意味着查询可以被转换为表达式树,然后在运行时被解析和执行。这样的机制使得IQueryable能够支持强大的查询优化和延迟加载。

        通过使用IQueryable接口,我们可以针对各种数据源(如数据库、集合、XML等)进行复杂的查询操作。使用LINQ查询表达式或方法语法,我们可以轻松地编写查询条件、投影、排序和分组等操作。IQueryable的一些常用特性和方法:

        (1)查询运算符:
        IQueryable提供了一组丰富的查询运算符,如Where、Select、OrderBy、GroupBy等,用于筛选、变换、排序和分组数据。
        (2)延迟加载:
        IQueryable支持延迟加载的特性,即查询不会立即执行,只有在需要结果时才会触发执行。这允许我们构建复杂的查询链,并在最后一刻执行查询,以减少不必要的计算。
        (3)表达式树:
        IQueryable接口通过将查询转换为表达式树的形式,使得查询操作可以在运行时进行解析和执行,从而实现了更高级别的查询优化和灵活性。
        (4)扩展性:
        通过扩展IQueryable接口,我们可以自定义数据源的查询能力,以适应特定的业务需求。

        问:什么是表达式树?
        答:表达树之所以被称为表达树,是因为它的结构类似于树,其中根节点表示整个表达式,子节点表示表达式的组成部分。通过遍历和操作表达树,我们可以对表达式进行求值、转换、优化等操作。
            例如:有一个简单的数学表达式 "2 + 3 * 4"。可以使用表达树来表示这个表达式,其中根节点是加法操作符,左子树是常量2,右子树是乘法操作符;左子树是常量3,右子树是常量4。这样的表达树可以帮助我们理解和操作表达式的结构。
            通俗理解认为表达式就是表达树,只是表达树更准确。
            
    
    2、Count与Any
    
        检查序列是否有元素,通常不用Count:

        var seq = Enumerable.Range(0, 900000000);
        bool exist = seq.Count() > 0;//a
        Console.WriteLine(exist);


        上面要延迟几秒才显示true,如果a处改为:

        bool exist = seq.Any();


        几乎瞬间出来。因为Count需要统计900000000后才出结果,而Any只要检测到第一个元素时,马上返回结果。
        
        优化效率的开始:
        
        问:如果检测元素个数是否大于3呢?
        答:显然bool exist = seq.Count() > 3;是一个笨拙的方法。它和开始的方法一样慢。
            
            使用bool exist = seq.Skip(3).Any();最佳选择,skip表明先跳过3个元素,从剩下的任选一个,就马上返回结果。(当序列的元素为空,或不足3个时将返回空元素,所以后面的Any只能为false).
            
            它的效果实际与Any的效果一样,很快。
            
            这里不能用take(3),它是取前3个元素(不足3个时,取全部,没有元素是返回空)
            
            
        问:如果检测有大于20的元素存在?
        答:bool exist = seq.Where(n => n > 20).Any();这将列举整个序列。
            seq.SkipWhile(n => n <= 20).Any()能达到要求吗?不能,因为skipwhile遇到21后会返回后面所有元素,又是一个巨大的数字。
            bool exist = seq.Any(n => n > 20);查找比较快,它是从序列第一个元素开始查找,找到立即结束并返回true,否则一直向下查找。
    
        
        SkipWhile方法
        SkipWhile方法用于跳过序列中满足指定条件的元素,直到遇到不满足条件的元素为止。不是象where那样过滤,是不满足即终止。

        List<int> ones = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, };
        List<int> twos = new List<int> { 1, 2, 333, 4, 5, 6, 7, 8, };
        ones.SkipWhile(x => x <= 5).ToList().ForEach(x => Console.WriteLine(x));//6,7.8
        twos.SkipWhile(x => x <= 5).ToList().ForEach(x => Console.WriteLine(x));//333,4,5,6,7,8   

 
    
        TakeWhile方法
        TakeWhile用于选取序列中满足指定条件的元素,直到遇到不满足条件的元素为止。

        seq.TakeWhile(n => n % 5 == 0).Skip(2).Any()


        第一个元素0满足n%5==0,选取;第二个元素1不满足,不选取,而且终止执行。然后skip(2),由于只有一个元素,故返回为null,后面的any只能为false
        
        总结:skipwhile与takewhile都是非常挑剔的小孩,只要不满足就大哭终止再比较。区别是前者终止时选择后面剩余的所有元素,后者终止是选择前面通过的所有元素。
            或许用“圈地”来形容takewhile更为恰当。用“突围”来形容skipwhile更为适合。
    
    
        Single方法
        Single 是一个用于查询操作的方法,用于返回序列中满足特定条件的唯一元素。如果序列中没有满足条件的元素,或者有多个满足条件的元素,Single 方法将引发异常。因此,在使用 Single 方法之前,我们需要确保序列中的元素满足这些条件。
        因此,single必须遍历整个序列才能确信它是一个唯一值,为空,为多值,都会引发异常。

        int[] seq = { 1, 2, 3 };
        int s1 = seq.Single(n => n == 2);//2
        int s2 = seq.Single(n => n > 2);//3
        try
        {
            int s3 = seq.Single(n => n > 1);
        }
        catch (Exception)
        {
            Console.WriteLine("不是唯一值");
        }       

 
    
        First方法
        取得第一个元素。First()则取序列第一个元素。序列为空时则异常。
        First(Func<TSource, bool> predicate)则返回第一个满足条件的元素,找不到则异常。

        int[] seq = { 1, 2, 3 };
        int s1 = seq.First(n => n == 2);//2
        int s2 = seq.First(n => n > 2);//3
        try
        {
            int s3 = seq.First(n => n > 4);
        }
        catch (Exception)
        {
            Console.WriteLine("异常,没找到");
        }  

     
        为了克服first为空引发异常,使用firstOrdefault来返回为空时的默认值:default(TSource)。

        int s3 = seq.FirstOrDefault(n => n > 4);//0  default(int)为0


        
        Last方法与LastOrDefault方法与前面的相似。
        
        
        警告:
        需要小心使用Last或lastOrDefault,因为它会枚举整个序列,即使你使用reverse反转想用first,但reverse也要枚举整个序列,特别是一个非常大的序列时,也会造成效率低下。
        
        遍历是一种通用的循环机制,而枚举是一种特殊的遍历方式,通过枚举器来实现对集合元素的迭代。枚举器提供了更简洁、安全和易用的方式来遍历集合,尤其是在处理复杂集合类型时。
        
        
    3、linq一般都是延迟加载
        
        有时为了提前缓存,可以使用ToList()或ToArray()。或者使用ToLookup()或ToDictionary
        
        
    4、ToLookup()分组查找并缓存
        ToLookup是LINQ中的一个方法,用于将一个序列的元素按照指定的键进行分组,并将分组结果存储在一个ILookup<TKey, TElement>接口的实例中。

        ILookup<TKey, TElement>接口表示一个键到多个值的映射关系,类似于字典(Dictionary<TKey, TValue>),但一个键可以对应多个值。它提供了一种方便的方式来对数据进行分组,并且可以通过键来快速访问对应的值。

        ToLookup方法接受一个键选择器函数,该函数用于从序列的每个元素中提取一个键。然后,ToLookup方法将序列的元素按照键进行分组,并返回一个ILookup<TKey, TElement>对象,其中键的类型为TKey,值的类型为TElement。        

        string[] fruits = { "apple", "banana", "cherry", "date", "elderberry", "fig", "grape" };
        ILookup<char, string> fruitGroups = fruits.ToLookup(fruit => fruit[0]);
        foreach (var group in fruitGroups)
        {
            Console.Write($"Fruits starting with '{group.Key}':  ");
            foreach (var fruit in group)
                Console.WriteLine(fruit);
        }


        结果:
        Fruits starting with 'a':  apple
        Fruits starting with 'b':  banana
        Fruits starting with 'c':  cherry
        Fruits starting with 'd':  date
        Fruits starting with 'e':  elderberry
        Fruits starting with 'f':  fig
        Fruits starting with 'g':  grape
        使用ToLookup方法将这些水果按照首字母进行分组。fruitGroups是一个ILookup<char, string>对象,它将首字母作为键,对应的水果作为值。
        
        根据某个属性将对象列表分组:

        List<Student> students = new List<Student>
        {
            new Student { Name = "Alice", Grade = 10 },
            new Student { Name = "Bob", Grade = 9 },
            new Student { Name = "Charlie", Grade = 10 },
            new Student { Name = "David", Grade = 9 },
            new Student { Name = "Eve", Grade = 11 },
            new Student { Name = "Frank", Grade = 11 }
         };
        ILookup<int, Student> studentsByGrade = students.ToLookup(student => student.Grade);

        foreach (var group in studentsByGrade)
        {
            Console.WriteLine($"Students in Grade {group.Key}:");
            foreach (var student in group)
                Console.WriteLine($"\t Name: {student.Name}");
        }


        通过使用ToLookup方法,我们可以方便地将对象列表按照某个属性进行分组,并通过键快速访问对应的对象。这在很多场景下都非常有用,例如根据某个属性进行统计、创建索引等。
    
    
        问:ToLookup与Groupby有什么区别?
        答:ToLookup方法和GroupBy方法都可以用于将序列按照某个键进行分组,区别:

        (1)返回类型:
        ToLookup方法返回一个ILookup<TKey, TElement>对象,它实际上是一个字典,其中键是分组键,值是对应的元素序列。
        GroupBy方法返回一个IEnumerable<IGrouping<TKey, TSource>>对象,它是一个可枚举的序列,每个IGrouping<TKey, TSource>对象表示一个分组,其中键是分组键,值是对应的元素序列。

        (2)延迟执行:
        GroupBy方法是延迟执行的,只有在枚举结果时才会进行实际的分组操作。
        而ToLookup方法是立即执行的,它会遍历源序列并构建字典。

        (3) 可变性:
        ToLookup方法返回的ILookup对象是不可变的,它只能用于查找操作。
        而GroupBy方法返回的IGrouping对象是可变的,可以通过ToList等方法将其转换为列表,进行修改和排序等操作。

        (4) 查找操作:
        ILookup对象提供了快速的键值查找和遍历功能,可以通过键快速访问对应的元素序列。
        IGrouping对象提供了遍历和获取分组键的功能,但不支持快速的键值查找。

        总结,ToLookup方法适用于需要频繁进行查找操作的场景,它提供了快速的键值查找和遍历功能。
            GroupBy方法适用于需要对分组进行进一步操作的场景,它返回的IGrouping对象可以进行修改、排序等操作。
    
    
        问:感觉ToLookup很慢,是算法还是缓存的原因?
        答:ToLookup方法的性能通常是很好的,特别是在需要频繁进行查找操作的场景下。
            但是,如果源序列非常大,或者分组键的数量非常大,那么创建ILookup对象可能会花费一些时间和内存。在这种情况下,你可以考虑使用其他数据结构或算法来优化性能。
    
    
    
    5、ToDictionary
    
        ToDictionary是C#中的一个LINQ扩展方法,用于将一个集合转换为一个字典。它接受一个委托作为参数,该委托定义了如何从集合的元素中提取键和值。方法签名如下:

        public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>(
            this IEnumerable<TSource> source,
            Func<TSource, TKey> keySelector,
            Func<TSource, TElement> elementSelector
        )

        参数说明:
        source:要转换为字典的源集合。
        keySelector:一个委托,用于从源集合的每个元素中提取键(Tkey)。
        elementSelector:一个委托,用于从源集合的每个元素中提取值(TElement)。        使用ToDictionary方法可以将一个集合转换为一个字典,其中键和值的类型由keySelector和elementSelector委托指定。例如,可以将一个包含学生对象的集合转换为一个字典,以学生的ID作为键,学生对象本身作为值。

        class Student
        {
            public int ID { get; set; }
            public string Name { get; set; }
        }

        static void Main(string[] args)
        {
            List<Student> students = new List<Student>
            {
                new Student { ID = 1, Name = "Alice" },
                new Student { ID = 2, Name = "Bob" },
                new Student { ID = 3, Name = "Charlie" }
            };
            Dictionary<int,string> dics=  students.ToDictionary(d => d.ID, d => d.Name);

            foreach (var item in dics)
                Console.WriteLine(item.Key+","+item.Value);
        }


        输出:
            ID: 1, Name: Alice
            ID: 2, Name: Bob
            ID: 3, Name: Charlie
        
        警告:
        ToDictionary方法在创建字典时,如果源序列中存在重复的键,则会抛出一个ArgumentException异常。这是因为字典要求键是唯一的,不允许有重复的键。
        
        如果你确定源序列中可能存在重复的键,并且你希望忽略重复的键,可以使用ToLookup方法来创建一个ILookup对象,它允许有重复的键。ILookup对象实际上是一个字典,其中键是分组键,值是对应的元素序列。你可以通过ILookup对象进行键值查找和遍历操作。
        
        
        问:上面一个key有多个对应的值,若只选择最后一个值作为键值对呢?
        答:可以通过其它来表示:

        List<Student> students = new List<Student>
        {
            new Student { Name = "Alice", Grade = 10 },
            new Student { Name = "Bob", Grade = 9 },
            new Student { Name = "Charlie", Grade = 10 },
            new Student { Name = "David", Grade = 9 },
            new Student { Name = "Eve", Grade = 11 },
            new Student { Name = "Frank", Grade = 11 }
         };//h

        Dictionary<int, string> dics1 = students.GroupBy(s => s.Grade)
             .ToDictionary(g => g.Key, g => g.First().Name);//a
        Dictionary<int,string> dics2 = students.GroupBy(s => s.Grade)
            .ToDictionary(s => s.Key, s => s.Last().Name);//b
        Dictionary<int, string> dics3 = students.GroupBy(s => s.Grade)
            .ToDictionary(s => s.Key, s =>
            {
                if (s.Count() > 2) return s.ElementAt(1).Name;//d
                else return s.First().Name;
            });//c


        上面a处是分组后取第一个元素,b处是取最后一个元素。若取第二个元素,因涉及可能没有第二个元素的,需要判断用C处。注意:d处用(1)而不是[1],因为这里不是变量,而是一个方法,传递参数用圆括号。
        
        练习:
        将上面h处的数据,按Grade分组后再按Grade排序,然后组内按名字排序,最后,输出单一的Grade,Name对。

        var dics = students
          .GroupBy(s => s.Grade)
          .OrderBy(g => g.Key)
          .Select(x => new { Grade = x.Key, Names = x.OrderBy(y => y.Name).Select(z => z.Name).ToList() })
          .SelectMany(x => x.Names, (x, y) => new { Name = y, Grade = x.Grade });        
        实际上就是排序再排序的问题:
        var dics = students
          .OrderBy(g => g.Grade)
          .ThenBy(g=>g.Name)
          .Select(x =>  new { x.Grade,x.Name });


    
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值