(24)Linq初步:原理,子句,过滤,投射,分组,量词,聚合,分段,排序,拼接,联结,转换等

    
    有点长,忙于交差论文,累得象狗,这个丢下了。再复习复习。难怪“阿姨我不想努力了”点赞无数:)


一、IEnumerable接口


    1、问题引出:

        int[] ones = Enumerable.Range(0, 10).ToArray();
        foreach (var item in ones)//a
            Console.WriteLine(item);  

     
        为什么上面a处能枚举ones的内容?或者把ones改为一个整形变量,就会提示错误:没有公共方法GetEnumerator或扩展方法。
        
        原因就是foreach必须用在可枚举的接口上,即IEnumerable接口上。没有实现这个接口的会出错。
    
    
    2、IEnumerable 可枚举接口
        
        IEnumerable必须具体两个条件:(1)有公有的GetEnumerator方法;(2)返回IEnumerator枚举器。

        internal class Program
        {
            private static void Main(string[] args)
            {
                int[] ns = {1,2,3,4,5}; 
                MyClass<int> mc=new MyClass<int>(ns);
                foreach (var item in mc)
                {
                    Console.WriteLine(item);
                }
                Console.ReadKey();
            }
        }
        public class MyClass<T> : IEnumerable//c
        {
            private T[] n ;
            public MyClass(T[] ns)
            { n = ns; }
            public IEnumerator GetEnumerator()//a
            { 
                return new MyEnumerator<T>(n);
            }
        }

        public class MyEnumerator<T> : IEnumerator//b
        {
            private T[] n;
            private int position = -1;
            public object Current
            { get { return n[position]; } }
            public MyEnumerator(T[] ns)
            { this.n = ns; }
            public bool MoveNext()
            {  return position++ < n.Length; }
            public void Reset()
            { position = -1; }
        }


        a处有GetEnumerator方法,同时有返回的枚举器。
        后面的枚举器必须有三个方法:Current,MoveNext,Reset。
        
        
        问:前面提示没有GetEnumerator,若有了该方法,foreach能运行吗?
        答:不能!
            
            有公有的GetEnumerator()并不能运行,还必须要有返回的IEnumerator枚举器。这正是IEnumerable接口的特征。公有方法确保了使用接口时知道从哪里进入,枚举器确保了元素如何枚举出来。两者缺一不可。
            
            简言之:foreach必须要有IEnumerable接口才能枚举。
            
            
            
        问:IEnumerable与Enumerable,IEnumerator与Enumertor有什么区别?
        答:详见上一节(https://blog.csdn.net/dzweather/article/details/133211490?spm=1001.2014.3001.5501)
            
            
            有I的是接口,是一个规范标准,只规定了简单几个方法。对应后面的是类。
            
            IEnumerable只是简单规定几个方法,而Enumerable是一个静态类,比IEnumerable功能更全,是“十全大补丸”。
            
            IEnumertor同样是接口,而Enumerator是实体类,它的功能规定似乎与IEnumertor相同,这点与上面不一样。简言之:Enumerator是IEnumerator接口的具体实现类。
            
            
        问:例子中b与c处,已经在实现IEnumerable与IEnumerator,为什么后面还要重复加上它们?
        答:因为脱了裤子放屁,让人更知道!当然可以省略
            
            简言之:显式地指定接口的名称可以提高代码的可读性、可维护性和避免命名冲突。这是为什么在实现接口时会重复地加上接口名称的原因。
            
            当一个类实现一个接口时,可以选择显式地指定接口的名称,也可以隐式地实现接口。显式地指定接口的名称可以提高代码的可读性和可维护性。

            当一个类实现一个接口时,显式地指定接口的名称可以使代码更加清晰,明确地表明该类实现了某个特定的接口。这样,其他人在阅读代码时可以更容易地理解该类的功能和用途。
            所以,当你按F12去查看源码时,并不是看里面的方法,而是首先看的后面的继承与接口,一眼就知道它的功能了。

            此外,显式地指定接口的名称还可以避免命名冲突。如果一个类实现了多个接口,而这些接口中有相同的方法或属性名称,那么通过显式地指定接口的名称,可以消除命名冲突,明确地指定使用哪个接口中的方法或属性。
    
    
        问: public object Current => n[position];这是什么意思?
        答:这是一个Lambda表达式,在属性时的特殊写法。它 定义了一个只读属性Current,它的getter返回数组n中索引为position的元素。原型:

            public object Current
            {
                get
                {
                    return n[position];
                }
            }


            要使用lambda表达式来定义属性的setter,可以在lambda表达式中使用value关键字来表示赋给属性的值。

            public int Count
            {
                get => count;
                set => count = value;
            }


            上面,定义了一个可读写的属性Count,它的getter返回count的值,setter将传入的值赋给count。
            
            注意,lambda表达式定义的属性只能是自动实现的属性(即没有显式的字段),因为lambda表达式本身就是一个表达式,无法包含复杂的逻辑或方法体。
    
            这是属性的一种特殊写法,大多数lambda表达式外面都是有()的.


二、扩展方法


    参考:https://blog.csdn.net/dzweather/article/details/132070759?spm=1001.2014.3001.5501
    
    复习要点:
        (1)静态类中的静态方法;
        (2)静态方法第一个参数是 this+前面的类型及形参。
        (3)使用场景:不修改源码时的增加方法。
        
        
    问:为什么Linq中大多数都使用扩展方法?
    答:在 LINQ 中,大多数方法都是扩展方法的原因:

        (1)提供更自然的语法:
        通过将方法定义为目标类型的扩展方法,可以在使用 LINQ 查询时实现一种更自然的语法。这样,可以在查询表达式中直接调用方法,而不需要显式地引用目标类型。

        (2)避免修改源代码:
        扩展方法允许在不修改现有类的情况下向其添加新的功能。这对于已经存在的类和框架非常有用,因为它们不需要重新编译或修改源代码。

        (3) 支持方法链式调用:
        扩展方法可以像实例方法一样被链式调用,这样可以在查询中使用多个方法来逐步过滤、排序或转换数据。

        (4)统一的 API 风格:
        通过使用扩展方法,可以为不同类型的对象提供类似的方法,从而实现一种统一的 API 风格。这使得代码更易于阅读、理解和维护。

        扩展方法为 LINQ 提供了一种便捷、自然和一致的编程模型,使得查询和操作数据集合变得更加简单和直观。
    
            
    问:扩展方法对第一参数有要示,所以linq扩展方法第一参数要求为IEnumerable?
    答:是的.
    
        LINQ 扩展方法的第一个参数必须是一个可枚举类型(如数组、列表、集合等),以便 LINQ 可以在该序列上执行查询操作。这是因为 LINQ 扩展方法是为了在序列上进行查询和操作而设计的。

        通过将扩展方法定义为目标类型的静态方法,并将目标类型作为第一个参数,可以在使用 LINQ 查询时实现一种更自然的语法。这样,可以在查询表达式中直接调用方法,而不需要显式地引用目标类型。

        例如,假设有一个名为numbers的整数列表,我们可以使用 LINQ 扩展方法Where来筛选出大于 5 的数字:
        var result = numbers.Where(n => n > 5);

        在这个例子中,numbers是一个整数列表,Where是一个 LINQ 扩展方法,它的第一个参数是numbers列表本身。通过这种方式,我们可以直接在查询表达式中调用Where方法,而不需要显式地引用numbers列表。

        LINQ 扩展方法的第一个参数必须是一个可枚举类型,以便在该序列上执行查询操作。这种设计使得 LINQ 查询变得更加直观和易于使用。


三、Linq原理


    1、Linq主要对以下数据源进行查询:

        (1)集合(List、Array、Dictionary 等)
        (2)数据库(使用 Entity Framework 或其他 ORM 工具)
        (3)XML 文件(使用 LINQ to XML)
        (4)JSON 数据(使用 JSON.NET 库)
        (5)Web 服务(使用 HttpClient 或其他 HTTP 请求库)
        (6)并行数据源(使用 Parallel LINQ 进行并行查询)
        (7)内存中的对象集合(使用 LINQ to Objects)
        (8)数据流(使用 LINQ to DataStreams)
        (9)事件集合(使用 LINQ to Events)
        (10)SharePoint 列表(使用 LINQ to SharePoint)    
        
        一般来说,C# LINQ 主要用于对以下三种主流数据源进行查询和处理:

        (1)对象集合(Object):
        LINQ to Objects 是 C# 中最常用的 LINQ 提供程序之一,它允许对“内存中的对象集合”进行查询和操作。可以使用 LINQ 查询语法或方法语法来处理集合中的数据。

        (2)关系型数据库(SQL):
        使用 Entity Framework 或其他 ORM 工具,可以将“数据库”中的表映射为 C# 中的对象,并使用 LINQ to SQL 或 LINQ to Entities 进行查询和操作。LINQ to SQL 和 LINQ to Entities 提供了类似于 SQL 的查询语法,可以在 C# 中直接使用。

        (3)XML 文件(XML):
        使用 LINQ to XML 可以对“XML 文件” 进行查询和操作。LINQ to XML 提供了一组用于查询和操作 XML 数据的类和方法,可以使用 LINQ 查询语法或方法语法来处理 XML 数据。

        这三种主流数据源覆盖了大多数常见的数据查询和处理需求,LINQ 提供了统一的语法和方法来处理这些数据源,简化了开发过程。        
        
        
        源数据介绍:
        
            通常,源数据会在逻辑上组织为相同种类的元素序列。
            SQL数据库表包含一个行序列。与此类似,ADO.NETDataTable 包含一个 DataRow 对象序列。在XML 文件中,有一个XML元素“序列” (不过这些元素按分层形式组织为树结构)。内存中的集合包含一个对象序列。
            从应用程序的角度来看,原始源数据的具体类型和结构并不重要。应用程序始终将源数据视为一个IEnumerable<T>或lQueryable<T> 集合。
            在 LINQto XML 中,源数据显示为一个IEnumerable<XElement>。
            在LINQto DataSet 中,它是一个Enumerable<DataRow>。
            在LINQtoSQL中,它是您定义用来表示 SQL表中数据的任何自定义对象的IEnumerable 或iQueryable。        
    
    
    2、扩展方法
    
        理解linq为什么用扩展方法,怎么用扩展方法,具体怎么写扩展方法?
        参考:https://blog.csdn.net/dzweather/article/details/133211490?spm=1001.2014.3001.5501
        这里面的第四部分,分别写了where,select等。
        
    
    3、linq查询有两种表达式: 查询式、方法链式。
        
        这两种表达式在功能上是等效的,可以根据个人喜好和需求选择使用哪种表达式。无论是查询表达式还是方法链表达式,它们都可以通过编译器转换为相同的查询语句,并产生相同的结果。

        (1)查询表达式(Query Expression):
        查询表达式使用类似于 SQL 的语法来编写查询。它使用关键字(如 from、where、select 等)来描述查询的逻辑和条件。查询表达式更易读和理解,特别适用于简单的查询。

        var query = from student in students
                    where student.Age > 18
                    select student.Name;

        (2)方法链表达式(Method Chaining Expression):
        方法链表达式使用一系列的方法调用来构建查询。它使用 LINQ 扩展方法(如 Where、Select、OrderBy 等)来描述查询的逻辑和条件。方法链表达式更加灵活和直观,特别适用于复杂的查询和操作。

        var query = students
                    .Where(student => student.Age > 18)
                    .Select(student => student.Name);

                
    4、匿名类
        
        匿名类是一种临时创建的类,它允许在运行时动态定义类的结构和属性,而无需显式定义一个具名的类。

        匿名类通常用于临时存储和传递一组相关的数据,而无需为每个数据定义一个独立的类。它可以在 LINQ 查询、匿名类型的初始化和临时数据传递等场景中使用。匿名类的定义形式如下:
        var anonymousObject = new { Property1 = value1, Property2 = value2, ... };
        其中,Property1、Property2 等是匿名类的属性名,value1、value2 等是对应属性的值。属性名和值是一一对应的。        匿名类的属性是只读的,即不能修改属性的值。它的属性类型是根据属性值的类型自动推断的。

        var person = new { Name = "John", Age = 25, Occupation = "Engineer" };

        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}, Occupation: {person.Occupation}");


        上面我们创建了一个匿名类 person,它有三个属性:Name、Age 和 Occupation。我们可以通过属性名来访问和获取属性的值。

        注意,由于匿名类是在运行时动态创建的,因此它的类型是编译器自动生成的,无法在代码中显式引用。因此,匿名类的作用范围通常是局限在当前方法或作用域内部。        
    
    
        问:为什么匿名类的属性是只读的,即不能修改属性的值?
        答:匿名类是一种特殊类型的类,它用于临时封装一组相关的只读属性。匿名类的属性是只读的,是因为匿名类的设计初衷是用于快速创建临时对象,并提供一种方便的方式来封装一组数据,而不是用于修改数据。            当你创建一个匿名类时,它的每个属性都被初始化为指定的值,并且这些值不能在后续的代码中进行修改。这是因为匿名类是不可变的(immutable),一旦创建就不能修改。

            var person = new { Name = "John", Age = 30 };

            Console.WriteLine(person.Name); // 输出 "John"
            Console.WriteLine(person.Age);  // 输出 30

            // 以下代码会导致编译错误,因为属性是只读的
            // person.Name = "Alice";
            // person.Age = 25;

            如果你需要修改属性的值,那么匿名类并不适合你的需求。相反,可以考虑定义一个具名类(named class),其中的属性可以根据需要进行修改。
    
    
    5、Select 映射(返回一个新的序列)
    
        select用于从查询结果中选择指定的数据或属性。它可以用于选择整个对象、对象的属性或计算的结果。

        (1)选择整个对象:

        var query = from student in students
                    select student;

        (2)选择对象的属性:

        var query = from student in students
                    select student.Name;

        (3)选择计算的结果:

        var query = from student in students
                    select new { FullName = student.FirstName + " " + student.LastName, Age = DateTime.Now.Year - student.BirthYear };


        这个查询将返回一个匿名类型的集合,每个匿名类型对象都有 FullName 和 Age 属性。FullName 属性是学生的全名,由 FirstName 和 LastName 属性拼接而成。Age 属性是学生的年龄,根据 BirthYear 属性计算得出。

        select在方法链表达式中的用法:

        var query = students.Select(student => student.Name);


    
        
        问:select中匿名类与具名类有什么区别?
        答:下面a处与b处:

        internal class Program
        {
            private static void Main(string[] args)
            {
                List<Student> list = new List<Student>()
                {
                    new Student() {Id=0,Name="Ant编程1",ClassId=3,Age=25},
                    new Student() {Id=1,Name="Ant编程2",ClassId=2,Age=13},
                    new Student() {Id=2,Name="Ant编程3",ClassId=2,Age=17},
                    new Student() {Id=3,Name="Ant编程4",ClassId=2,Age=16},
                    new Student() {Id=4,Name="Ant编程5",ClassId=2,Age=25},
                    new Student() {Id=5,Name="Ant编程6",ClassId=3,Age=24},
                    new Student() {Id=6,Name="Ant编程7",ClassId=2,Age=21},
                    new Student() {Id=7,Name="Ant编程8",ClassId=2,Age=18},
                    new Student() {Id=8,Name="Ant编程9",ClassId=2,Age=34},
                    new Student() {Id=9,Name="Ant编程10",ClassId=3,Age=30},
                    new Student() {Id=10,Name="Ant编程13",ClassId=3,Age=30},
                    new Student() {Id=11,Name="Ant编程11",ClassId=3,Age=25},
                    new Student() {Id=12,Name="Ant编程12",ClassId=3,Age=28},
                };
                var query1 = list.Select(s => new { s.Id, s.Name });//a
                var query2 = list.Select(s => new StudentN{ Id=s.Id, Name=s.Name });//b
                foreach (var student in query2)//c
                {
                    ProcessStudent(student); //d 需要将匿名类型转换为object类型传递给方法
                }
                Console.ReadKey();
            }
            private static void ProcessStudent(StudentN student)//e
            {
                // 处理学生对象的逻辑
            }
        }
        class StudentN
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }

        class Student
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public int ClassId { get; set; }
            public int Age { get; set; }
        }


        上面a处是匿名类,b处是具名类。
        当需要引用查询结果的类型或将结果传递给其他方法或部分时,使用具体类型的查询方式会更有优势。
        d处进行引用到e处进行使用。因为c处用的是查询使用的是具名类,因为在e处可以指明是StudentN,但若在c处query2改为query1,则e处报错,因为匿名类没有类名,须将e处StudentN改为object,方为正确,
    
        尽管匿名类型可以在某些情况下完成任务,但具体类型通常更有利于代码的类型安全性和可维护性,特别是在需要后续处理、传递或与其他方法交互的情况下。尤其是在以下方面:

        (1)类型安全性: 具体类型提供了编译时的类型检查,这意味着编译器可以捕获到类型不匹配的错误。如果你在后续操作中需要处理特定类型的数据,使用具体类型可以减少类型相关的错误。

        (2)代码维护性: 具体类型提供了更明确的代码结构,使代码更容易理解和维护。其他开发人员可以更容易地理解代码的意图,因为类型信息是显式的。

        (3)方法调用: 如果你需要将结果传递给其他方法,那么具体类型可以更容易地与这些方法的参数匹配。匿名类型在方法之间传递时可能需要进行转换,而具体类型则可以直接传递。
    
    
    6、LINQ分类
    
        INQ包括五个部分: LINQ to Obiects、LINQ to DataSets、LINQto SQL、LINQto Entities、LINQ to XML
        
        LINQ to obiect
            LINQ的核心组件,将查询表达式与C#编程语言集成,实现对内存对象表示的数据源进行查询和分析。
        
        数据源为实现了接口IEnumerable<T>或IQueryable<T>的内存数据集合。
        LINQ to ADO.NET 将LINQ与ADO.NET集成,借助ADO.NET实现对多种数据库数据的查询和分析。
        包括LINQ to SQL和LINQ to DataSet,前者主要操作外部数据库,后者操作内存DataSet。
        LINQ to XML是一种全新的XML数据操作模式,数据源为XML文档,通过XElement,Xattribute等类型将XML文档数据加载到内存,通过LINQ进行数据查询。
        LINQ to Entities 使开发人员能够通过使用 LINQ 表达式和 LINQ 标准查询运算符,直接从开发环境中针对实体框架对象上下文创建灵活的强类型查询。
        LINQ to Entities 查询使用对象服务基础结构    

    
    7、LINQ查询包括四个主要元素
        
        LINQ查询的根本目的是从指定的数据源中查询满足符合特定条件的元素,并且根据需要对这些查询到的元素进行排序、连接等操作。
        
        LINQ查询包括四个主要元素:
        数据源
            数据源表示LNQ查询将从哪里查找数据,它通常是一个或多个数据集,每个数据集包含一系列的元素.
        目标数据
            目标数据用来指定查询具体想要的是什么数据,在LNQ中,它定义了查询结果数据集中元素的具体类型.
        筛选条件
            筛选条件定义了对数据源中元素的过滤条件,只有满足条件的元素才作为查询结果返回。筛选条件可以是简单的逻辑表达式,也可以是复杂的逻辑函数。
        附件操作
            附加操作表示一些其他的对查询结果的辅助操作,比如,对查询结果进行排序,分组等.
        
        数据源和目标数据是LINQ查询的必备元素,筛选条件和附加操作是可选元素.
        
        注意: LINQ查询代码中关键字必须小写。


四、Linq to Sql


    1、对数据库查询主要是利用IQueryable
        
        当我们需要与数据库进行交互时,通常会使用IQueryable接口进行查询操作,而当我们对内存中的集合进行查询时,可以使用IEnumerable接口。
        
        LINQ to Objects主要使用IEnumerable接口进行查询。IEnumerable接口表示一个可枚举的集合,可以对其进行迭代和查询操作。
        当我们对内存中的集合、数组等进行查询时,通常使用LINQ to Objects。

        而对于数据库查询,LINQ to SQL或Entity Framework等提供的LINQ提供者主要使用IQueryable接口进行查询。IQueryable接口继承自IEnumerable接口,提供了更丰富的查询能力和优化性能的机会。
        通过IQueryable接口,我们可以将查询表达式转换为相应的SQL查询,并通过数据库提供者执行查询。

        IQueryable接口的主要优势在于它支持延迟加载和表达式树。
        延迟加载意味着查询不会立即执行,而是在需要结果时才会执行,这样可以优化查询性能。而表达式树允许我们以代码的形式构建查询,使得查询的构建更加灵活和可组合。
    
    
    
    2、IQueryable 可查询接口
        
        IQueryable接口是LINQ查询的核心部分之一,它继承IEnumerable接口并扩展,提供了更丰富的查询能力。
        
        IQueryable接口定义了用于构建和执行查询的方法和属性。它允许开发人员以一种类似于SQL的方式来查询数据源,无论是内存中的集合、数据库还是其他数据源。

        public interface IQueryable:IEnumerable
        {
            Expression Expression
            Type ElementType
            IQueryProvider Provider
        }


        IQueryable接口的主要特点包括:

        (1)延迟加载:
        IQueryable查询是延迟加载的,即查询不会立即执行,而是在需要结果时才会执行。这样可以优化查询性能,只加载需要的数据。

        (2)表达式树:
        IQueryable查询使用表达式树来表示查询操作。开发人员可以使用Lambda表达式或其他方式来构建查询表达式树,这样可以更灵活地构建查询。

        (3)强类型:
        IQueryable查询是强类型的,即查询结果的类型是在编译时确定的。这样可以提供更好的类型安全性和编译时错误检查。

        (4)可组合性:
        IQueryable查询可以进行多次组合,以构建复杂的查询。开发人员可以通过链式调用方法来添加过滤条件、排序规则、投影等操作,从而构建出最终的查询。    
    
    
        问:IQueryable只针对数据库,既然功能更丰富,能用在Linq to object吗?
        答:可以。须经转换,但因转换性能反而降低。一股只用IEnumerable.
            
            在LINQ to Objects中,我们通常使用IEnumerable接口进行查询操作,而不是IQueryable接口。
            因为LINQ to Objects主要是针对内存中的集合、数组等进行查询,不需要将查询转换为SQL语句。

            IEnumerable接口提供了一组基本的查询操作方法,例如Where、Select、OrderBy等,可以对集合进行筛选、投影、排序等操作。
            这些方法返回的结果仍然是IEnumerable类型,因此在LINQ to Objects中,查询操作是在内存中进行的,不需要额外的数据库查询。

            虽然在LINQ to Objects中不能直接使用IQueryable接口,但是可以通过调用AsQueryable方法将IEnumerable转换为IQueryable,从而使用IQueryable提供的更丰富的查询能力。
            但是这种转换并不会将查询转换为SQL语句,仍然是在内存中进行查询。
            List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
            IQueryable<int> query = numbers.AsQueryable();
            var result = query.Where(n => n > 2).Select(n => n * 2);
            foreach (var item in result)
                Console.WriteLine(item);
            将IEnumerable转换为IQueryable主要是为了能够使用IQueryable提供的更丰富的查询能力,例如使用表达式树构建查询、进行延迟加载等。
            但是在性能方面,并没有直接的提升,甚至可能稍微降低性能。如果只需要在内存中进行简单的查询操作,直接使用IEnumerable可能更加高效。
            
            将IEnumerable转换为IQueryable并不会直接提高性能,因为转换后的查询仍然是在内存中进行的。
            实际上,这种转换可能会稍微降低性能,因为它需要进行额外的转换和包装操作。

            在LINQ to Objects中,IEnumerable接口是基于迭代器模式实现的,它逐个返回集合中的元素。
            而IQueryable接口是基于表达式树的查询提供者模式实现的,它将查询表达式转换为相应的查询语句并执行。

            当我们调用AsQueryable方法将IEnumerable转换为IQueryable时,实际上是将IEnumerable包装为一个查询提供者,这个查询提供者可以解释和执行查询表达式。但是,由于LINQ to Objects是在内存中进行查询,所以这个查询提供者并不会将查询转换为SQL语句或其他外部查询语言。
            
            
        问:IQueryabel既然比较丰富,IEnumerable的能用IQueryable吗?
        答:下面是可将Linq to object转为Linq to SQL方法,即强制用IQueryable:

        var query = list.Where(x => x.Id > 3);//a
        var query1 = list.AsQueryable();
        var query2 = query1.Where(x => x.Id > 3);//b
        
        public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
        
        public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)


    
        使用Expression<Func<T, bool>>而不是Func<T, bool>可以提供更高级的查询功能,并允许我们在运行时动态构建和修改查询表达式。
        
        在LINQ查询中,使用Expression<Func<T, bool>>而不是Func<T, bool>的原因是为了构建可执行的查询表达式树。

        当我们使用Expression<Func<T, bool>>时,我们实际上是在构建一个表示查询条件的表达式树。表达式树是一个由表达式节点构成的树状结构,它可以被解释和执行,而不仅仅是作为一个委托进行调用。

        通过使用表达式树,我们可以将查询条件表示为代码的结构化形式,而不仅仅是作为一个委托的匿名函数。这使得我们可以在运行时动态地构建和修改查询表达式,从而实现更灵活和可扩展的查询。

        另外,使用Expression<Func<T, bool>>还可以将查询条件传递给其他支持表达式树的查询提供程序,如LINQ to SQL或Entity Framework。这些查询提供程序可以解析表达式树,并将其转换为相应的查询语言,例如SQL,以在数据库中执行查询。
    
    
        问:同样的IEnumerable可以查询数据库吗?
        答:可以。但有下面的缺点:
            
            内存消耗:
            当使用IEnumerable进行查询时,所有的数据都会加载到内存中,这可能导致内存消耗过大,尤其是对于大型数据集或复杂查询情况下。( 即使你用了where,也会从服务器上把整个数据返回到本地的内存中,再进行过滤)

            性能问题:
            由于IEnumerable是在内存中进行查询,而不是在数据库中进行,因此查询的性能可能较低。这是因为查询需要从数据库中检索所有的数据,然后在内存中进行筛选和操作。

            延迟加载:
            IEnumerable是基于迭代器的,它使用延迟加载机制,只有在访问数据时才会执行查询。这可能导致在某些情况下多次访问数据库,增加了数据库的负担。


五、子句


    1、查询表达式
        
        查询表达式必须以from子句开头,以select或group子句结束。
        第一个from子句和最后一个select子句或group子句之间,可以包含一个或多个where子句、let子句、join子句、orderby子句和group子句,甚至还可以是from子句。它包括8个基本子句,具体说明如下所示:
        
        from子句: 指定查询操作的数据源和范围变量
        select子句: 指定查询结果的类型和表现形式。
        where子句: 指定筛选元素的逻辑条件。
        let子句:引入用来临时保存查询表达式中的字表达式结果的范围变量。
        orderby子句: 对查询结果进行排序操作,包括升序和降序。
        group子句: 对查询结果进行分组。
        into子句: 提供一个临时标识符。join子句、group子句或select子句可以通过该标识符引用查询操作中的中间结果
        join子句: 连接多个用于查询操作的数据源。        

        int[] arr = Enumerable.Range(0, 10).ToArray();

        var query1 = from n in arr
                     select n.ToString();//由整形序列变成也字符串序列

        var query2 = from n in arr
                     where n % 3 == 0
                     select n * 3 + 4;

        foreach (var item in query2)
            Console.WriteLine(item);    


    
    
    2、from...in...数据源。自动推断类型。
        
        (1)复合form子句

        List<int> list1 = Enumerable.Range(0, 3).ToList();
        List<int> list2 = Enumerable.Range(0, 3).ToList();
        var query1 = from a in list1
                     from b in list2
                     select a + b;
        var query2 = list1.Join(list2, a => 1, b => 1, (a, b) => a + b);
        叉积,共3X3=9个元素.加上where后变化:
        var query1 = from a in list1
                     where a != 2
                     from b in list2
                     where b < 2
                     select a + b;
        var query2 = list1.Where(x => x != 2).Join(list2.Where(y => y < 2), a => 1, b => 1, (a, b) => a + b);    

    
        
        (2)联接form

        internal class Student
        {
            public List<int> Scores { get; internal set; }
            public string Name { get; internal set; }
        }        
        //主程序中:
        List<Student> stus = new List<Student>
        {
           new Student {Name="Ome", Scores= new List<int> {97, 72, 81, 60}},
           new Student {Name="Don", Scores= new List<int> {75, 84, 91, 39}},
           new Student {Name="Mor", Scores= new List<int> {88, 94, 65, 85}},
           new Student {Name="Gar", Scores= new List<int> {97, 89, 85, 82}},
           new Student {Name="Bee", Scores= new List<int> {35, 72, 91, 70}}
        };
        var query1 = from s in stus
                     from sc in s.Scores
                     where sc > 80
                     select new { s.Name, sc };
        var query2 = stus.SelectMany(x => x.Scores, (x, y) => new { Name = x, Score = y }).Where(a => a.Score > 80);    


        
        
    3、select 映射,又名map
        
        将原序列中的每个元素映射为新的元素,并返回一个新的序列。
        比如映射中改变序列类型(见前面例子tostring),或者进行运算.

        var list = new List<int> { 6, 7, 8, 9 };
        var query1 = from idx in Enumerable.Range(0, list.Count)
                     let value = list[idx]
                     select new { ID = idx + 1, Value = value };
        var query2 = list.Select((value, index) => new { ID = index + 1, Value = value });    


        注意:
        (1)let子句在查询表达式中用于引入一个临时变量,可以在后续的子句中使用。上面,let value = list[index]的作用是为每个索引对应的元素创建一个临时变量value,以便在后续的select子句中使用。
        
        使用let关键字声明的变量是只读的,不能对其进行修改。
        
        (2)select后面的lambda只有两种:Func<T,TResult>或Func<T,T1,TResult>,所以可引用本身的元素,也可以两个,另一个可以作为索引。
        
        在Select方法使用两个参数时,第一个参数是当前元素的值,第二个参数是当前元素的索引。在你提供的示例中,n是当前元素的值,x是当前元素的索引。
        var query2 = list.Select((n, x) => new { FF = n + 1, DD = x + 1 });
        上面就可以判断n是元素,x是索引
    
        如果添加索引的话,可以使用这个功能。
        注意:在查询表达式中,使用Range先提取索引,再let值,最后,变换输出。这是一个固定的套路。
    
        
    4、Where 过滤(filter) 返回满足条件的元素。
        
        可通过多个条件来达到,比如&&,||等,甚至还用方法返回值。

        List<int> list = Enumerable.Range(1, 9).ToList();
        var query1 = from n in list
                     where n > 3
                     where n % 2 == 0
                     select n;//a
        var query2 = from n in list
                     where n > 3 && n % 2 == 0
                     select n;//b
        var query3 = from n in list
                     where GetBool(n)//c
                     select n;//d
        var query4 = list.Where(n => n > 3 && n % 2 == 0);//e        
        
        private static bool GetBool(int n)
        {
            return n > 3 && n % 2 == 0;
        }


        上面各语句都是等效的。c处使用方法。
        a,b,d处都的select是不能省略的,否则提示:查询正文必须以select或group结束.
        
        当查询语句没有以 select 或 group 结束时,编译器无法确定查询的最终结果应该是什么,因此会出现语法错误。
        
        注意:这只是针对查询式表达式,对方法链表达式没有此要求。

        List<int> list = Enumerable.Range(1, 9).ToList();
        var query1 = from n in list
                     group n by n % 2;
        var query2 = list.GroupBy(x => x % 2);

        foreach (var item in query2)
            foreach (var n in item)
                Console.WriteLine(item.Key + "--" + n);


        上面以group结束,即以分组结束.
        
    
    5、let 范围变量。
        
        暂存范围变量,以供后面引用,使用值进行初始化后,范围变量不能用于存储另一个值(只读)。 但是,如果范围变量持有可查询类型,则可以查询该变量。

        只用于查询式表达式。
    

        IEnumerable<int> list1 = new List<int>() { 1, 3, 5, 7 };
        IEnumerable<int> list2 = new List<int>() { 2, 4, 6, 8, 0 };
        List<int> list = list1.Concat(list2).ToList();
        var query1 = from n in list
                     let num = n % 2  //a
                     where num == 0
                     select n;
        var query2 = list.Where(n => n % 2 == 0);  

 
        上面a处对每一个元素,用范围变量num来暂存,以供后面where使用。经过取余后,只有1和0两种,where过滤采用0即偶数,故最后的结果实际就是选取偶数。
        
        
        下面是一个自查询:

        string[] strs =
        {
            "A penny saved is a penny earned.",
            "The early bird catches the worm.",
            "The pen is mightier than the sword."
        };
        var query1 = from sentence in strs
                     let words = sentence.Split(' ')
                     from word in words
                     let w = word.ToLower()
                     where w[0] == 'a' || w[0] == 'p' || w[0] == 'e'
                     select word;
        var query2 = strs.SelectMany(s => s.Split(' '), (s, w) => w).Where(x => { char c = x.ToLower()[0]; return c == 'a' || c == 'p' || c == 'e'; });
        foreach (var item in query1)
            Console.WriteLine(item);


        strs提取每个句子,再查每个单词,再取首字母,返回满足条件的单词。
        
        
        问:范围变量取名为中间变量不是更为贴切?
        答:you are right!
        
            尽管中间变量是更贴切的术语,但C#官方文档中将这些变量称为范围变量。
            这是因为这些变量的作用范围仅限于查询表达式内部,而不是整个方法或类。这种命名约定可能是为了强调变量的作用范围的限制。
            无论是将这些变量称为中间变量还是范围变量,它们的作用是一样的:在查询表达式中临时存储计算结果。
    
    
    6、OrderBy 排序
    
        对序列或子序列(组)以升序或降序排序。 若要执行一个或多个次级排序操作,可以指定多个键。 默认排序顺序为升序ascending。 

        var list = new List<int>() { 8, 2, 7, 9, 3, 4, 1, 5 };
        var query1 = from n in list
                     orderby n
                     select n;
        var query2 = list.OrderBy(n => n);    

        可多级排序。只是对于方法链来说Thenby只能在OrderBy之后。

        List<Student> stus = new List<Student>
        {
           new Student {First="Svetlana", Last="Omelchenko", ID=111},
           new Student {First="Claire", Last="O'Donnell", ID=112},
           new Student {First="Sven", Last="Mortensen", ID=113},
           new Student {First="Cesar", Last="Garcia", ID=114},
           new Student {First="Debra", Last="Garcia", ID=115}
        };
        var query1 = from s in stus
                     orderby s.First, s.Last descending
                     select s;
        var query2 = stus.OrderBy(s => s.First).ThenByDescending(x => x.Last);

        var query3 = from s in stus
                     orderby s.First, s.Last descending
                     group s by s.Last[0] into newGroup
                     orderby newGroup.Key
                     select newGroup;
        var query4 = stus.OrderBy(s => s.First).ThenByDescending(x => x.Last).GroupBy(y => y.Last[0]).OrderBy(x => x.Key);
        foreach (var s in query4)
            foreach (var x in s)
                Console.WriteLine(s.Key + " " + x.First + " " + x.Last);    


    
        
        Reverse 反转(倒置)
        Reverse是一个用于反转序列的操作符。它会将原始序列中的元素按照相反的顺序重新排序,并返回一个新的序列。

        Reverse操作符可以应用于任何实现了IEnumerable<T>接口的集合类型,包括数组、列表、链表等。它可以用于反转整个序列,也可以用于反转序列的一个子集。

        int[] nums = { 1, 2, 3, 4, 5 };
        var query1 = nums.Reverse();//5,4,3,2,1,
        var query2 = nums.Skip(1).Take(3).Reverse();//4,3,2


        注意,Reverse操作符不会改变原始序列,而是返回一个新的反转序列。
            如果您希望在原始序列上进行反转操作,可以使用Array.Reverse或List.Reverse等原地反转方法。

        int[] nums = { 1, 2, 3, 4, 5 };
        Array.Reverse(nums);
        foreach (var item in nums)
            Console.Write(item + ",");


        上面是对原序列进行更改,nums变成了5,4,3,2,1。
    
    
    7、Group 分组
        
        对序列进行分组时,每个分组的元素会被重新组织,并包含额外的 Key 属性。这个 Key 属性代表了分组的依据,它是一个用来唯一识别分组的值。

        除了分组的 Key 属性之外,每个分组还包含一个 IEnumerable<T> 类型的集合,其中存储了原始序列中属于该组的元素。因此应注意每一个元素中还有子序列。
        
        IGrouping<TKey,TElement>
        group 子句返回一个 IGrouping<TKey,TElement> 对象序列,这些对象包含零个或更多与该组的键值匹配的项。
        TKey 键的类型。这是协变类型参数。 即,可以使用指定的类型,也可以使用派生程度较高的任何类型。 
        TElement 值的类型。这是协变类型参数。 即,可以使用指定的类型,也可以使用派生程度较高的任何类型。 

        var list = Enumerable.Range(1, 10).ToList();
        var query1 = from n in list
                     group n by n % 3;//a
        var query2 = from n in list
                     group n by n % 3 into g
                     select g;//b
        var query4 = list.GroupBy(g => g % 3);

        foreach (var s in query4)
            foreach (var x in s)
                Console.WriteLine(s.Key + " " + x);      

 
        上面a处不再利用分组结果,所以省略into直接结束。而b处要使用分组,必须加上into g,后面再对分组结果映射。
        下面是错误的:
        var query1 = from n in list
                     group n by n % 3
                     select n;
        尽管不使用分组结果(所以没有into g),但linq本身对显而易见的累赘是报错的。不要考验机器的智商,它会不耐烦的。
        
        
        分组的Key是由by后面的值来决定,上面是n%3,只有三种1,2,0,因此分组的结果只有三组。对于>,<,>=等结果值只有true与false两种,故分组只有true,false两种。另外注意的是,分组后,有key,但不是一定必须有后面的TElement,即组里没有成员。

        List<Student> stus = new List<Student>
        {
           new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 81, 60}},
           new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int> {75, 84, 91, 39}},
           new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {99, 89, 91, 95}},
           new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}},
           new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {97, 89, 85, 82}}
        };
        var query1 = from s in stus
                     group s by s.Scores.Average() > 80;
        var query2 = stus.GroupBy(s => s.Scores.Average() > 80);
        foreach (var s in query1)
            foreach (var x in s)
                Console.WriteLine(s.Key + "   " + x.First);     

   
        结果:
            False   Svetlana
            False   Claire
            False   Cesar
            True   Sven
            True   Debra
        
        
        要取分数段的值进行分组,比如60-69一组,70-79一组,80-90一组等等,

        var query1 = from s in stus
                     group s by s.Scores.Average() / 10;
        var query2 = stus.GroupBy(s => s.Scores.Average() / 10);   

     
        结果:
            7.75   Svetlana
            7.225   Claire
            9.35   Sven
            7.55   Cesar
            8.825   Debra        
        (1)当两个整数进行除法运算时,如果无法整除,C#会将结果扩展为 double 类型。这意味着除法运算的结果将是一个浮点数,而不是一个整数。所以结果左侧的分组是小数,没有起到作用。
        (2)要起到范围分组,需要舍去小数,取整。

        var query1 = from s in stus
                     group s by Math.Floor(s.Scores.Average() / 10);
        var query2 = stus.GroupBy(s => Math.Floor(s.Scores.Average() / 10));     

   
        结果:
            7   Svetlana
            7   Claire
            7   Cesar
            9   Sven
            8   Debra
        Math.Floor()是向下取整,Math.Ceiling()是向上取整。后面再对key排序:

        var query1 = from s in stus
                     group s by Math.Floor(s.Scores.Average() / 10) into g
                     orderby g.Key
                     select g;//此处不能省略                         
        var query2 = stus.GroupBy(s => Math.Floor(s.Scores.Average() / 10)).OrderBy(g => g.Key);        


        也可对里面的范围增加说明:
        var query1 = from s in stus
                     group s by Math.Floor(s.Scores.Average() / 10) into g
                     orderby g.Key
                     select new { Range = $"{g.Key * 10}-{g.Key * 10 + 9}", Students = g };
        var query2 = stus.GroupBy(s => Math.Floor(s.Scores.Average() / 10)).OrderBy(g => g.Key).Select(g => new { Range = $"{g.Key * 10}-{g.Key * 10 + 9}", Students = g });        
        
        
        问:为什么group x by x.yy中的x是不能省略的?(中级)
        答:(1)分组实际是按x.yy分组,x.yy有几种值,那么分的组就有多少组。
            (2)分组后的值是IGrouping<Tkey,TElement>,这个TKey就上面分组后的Key,TElement是一个子序列(可能是空)就是每组的序列。
            最重要的,这个TElement就来源于这个x,由x组成。包括如果有into g ,这个g就包含了x.
            如果省略了这个x,那么谁来指定TElement序列的组成呢?
            所以这个x是不能省略的。
        
        上面例子都是由原始的元素来指定,所以看不同差异。

        List<TableA> TableA = new List<TableA>()
        {
            new TableA() { Id = 1, Name = "John" },
            new TableA() { Id = 2, Name = "Mary" },
            new TableA() { Id = 3, Name = "John" }
        };
        List<TableB> TableB = new List<TableB>()
        {
            new TableB() { ForeignKey = 1, Value = 10.5m },
            new TableB() { ForeignKey = 2, Value = 20.2m },
            new TableB() { ForeignKey = 1, Value = 15.3m },
            new TableB() { ForeignKey = 2, Value = 5.8m },
            new TableB() { ForeignKey = 1, Value = 8.2m }
        };
        var query1 = from a in TableA
                     join b in TableB on a.Id equals b.ForeignKey
                     group b.Value by a.Name into g
                     select new { g.Key, Sum = g.Sum() };
        var query2 = TableA.Join(TableB, a => a.Id, b => b.ForeignKey, (a, b) => new { a, b }).GroupBy(g => g.a.Name, g => g.b.Value).Select(x => new { x.Key, Sum = x.Sum() });     

   
        (1)在查询式表达式中:分组中g的key是由by后面的a.Name来决定,即以a.Name来分组,每组组成的子序列是由group与by之间的b.Value来组成。
        
        (2)在方法链表达式中:GroupBy(g => g.a.Name, g => g.b.Value)。
        通常情况下,GroupBy方法的第一个参数指定了分组的键类型TKey,而第二个参数则指定了组内元素的类型TElement。

        如果只指定了分组的键类型TKey,而没有指定组内元素的类型TElement,则默认情况下,组内元素的类型将是源序列的元素类型。
                
        
    8、into 上下文关键字创建临时标识符
        
        将 group、join 或 select 子句的结果存储至新标识符。 此标识符本身可以是附加查询命令的生成器。 有时称在 group 或 select 子句中使用新标识符为“延续”。

        var list = new List<int> { 1, 2, 3, 4, 5, 6 };
        var query = from n in list
                    group n by n % 2 into g
                    select new { Key = g.Key, Values = g.ToList() };


        简言之:它就是创建了一个临时变量,方便后继子句的再次引用。
        
        注意:
        List<int> list = Enumerable.Range(1, 10).ToList();
        var query1 = from n in list
                     where n > 1 && n < 6
                     group n by n % 2 into g//a
                     from nc in g//b
                     select nc;//c
        var query2 = list.Where(n => n > 1 && n < 6).GroupBy(g => g % 2).SelectMany(x => x);
        最后的结果是单元素列举,而不是组元素。对于每一个组元素(a处)在b处进行再次列举出每个单一元素,最后通过c处的select列出每个单一的元素。所以最后的结果不再是组元素组成,而是单一的元素序列。
        
    
    9、Join 连接.
        
        linq中的join只有:内连接,组连接,左外连。
        
        
        (1)内联接(join in on)
        用于将两个或多个集合中的元素匹配的操作符。内联连接中,使用关键字 join 和 on 来指定要连接的集合、匹配条件和返回结果。两表连接 on 后面条件只能用equals。
        内联连接只会返回集合中满足匹配条件的元素。

        List<Category> cates = new List<Category>()
        {
            new Category() { CategoryId = 1,CategoryName = "服装"},
            new Category() { CategoryId=2,CategoryName="食品"},
            new Category() { CategoryId=3,CategoryName="办公"},
            new Category() { CategoryId = 4, CategoryName = "饮料" }
        };
        List<Product> prods = new List<Product>()
        {
            new Product() { ProductId = 1, ProductName = "雪中飞羽绒服", Price = 998,Storage = 1500,CategoryId = 1},
            new Product() { ProductId= 2, ProductName= "安踏运动鞋", Price=198,Storage=500,CategoryId=1 },
            new Product() { ProductId= 3, ProductName= "361T恤", Price= 95,Storage= 120,CategoryId= 1 },
            new Product() { ProductId = 4, ProductName= "旺旺雪饼", Price = 16.5,Storage = 200,CategoryId = 2 },
            new Product() { ProductId = 5, ProductName = "汇源果汁", Price = 6, Storage = 2000, CategoryId = 2 },
            new Product() { ProductId = 6, ProductName = "英雄钢笔", Price = 12.5, Storage = 10, CategoryId = 3 },
            new Product() { ProductId = 7, ProductName = "小米音箱", Price = 99, Storage = 10, CategoryId = 7 }
        };
        var query1 = from p in prods
                     join c in cates on p.CategoryId equals c.CategoryId
                     select new { p.ProductName, c.CategoryName };
        var query2 = prods.Join(cates, p => p.CategoryId, c => c.CategoryId, (p, c) => new { p.ProductName, c.CategoryName });
        foreach (var item in query1)
            Console.WriteLine(item);


        上面prods中第7个元素(小米音箱),因为在cates没有匹配,所以不会在结果中显示。
        
        警告:
            上面内联接中on条件中的左右顺序不能颠倒。比如写成:join c in cates on c.CategoryId equals p.CategoryId,把左表成员p写在equals的右侧,而右表的成员c写在equals的左侧,也会报错,所以这里按左右表的顺序要对应写。
        
        
        (2)组连接(join in on into)
        组联接(join into)是一种内联连接(join)操作的变体,它允许我们对结果进行分组。除了使用 join 和 on 来指定连接条件外,我们还可以使用 into 关键字将结果分组,并使用 group 子句来指定分组条件。仍然是上面的数据:

        var query1 = from c in cates
                     join p in prods on c.CategoryId equals p.CategoryId into cp
                     select new { c.CategoryName, CP = cp };
        var query2 = cates.GroupJoin(prods, c => c.CategoryId, p => p.CategoryId, (c, ps) => new { c.CategoryName, CP = ps });
        foreach (var item in query1)
            foreach (var s in item.CP)
                Console.WriteLine(item.CategoryName + ":" + s.ProductName);    


        结果:
            服装:雪中飞羽绒服
            服装:安踏运动鞋
            服装:361T恤
            食品:旺旺雪饼
            食品:汇源果汁
            办公:英雄钢笔
        (a)cp是一个分序列,由cates根据c.CategoryId对prods进行分组,凡是满足条件的p就进入cp。因此,cp是由p组成,类似是IEnumerable<Category>序列,元素可能是一个、多个,也有可能是0个。
        (b)列举时,因为每一个元素时有一个分序列,所以还要再次对每个元素进行再次的枚举,才能把满足条件的p枚举出来。而cp往往是使用聚合方法的关键。比如求平均

        var query1 = from c in cates
                     join p in prods on c.CategoryId equals p.CategoryId into cp
                     select new { c.CategoryName, Total = cp.Sum(x => x.Price * x.Storage) };
        var query2 = cates.GroupJoin(prods, c => c.CategoryId, p => p.CategoryId, (c, cp) => new { c.CategoryName, Total = cp.Sum(x => x.Price * x.Storage) });
        foreach (var item in query2)
            Console.WriteLine(item.CategoryName + ":" + item.Total);     

   
        结果:
            服装:1607400
            食品:15300
            办公:125
            饮料:0
        
        
        (3)左连接join in on into from in x.defaultifempty
        左连接是一种连接操作,它返回左侧序列中的所有元素,以及与右侧序列中匹配的元素。如果右侧序列中没有匹配的元素,则返回默认值或 null。
        
        将里面的数据源颠倒一下就可以实现右连接,所只需要左连接即可。
        
        左连接的集合中元素的个数,为内连接的个数加上左表与右表中不匹配的个数。
        因此左连接是左表为主,向右表发生匹配与不匹配的结合。
        
        上面商品prods与分类cates两类,
        (a)有商品没有分类,因此为左表为商品

        var query1 = from p in prods
                     join c in cates on p.CategoryId equals c.CategoryId into pc
                     from pc2 in pc.DefaultIfEmpty()//a
                     select new { p.ProductName, CategoryName = pc2 == null ? "" : pc2.CategoryName };//b
        var query2 = prods.GroupJoin(cates, p => p.CategoryId, c => c.CategoryId, (p, cs) => new { p, cs }).SelectMany(pc => pc.cs.DefaultIfEmpty(), (pc, pc2) => new { pc.p.ProductName, CategoryName = pc2 == null ? "" : pc2.CategoryName });
        foreach (var item in query2)
            Console.WriteLine(item.ProductName + "----" + item.CategoryName);     

   
        上面b处不能直接写pc2.CategoryName,因为pc2可能为null,所以要先判断一下再取成员。
        
        还有另一种写法,直接在a的方法中,对空元素进行创建对象,甚至进一步赋新值(例中为“无”)。

        var query1 = from p in prods
                     join c in cates on p.CategoryId equals c.CategoryId into pc
                     from pc2 in pc.DefaultIfEmpty(new Category() { CategoryName = "无" })//a
                     select new { p.ProductName, pc2.CategoryName };//b
        var query2 = prods.GroupJoin(cates, p => p.CategoryId, c => c.CategoryId, (p, cs) => new { p, cs }).SelectMany(pc => pc.cs.DefaultIfEmpty(new Category() { CategoryName = "无" }), (pc, pc2) => new { pc.p.ProductName, pc2.CategoryName });        


    
        
        (b)有分类没有商品,因此为左表为分类

        var query3 = from c in cates
                     join p in prods on c.CategoryId equals p.CategoryId into cp
                     from cp2 in cp.DefaultIfEmpty(new Product() { ProductName = "无" })
                     select new { c.CategoryName, cp2.ProductName };
        var query4 = cates.GroupJoin(prods, c => c.CategoryId, p => p.CategoryId, (c, ps) => new { c, ps }).SelectMany(cp => cp.ps.DefaultIfEmpty(new Product() { ProductName = "无" }), (cp, cp2) => new { cp.c.CategoryName, cp2.ProductName });
        foreach (var item in query4)
            Console.WriteLine(item.CategoryName + "----" + item.ProductName);


        
        注意:
        (a)GroupJoin四个参数,第一参数prods是右表,第二参数c.CategoryId是左表需要匹配的值(成员),第三参数是p.CategoryId是右表提取的需要匹配的值(成员),第四参数(c, ps)是参数对,前者c是左表的元素,后者是右表匹配上左表的所有元素的子序列ps。
        (b)selectmany第一个参数是需要扁平化的子序列(在元素的某一成员cp.ps),第二个参数是一个参数对(cp, cp2) ,前者cp为数据源的元素,后者cp2为前面子序列cp.ps中的元素。
        
    
    10、Distinct 去重
    
        Distinct操作用于返回一个去重后的集合,即去除集合中的重复元素。
        
        Distinct操作使用默认的相等比较器来判断元素是否相等。
        如果需要自定义比较逻辑,可以使用重载的Distinct方法,该方法接受一个实现了IEqualityComparer<T>接口的比较器作为参数。

        var numbers = new List<int> { 1, 2, 2, 3, 3, 4, 5, 5 };
        var query1 = numbers.Distinct();
        foreach (var item in query1)
            Console.Write(item + ",");//1,2,3,4,5,      

 
        注意:
            (1)Distinct操作默认使用元素的默认相等比较器进行比较,因此对于引用类型的元素,要注意是否需要重写Equals和GetHashCode方法,以确保正确的去重。
            (2)如果需要自定义比较逻辑,要使用重载的Distinct方法,并提供一个实现了IEqualityComparer<T>接口的比较器。
            (3)Distinct操作返回的是一个新的集合,原始集合不会受到影响。如果需要在原始集合上进行去重操作,可以使用List<T>的RemoveAll方法。
            numbers.RemoveAll(x => numbers.Count(n => x == n) > 1);//1,4
            这样原来的numbers就只有1和4了。
        
        
        IEqualityComparer<T>介绍
            IEqualityComparer<T>接口是一个用于比较两个对象是否相等的接口。它定义了两个方法:

            (1)bool Equals(T x, T y):
            用于判断两个对象x和y是否相等。返回true表示相等,返回false表示不相等。
            (2)int GetHashCode(T obj):
            用于获取对象的哈希码。哈希码是一个整数,用于快速比较对象是否相等。
            如果两个对象相等,它们的哈希码应该相同,但是两个哈希码相同的对象不一定相等。
        
        IEqualityComparer<T>接口通常用于需要自定义比较逻辑的场景,例如在LINQ的Distinct、GroupBy等操作中,可以通过传入实现了IEqualityComparer<T>接口的比较器来指定自定义的比较规则。        实现了IEqualityComparer<T>接口的例子:

        public class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }

        public class PersonEqualityComparer : IEqualityComparer<Person>
        {
            public bool Equals(Person x, Person y)
            {
                if (x == null && y == null) return true;
                if (x == null || y == null) return false;
                return x.Name == y.Name && x.Age == y.Age;
            }

            public int GetHashCode(Person obj)
            {
                return obj.Name.GetHashCode() ^ obj.Age.GetHashCode();//a
            }
        }


        上面Person类实现了IEqualityComparer<Person>接口。在Equals方法中,通过比较Person对象的Name和Age属性来判断两个对象是否相等。在GetHashCode方法中,通过将Name和Age的哈希码进行异或运算来获取对象的哈希码。

        使用自定义的比较器,可以在LINQ的操作中指定该比较器来进行自定义的比较逻辑。例如:

        var people = new List<Person>
        {
            new Person { Name = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Alice", Age = 25 },
        };
        var distinctPeople = people.Distinct(new PersonEqualityComparer());
        foreach (var person in distinctPeople)
            Console.WriteLine($"{person.Name}, {person.Age}");


        
        问:上面a处有什么用?
        答:期望得到一个唯一性的对象(有别于其它对象,用HashCode来区分).
            
            在实现 GetHashCode 方法时,我们通常希望生成一个唯一的哈希码来表示对象。这样可以在哈希表等数据结构中快速查找对象。为了尽可能保证哈希码的唯一性,我们需要将对象的多个成员组合起来生成哈希码。

            a处使用了异或运算符 (^) 来将两个成员的哈希码进行组合。异或运算符可以将两个整数的二进制位进行异或操作,生成一个新的整数。这样可以确保当两个成员的哈希码不同时,它们的组合哈希码也会不同。

            通过将两个成员的哈希码进行异或运算,我们可以将它们的哈希码合并为一个整数,作为对象的最终哈希码。这样就可以保证在哈希表等数据结构中,相等的对象会有相同的哈希码,从而提高查找效率。

            注意,哈希码的唯一性并不能完全保证,因为在有限的整数范围内,总会存在哈希冲突的情况。因此,在实现 GetHashCode 方法时,我们应该尽量选择能够均匀分布的哈希码算法,以减少哈希冲突的概率。            
        
        
    11、Union 合并(去重)
        Concat 合并(含重)

        
        (1)Union是一个用于合并两个或多个集合的操作符。它返回一个包含所有唯一元素的新集合,它将两个原始集合的元素合并在一起,并去除了重复的元素。
        它是一种方便的合并和去重操作,可以简化代码并提高开发效率。

        Union操作符的使用方式是通过扩展方法调用或查询表达式来实现。

        var collection1 = new List<int> { 1, 2, 3 };
        var collection2 = new List<int> { 3, 4, 5 };
        var query1 = collection1.Union(collection2);
        var query2 = (from num in collection1
                      select num).Union(collection2);
        foreach (var item in query1)
            Console.Write(item + ",");//1,2,3,4,5,


        
        Union操作符还可以与自定义的比较器一起使用,以便在合并集合时进行自定义的元素比较。这在需要根据特定的属性或条件进行比较时非常有用。(使用上面10中比较器):

        var people1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Alice", Age = 25 },
        };
        var people2 = new List<Person>
        {
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Tom", Age = 25 },
        };
        var query1 = people1.Union(people2, new PersonEqualityComarer());
        foreach (var item in query1)
            Console.Write(item.Name + ",");//Alice,Bob,Tom,    

    
        
        
        (2)Concat是用于连接两个或多个集合的操作符。与Union操作符不同,Concat操作符会将两个集合的所有元素连接在一起,不会去重。
        
        对于引用类型的集合,Concat操作符只是简单地连接集合,而不会进行任何深层次的复制或克隆。
        
        在使用Concat操作符时,易错点可能出现在对操作数的顺序和使用上。    注意要始终正确指定要连接的集合的顺序,以获得期望的结果。此外,还要明确Concat操作符不会去重,以免产生意外的结果。

        var collection1 = new List<int> { 1, 2, 3 };
        var collection2 = new List<int> { 3, 4, 5 };
        var query1 = collection1.Concat(collection2); //1,2,3,3,4,5,
        var query2 = collection2.Concat(collection1);//3,4,5,1,2,3,
        var query3 = collection1.Union(collection2);//1,2,3,4,5,    

    
        
        
    12、Intersect 交集(重合部分)
        Except  差集

        
        (1)Intersect用于获取两个集合的交集(即共同的元素)。它返回一个新的集合,其中包含同时存在于两个源集合中的元素。
        
        注意:
            (a)集合类型:
            类型须一致否则异常。除了常用基本类型比较,还可以自定义类型用实现IEqualityComparer<T>的来比较。
            
            (b)去重:
            Intersect操作符会自动去重,确保结果集中不包含重复的元素。这意味着即使源集合中有重复的元素,结果集中也只会包含一个副本。

            (c)集合顺序:
            Intersect操作符不关心源集合的顺序,它只关心元素的相等性。因此,结果集中的元素顺序可能与源集合的顺序不同。        
        var nums1 = new List<int> { 1, 2, 3, 4, 5 };
        var nums2 = new List<int> { 3, 4, 5, 6, 7 };
        var query1 = nums1.Intersect(nums2);//3,4,5,
        同样的道理,第二参数可以指定一个自定义的比较器EqualityComparer(),进行比较。
        
        
        (2)Except用于获取两个集合的差集。它返回一个新的集合,其中包含只存在于第一个源集合中的元素,而不包含在第二个源集合中的元素。
        var nums1 = new List<int> { 1, 2, 3, 4, 5 };
        var nums2 = new List<int> { 3, 4, 5, 6, 7 };
        var query1 = nums1.Except(nums2);//1,2
        var query2 = nums2.Except(nums1);//6,7
        注意点与上面(1)的Intersect一样,只是差集有A无B的元素,所以前后顺序直接影响结果。
        
        
    13、OfType<T>() 转换(失败则忽略)
        Cast<T>()  转换(失败则异常)

        
        (1)OfType操作符:
        OfType操作符用于筛选出指定类型的元素,并返回一个新的集合。如果源集合中的元素无法转换为目标类型,OfType操作符会忽略这些元素。换句话说,OfType操作符只返回可以成功转换为目标类型的元素。
                
        (2)Cast操作符:
        Cast操作符用于将集合中的元素强制转换为指定类型,并返回一个新的集合。如果源集合中的元素无法转换为目标类型,Cast操作符会引发InvalidCastException异常。换句话说,Cast操作符要求源集合中的每个元素都可以成功转换为目标类型。
        var objs = new List<object> { 1, "hello", 2.5, true };
        var query1 = objs.OfType<int>();//1
        var query2 = objs.Cast<int>();//失败,异常        
        注意两者的区别:
            (a)都是对序列的每个元素进行转换。
            (b)逐个转换中OfType失败则忽略,而Cast失败则异常。
            (c)两者都成功的话,OfType与Cast看上去似乎对原序列进行过滤一样。
        一般常用OfType<T>().
        
        
    14、Skip, SkipWhile, Take, TakeWhile
        
        (1)Skip()  跳过。 从最前面开始,跳过,直接选择后面的所有元素。
            Take() 选取。 从最前面开始,选取,选择前面的个数。
        int[] nums = { 1, 2, 3, 4, 5 };
        var query1 = nums.Skip(2);//3,4,5  (跳过前面2个)
        var query2 = nums.Take(2);//1,2  (选取前面2个)        
            
        
        (2)SkipWhile() 满足条件跳过。从首部开始比较,不满足就直接终止,选择后面的所有元素。
            TakeWhile() 满足条件选取。从首部开始比较,满足就选取元素,不满足直接结束。
        int[] nums = { 8, 2, 4, 3, 7 };
        var query1 = nums.SkipWhile(x => x > 3);//2,4,3,7,
        var query2 = nums.TakeWhile(x => x > 3);//8,
        query1.ToList().ForEach(x => Console.WriteLine(x));        
        x>3跳过了8,2不满足,终止比较,直接选择剩下的所有元素(2,4,3,7).
        TakeWhile第一个8满足x>3,选取8,第二个元素2不满足x>3,终止比较,返回前面已经选择的8。
        
        
    15、ToLookup 分组
        
        ToLookup是LINQ中的一个扩展方法,它用于将一个序列分组并转换为一个ILookup<TKey, TElement>对象。
        ILookup<TKey, TElement>接口表示一个键到一个或多个值的映射,类似于字典(Dictionary<TKey, TValue>),但一个键可以对应多个值。

        public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);
        source是要分组的源序列,keySelector选择器(一个函数,用于从每个元素中提取一个键)。
        var stus = new[]
        {
            new { Name = "Alice", Grade = "A" },
            new { Name = "Bob", Grade = "B" },
            new { Name = "Charlie", Grade = "A" },
            new { Name = "David", Grade = "C" },
            new { Name = "Eve", Grade = "B" }
        };
        var query1 = stus.ToLookup(x => x.Grade);
        foreach (var group in query1)
        {
            Console.WriteLine($"Students with grade {group.Key}:");
            foreach (var stu in group)
                Console.WriteLine("\t" + stu.Name);
        }


        结果:
            Students with grade A:
                    Alice
                    Charlie
            Students with grade B:
                    Bob
                    Eve
            Students with grade C:
                    David            
        (1)ToLookup方法返回的是一个ILookup<TKey, TElement>对象,而不是一个集合类型。它类似于字典(Dictionary<TKey, TValue>),但不是字典。您不能直接使用索引器或Add方法来修改ILookup对象。
        由于ILookup<TKey, TElement>接口继承自IEnumerable<IGrouping<TKey, TElement>>接口,所以可以将ILookup<TKey, TElement>对象视为一个可枚举的序列,每个元素都是一个IGrouping<TKey, TElement>对象,表示一个分组。通过遍历ILookup<TKey, TElement>对象,您可以访问分组键和分组中的元素。
        
        (2)ILookup对象是只读的,即不能添加、删除或修改其中的键值对。它只能用于查找和遍历分组后的元素。
        
        (3)ToLookup方法返回的ILookup对象是延迟执行的,即在遍历时才会计算分组。这意味着它不会立即执行查询,而是在需要时按需计算。
        
        
        问:Group by与ToLookup的区别是什么?
        答:ToLookup方法返回一个只读的"键值对"映射,适用于需要频繁查找和遍历分组的场景。
            而group by语句返回一个只读的分组结果,适用于需要对分组进行进一步操作或转换的场景。
        
        (a)返回类型:
        ToLookup方法返回一个ILookup<TKey, TElement>对象,它表示一个键到一个或多个值的映射。ILookup类似于字典(Dictionary<TKey, TValue>),但一个键可以对应多个值。
        group by语句返回一个IEnumerable<IGrouping<TKey, TElement>>对象,它表示一个键到一个或多个值的分组。IGrouping接口类似于一个只读的字典,它提供了访问分组键和分组中的元素的方法。
        简言之:返回类型相似,但名字不同。
            ILookup<TKey, TElement>对象是一个按键进行索引的数据结构,它在内部使用哈希表或其他数据结构来实现快速的键值查找。这使得在ILookup<TKey, TElement>对象中查找某个键对应的值的操作非常高效。
            因此,ILookup<TKey, TElement>对象适用于需要频繁查找某个键对应的值的场景,而IEnumerable<IGrouping<TKey, TElement>>对象适用于需要按顺序遍历分组的场景。
            
            例如,假设有一个学生列表,每个学生都有一个班级编号作为键,将学生按班级进行分组。
            需要频繁查询某个班的学生,可以使用ILookup对象,这样可以通过班级编号,快速查找对应的学生列表。
            
            例如,假设有一个订单列表,每个订单都有一个日期作为键,将订单按日期进行分组。
            需要遍历全部订单信息,可以使用IEnumerable<IGrouping<TKey, TElement>>对象,按日期顺序遍历每个分组。
            
            注意:两者分组的操作本质上是一样的,差异主要体现在对分组结果的使用场景上。
                如果需要频繁查询某个键对应的值,可以选择ILookup。如果需要按顺序遍历分组并获取键和元素集合的信息,可以选择Groupby.
        
        (b)可修改性:
        ToLookup返回的ILookup对象是只读的,即不能添加、删除或修改其中的键值对。它只能用于查找和遍历分组后的元素。
        group by语句返回的IGrouping对象也是只读的,不能修改。但是,您可以使用into关键字将分组结果转换为其他可修改的集合类型,如List或Dictionary。
        
        (c)延迟执行:
        ToLookup方法返回的ILookup对象是延迟执行的,即在遍历时才会计算分组。这意味着它不会立即执行查询,而是在需要时按需计算。
        group by语句也是延迟执行的,只有在遍历结果时才会进行分组计算。
        
        (d)语法和使用:
        ToLookup方法是LINQ的一个扩展方法,它可以在任何实现IEnumerable<T>接口的序列上使用。它需要一个键选择器函数来指定如何从元素中提取键。
        group by语句是C#语言的一部分,它可以在LINQ查询表达式或方法链中使用。它使用关键字group和by来指定分组的键,并可以使用into关键字将结果转换为其他集合类型。        
    
    
    16、ToArray(), ToList(),ToDictionary() 分别转为数组,列表、字典。
        
        (1)ToArray:
        ToArray方法将查询结果转换为数组类型。
        它接受一个参数,用于指定数组的初始大小。如果没有指定初始大小,则会使用查询结果的长度作为初始大小。

        int[] nums = { 1, 2, 3, 4, 5 };
        int[] arr1 = nums.Where(n => n > 2).ToArray();
        int[] arr2 = new int[10];
        Array.Copy(arr1, arr2, 1);//3,0,0,0,0,0,0,0,0,0,
        foreach (var item in arr2)
            Console.Write(item + ",");

        Array.Resize(ref arr1, 10);//扩容到10
        Console.WriteLine("\r\n" + arr1.Length);//10
        foreach (var item in arr1)
            Console.Write(item + ",");//3,4,5,0,0,0,0,0,0,0,


        一个数组如果要改变长度,可以用Array.Resize()或Array.Copy()产生一个新数组来改变。
        
        
        (2)ToList:
        ToList方法将查询结果转换为List类型。它没有任何参数。
        
        
        (3)ToDictionary:
        ToDictionary方法将查询结果转换为Dictionary类型。
        它接受两个参数,第一个参数用于指定"键选择器函数",第二个参数用于指定"值选择器函数"。

        string[] fruits = { "apple", "banana", "orange", "pear" };
        Dictionary<string, int> dics = fruits.ToDictionary(fruit => fruit, fruit => fruit.Length);
        foreach (var item in dics)
            Console.Write(item.Key + ":" + item.Value + ",");//apple:5,banana:6,orange:6,pear:4,        
        
       List<Person> people = new List<Person>
       {
           new Person { Id = 1, Name = "John" },
           new Person { Id = 2, Name = "Jane" },
           new Person { Id = 3, Name = "Bob" },
           new Person { Id = 3, Name = "Alice" }
       };

       try
       {
           Dictionary<int, string> dics = people.ToDictionary(p => p.Id, p => p.Name);
       }
       catch (ArgumentException ex)
       {
           Console.WriteLine(ex.Message);
       }


        第一个例子没有重复键。第二个例子因为有重复的键(Id=3),所以用try捕获异常。
        
        
        
    17、AsEnumerable 将数据源转换为IEnumerable<T>类型
        AsQueryable  将数据源转换为IQueryable<T>类型

        
        (1)如果数据源是一个实现了IEnumerable<T>接口的集合(如List、Array等),则AsEnumerable方法不会进行任何实际的转换,只是将数据源视为IEnumerable<T>类型。
        但是,如果数据源是其他类型(如DataTable、DbSet等),则AsEnumerable方法会将其转换为IEnumerable<T>类型,以便在LINQ查询中使用。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        IEnumerable<int> enumerable = numbers.AsEnumerable();
        var query = enumerable.Where(n => n > 2);
        foreach (var number in query)
            Console.WriteLine(number);


        numbers列表已经实现了IEnumerable<int>接口,所以调用AsEnumerable()方法没有实际的转换操作,是多余的操作。
        
        
        (2)无论数据源是什么类型,AsQueryable方法都会将其转换为IQueryable<T>类型,以便在LINQ查询中使用。
        IQueryable<T>接口提供了更丰富的查询功能,可以在远程数据源(如数据库)上执行查询。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        IQueryable<int> queryable = numbers.AsQueryable();
        var query = queryable.Where(n => n > 2);
        foreach (var number in query)
            Console.WriteLine(number);   

     
        AsQueryable方法通常用于将本地数据源转换为可查询的数据源。如果数据源已经是可查询的(如Entity Framework的DbSet),则不需要使用AsQueryable方法。
        
        注意:
        AsEnumerable方法将数据源转换为IEnumerable<T>类型,适用于本地数据源。
        AsQueryable方法将数据源转换为IQueryable<T>类型,适用于可查询的数据源,提供更丰富的查询功能。
        
        
    18、SequenceEqual 序列相等(元素与顺序两者同时相同)
    
        SequenceEqual方法用于比较两个序列的元素是否完全相同,包括元素的顺序和数量。
        如果两个序列中的元素完全相同,则返回true;否则,返回false。

        int[] num1 = { 1, 2, 3, 4, 5 };
        int[] num2 = { 5, 4, 3, 2, 1 };
        int[] num3 = { 1, 2, 3, 4, 5 };
        bool b1 = num1.SequenceEqual(num2);//false
        bool b2 = num1.SequenceEqual(num3);//true


        如果是非基础数据类型的,可以用SequenceEqul方法的第二个参数IEqualityComparer来指定相等方法。
        
        
    19、ElementAt, ElementAtOrDefault 由索引指定元素。
        
        (1)ElementAt方法用于获取序列中指定索引位置的元素。
        它接受一个整数参数,表示要获取的元素的索引位置。
        如果序列中存在指定索引位置的元素,则返回该元素;否则,抛出ArgumentOutOfRangeException异常。

        List<string> fruits = new List<string> { "apple", "banana", "orange", "grape" };
        string fruit = fruits.ElementAt(2);
        Console.WriteLine(fruit); // orange


        
    
        (2)ElementAtOrDefault方法与ElementAt方法类似,也用于获取序列中指定索引位置的元素。
        但是,如果序列中不存在指定索引位置的元素,ElementAtOrDefault方法不会抛出异常,而是返回元素类型的默认值。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int number = numbers.ElementAtOrDefault(6);
        Console.WriteLine(number); //  0


        
        
    20、First, FirstOrDefault 用于获取序列中满足指定条件的第一个元素。
    
        (1)First方法用于获取序列中满足指定条件的第一个元素。
        它接受一个Func委托参数,表示要应用于每个元素的条件。
        如果序列中存在满足条件的元素,则返回该元素;否则,抛出InvalidOperationException异常。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int n1 = numbers.First();//1
        int n2 = numbers.First(x => x % 2 == 0);//2  

     
        
        
        (2)FirstOrDefault方法与First方法类似,也用于获取序列中满足指定条件的第一个元素。
        但是,如果序列中不存在满足条件的元素,FirstOrDefault方法不会抛出异常,而是返回元素类型的默认值。

        List<int> numbers = new List<int> { 1, 3, 5 };
        int n1 = numbers.FirstOrDefault(x => x % 2 == 0);//0


        
        
    21、Last, LastOrDefault 用于获取序列中满足指定条件的最后一个元素。
        
        (1)Last方法用于获取序列中满足指定条件的最后一个元素。
        它接受一个Func委托参数,表示要应用于每个元素的条件。
        如果序列中存在满足条件的元素,则返回该元素;否则,抛出InvalidOperationException异常。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int n1 = numbers.Last();//5
        int n2 = numbers.Last(x => x % 2 == 0);//4


        
        
        (2)LastOrDefault方法与Last方法类似,也用于获取序列中满足指定条件的最后一个元素。
        但是,如果序列中不存在满足条件的元素,LastOrDefault方法不会抛出异常,而是返回元素类型的默认值。

        List<int> numbers = new List<int> { 1, 3, 5 };
        int n = numbers.LastOrDefault(x => x % 2 == 0);//0


        
        
    22、Single, SingleOrDefault 用于获取序列中满足指定条件的唯一一个元素。
        
        (1)Single方法用于获取序列中满足指定条件的唯一一个元素。
        它接受一个Func委托参数,表示要应用于每个元素的条件。
        如果序列中存在满足条件的唯一一个元素,则返回该元素;如果序列中不存在满足条件的元素,则抛出InvalidOperationException异常;如果序列中存在多个满足条件的元素,则抛出InvalidOperationException异常。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int n1 = numbers.Single(x => x == 4);//4
        int n2 = numbers.Single(x => x % 2 == 0);//不唯一,异常


        
        
        (2)SingleOrDefault方法与Single方法类似,也用于获取序列中满足指定条件的唯一一个元素。
        但是,如果序列中不存在满足条件的元素,SingleOrDefault方法不会抛出异常,而是返回元素类型的默认值;如果序列中存在多个满足条件的元素,SingleOrDefault方法也会抛出InvalidOperationException异常。

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int n1 = numbers.SingleOrDefault(x => x == 4);//4
        int n2 = numbers.SingleOrDefault(x => x == 6);//0
        int n3 = numbers.Single(x => x % 2 == 0);//不唯一,异常


        
        
        问:上面single或singleordefault在不唯一时都要异常,怎么弥补不唯一?
        答:不唯一,说明该元素是重复。一般用first或last,为了保险最好用firstOrDefault或LastOrDefault来确定这个元素。
        
        
        问:上面只能指定最先或最后一个元素,若要指定重复元素的第三个?
        答:下面用两种方法来指定。

        List<int> ns = new List<int> { 1, 6, 2, 3, 4, 6, 5, 6, 8, 6, 3, 6, 9, 6 };
        int n1 = ns.Where(x => x == 6).ElementAtOrDefault(2);//重复元素中,索引为2的元素
        int n2 = ns.Where(x => x == 6).Skip(2).Take(1).FirstOrDefault();//a


        注意:a处不写FirstOrDefault()将报错,尽管只有一个元素,但Take()返回的是IEnumerable<T>是一个序列类型,是不能赋值到int基础类型上的。
        
        
    23、DefaultlfEmpty, Empty 处理在序列中找不到满足条件的元素时的情况
    
        (1)DefaultIfEmpty方法:
            DefaultIfEmpty方法用于在序列为空时提供一个默认值或默认元素。
            如果序列为空,则返回一个包含一个默认值或默认元素的单元素序列;如果序列不为空,则返回原始序列。
            
            DefaultIfEmpty方法有一个可选参数defaultValue,用于指定默认值或默认元素。

            List<int> numbers = new List<int>();
            IEnumerable<int> result = numbers.DefaultIfEmpty(0);//a
            foreach (int num in result)
                Console.WriteLine(num); //b   0


        如果a处改0为7,则在b处会输出7。默认值是可以自定义指定。
        
        
        (2)Empty方法:
        Empty方法用于创建一个空的序列。
        它返回一个不包含任何元素的序列,可以用于表示没有满足条件的元素的情况。

        IEnumerable<int> numbers = Enumerable.Empty<int>();
        foreach (var item in numbers)
            Console.WriteLine(item);//因为为empty,没有内容输出


        
        
        问:Empty与null有什么区别?
        答:一个变量,当它创建时,就会在内存中分配一个地址(房间),如果有内容,就会给这个变量(房间地址)赋值。
            
            而Empty是在内存中已经分配了房间,只是房间内无值(无人住)。所以Empty实际上是有明确的类型,但没有值。
            null是内存中都还没有分配,更无法说是否有人住(是否有值)。所以null无法单独知晓类型(有些情况可以推断 出),更无值。
        
        
    24、Range, Repeat 两个常用的静态方法,用于创建序列。
        
        (1)Range方法用于创建一个整数序列,该序列包含一个指定范围内的连续整数。
        它接受两个参数:起始值和元素数量。
        var numbers = Enumerable.Range(1, 5);// 1, 2, 3, 4, 5
        
        (2)Repeat方法用于创建一个包含指定元素重复若干次的序列。
        它接受两个参数:要重复的元素和重复次数。
        var repeatedNumbers = Enumerable.Repeat(10, 3);// 10, 10, 10
        
        
        
    25、Count, LongCount 计算集合中元素数量的方法
        
        (1)Count方法用于计算一个集合中的元素数量,并返回一个int类型的值。

        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        int count = numbers.Count();//5


        
        (2)LongCount方法与Count方法类似,但它返回一个long类型的值,用于处理较大的集合。

        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        long longCount = numbers.LongCount();// 5


        提示:
            有时用Count出错,就是因为计数超过了int,这时用LongCount以便用long来计数。
        
        
    26、Aggregate, Average, Max, Min,Sum
        
        Aggregate 累积(不是累计加法,也不是累计乘积,而一个迭代函数)
        用于在集合中进行累积操作。它接受一个累积函数作为参数,该函数定义了如何将集合中的元素逐个累积到一个结果中。
        
        (聚合)用于描述将多个值或数据集合合并为一个单一的值或数据集合的过程。在数据分析和统计学中,聚合常用于计算总和、平均值、最大值、最小值等统计量。
        
        (1)一个参数时:

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int sum = numbers.Aggregate((acc, x) => acc + x);


        acc是一个临时暂存变量,里面的委托相当于 acc+=x,逐个枚举加到到acc中,最后返回acc的值到sum中。
        注意:acc并没有定义赋值,它会推荐使用int类型的缺少值0。
        
        (2)两个参数时:

        var ns = new List<int> { 1, 2, 3, 4, 5 };
        int sum = ns.Aggregate(10, (acc, num) => acc + num);// 25


        第一个参数10,就是指定累积结果(临时变量)的初值即acc的初值。上面累积函数是一个累加操作,所以最后结果是25.
        
        (3)三个参数:

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int sum = numbers.Aggregate(0, (acc, x) => acc + x, acc => acc * 2);
        Console.WriteLine(sum);  //30


        第三个参数是对最后累积的结果acc进行操作,上面是acc*2,所以是30.
        
        
        aggregate因为是累积操作,所以可以代表很多操作:
        
        (1)Sum(求和):

        List<int> ns = new List<int> { 1, 2, 3, 4, 5 };
        int sum = ns.Aggregate(0, (acc, x) => acc + x);
        Console.WriteLine(sum);  // 15

        (2)Average(平均值):

        List<int> ns = new List<int> { 1, 2, 3, 4, 5 };
        double average = ns.Aggregate(0, (acc, x) => acc + x) / (double)ns.Count;
        Console.WriteLine(average);  // 3

        (3)Max(最大值):

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        int max = numbers.Aggregate((acc, x) => acc > x ? acc : x);
        Console.WriteLine(max);  //5

        (4)Min(最小值):
 

        List<int> ns = new List<int> { 1, 2, 3, 4, 5 };
        int min = ns.Aggregate((acc, x) => acc < x ? acc : x);
        Console.WriteLine(min);  // 1


六、逆变与协变(复习) 


    1、协变->和谐的变化->正常地变化->子转父,儿子赡养父亲,孝道。儿子向父亲供养out
        逆变->逆天的变化->不正常的变化->父转子,父亲供养儿子,巨婴。父亲向儿子输血in
        

        internal abstract class Animal
        { }

        internal class Dog : Animal
        { }

        internal class Program
        {
            private static void Main(string[] args)
            {
                Dog dog = new Dog();
                Animal animal = dog;//a 子转父,协变

                //尽管Dog是Animal的子类,但是List<Dog>和List<Animal>是两种不同的类型。
                List<Dog> dogs= new List<Dog>();
                List<Animal> animals = dogs;//b 此处错误
                Console.ReadKey();
            }
        }    

    
        a处是协变,因为子类对象(Dog)可以隐式地转换为父类对象(Animal)。这是因为Dog是Animal的子类,所以可以将Dog对象赋值给Animal类型的变量。这种情况下,编译器会自动进行类型转换。

        b处不是协变,因为List<Dog>不能隐式地转换为List<Animal>。尽管Dog是Animal的子类,但是List<Dog>和List<Animal>是两种不同的类型。如果允许将List<Dog>赋值给List<Animal>,则可能会导致类型不安全的操作。如果允许将List<Dog>赋值给List<Animal>,则可能会导致类型不安全的操作。例如,如果我们将一个Cat对象添加到List<Animal>中,而List<Animal>实际上是List<Dog>,则会导致类型不匹配的错误。因此,编译器不允许这样的隐式转换。
        
        注意:
            协变和逆变并不涉及对错的概念,它们只是类型系统中的一种变化方式。
        
        上面可以通过linq中的select逐个映射转换:
        List<Dog> dogs= new List<Dog>();
        List<Animal> animals = dogs.Select(d=>(Animal)d).ToList();
        
        
        协变用out标记,表示结果输出。如<out T>,说明它是用来输出的,作为结果返回。
        逆变用in标记,表示参数输入。如<in T>,说明它是用来输入的,只能作为参数。
        
        out关键字用于协变,表示泛型类型参数只能在输出位置使用,即用作返回类型。它保证了子类型的兼容性,允许将派生类型赋值给基类型。
        in关键字用于逆变,表示泛型类型参数只能在输入位置使用,即用作方法的参数类型。它保证了父类型的兼容性,允许将基类型赋值给派生类型。
        IEnumerable<Dog> somedogs=new List<Dog>();
        IEnumerable<Animal> someanimals = somedogs;//因为out T,所以强制转换合法
        当我们鼠标指向IEnumerable时,或者对IEnumerable按F12查看源码时:
        public interface IEnumerable<out T> : IEnumerable
        这里的out T说明是可以协变的,编译器就是根据这个来认为它可以由IEnumerable<Dog>转为IEnumerable<Animal>,所以它是正确的,不需要额外的转换。
        
         再来看看in的情况:

        Action<Animal> actionAnimal = new Action<Animal>(a => Console.Write("动物叫"));//a
        Action<Dog> actionDog = actionAnimal;//b        actionDog.Invoke(dog);//c 


        (1)上面Action查看源码:public delegate void Action<in T>(T obj);有一个参数,且被说明是<in T>,说明这个参数只能作为输入;
        (2)因为是in,即定义为逆变,所以b处由父转为子,编译器将根据in认为这个转换是正确的、可行的。
        (3)c处调用了actionDog委托,并将dog对象作为参数传递给它。由于已经将actionAnimal委托赋值给了actionDog委托,所以在这里并不涉及逆变的转换。
        
        
        上面是应用层,下面看一下定义层:
        定义一个接口,并实现协变<out T>

        internal abstract class Animal
        { }

        internal class Dog : Animal
        { }

        internal interface IMyList<out T>//a
        {
            T GetElement();//b
        }

        internal class MyList<T> : IMyList<T>
        {
            public T GetElement()
            { return default(T); }
        }

        internal class Program
        {
            private static void Main(string[] args)
            {
                IMyList<Dog> listDogs = new MyList<Dog>();
                IMyList<Animal> listAnimal = listDogs;

                Console.ReadKey();
            }
        }


        接口a处用了<out T>实现了协变,所以主程序中IMyList<Dog>可以向IMyList<Animal>进行转换。
        
        注意:

            MyList<Dog> listDogs = new MyList<Dog>();
            MyList<Animal> listAnimal = listDogs;//出错      

 
            上面将出错。MyList<T>不支持协变或逆变,因为它没有使用out T或in T修饰符来声明类型参数的协变或逆变性。
            
            MyList<out T>使用了协变修饰符out来声明类型参数的协变性。这意味着MyList<Derived>类型的实例可以隐式转换为MyList<Base>类型的变量,其中Derived是Base的派生类。因此,MyList<out T>支持协变。

            相反,如果我们想要支持逆变,我们需要使用逆变修饰符in,例如MyList<in T>。这将允许MyList<Base>类型的实例赋值给MyList<Derived>类型的变量,其中Derived是Base的派生类。但是,在这个例子中,我们没有使用逆变修饰符,所以MyList<T>不支持逆变。
        
        
        如果接口定义如下:

        internal interface IMyList<out T>//a
        {
            T GetElement();//b
            void Change(T t);//c 出错
        }    

    
        上面c将出错,因为a处已经说明T是协变,只能是输出结果,而c处将T变成输入的参数,所以出错。若再改为下面:

        internal interface IMyList<in T>//a
        {
            T GetElement();//b 出错
            void Change(T t);//c
        }


        上面b处出错,因为a处说明<in T>是逆变,只能作为输入参数,而b处用成输出结果,所以出错。
        
        若只想逆变in,上面相关可修改为:

        internal interface IMyList<in T>//a
        {
            void Change(T t);//c
        }

        internal class MyList<T> : IMyList<T>
        {
            public void Change(T t)
            {
                Console.WriteLine("这是逆变,父转子,作输入参数t");
            }
        }        
        //主程序中:
        IMyList<Animal> listAnimal = new MyList<Animal>();
        IMyList<Dog> listDog = listAnimal;


七、综合练习


    1、查询运算符(Query Operators)
    
        过滤 Where, OfType
        投射 Select, SelectMany
        分段 Skip, SkipWhile, Take, TakeWhile
        
        排序 OrderBy, OrderByDescending,ThenBy,ThenByDescending,Reverse
        拼接 Concat
        联结 Join, GroupJoin
        
        分组 GroupBy, ToLookup
        设置 Distinct, Except, lntersect, Union
        转换 AsEnumerable,AsQueryable, Cast, ToArray, ToList,ToDictionary
        
        相等 SequenceEqual
        元素 ElementAt, ElementAtOrDefault, First, FirstOrDefault._ast, LastOrDefault, Single, SingleOrDefault
        生成 DefaultlfEmpty, Empty, Range, Repeat
        
        量词 All, Any, Contains
        聚合 Aggregate, Average, Count, LongCount, Max, Min,Sum    
    2、数据准备

        internal class Kongfu
        {
            public int KongfuId { get; internal set; }
            public string KongfuName { get; internal set; }
            public int Lethality { get; internal set; }
        }

        internal class MartialArtsMaster
        {
            public int Age { get; internal set; }
            public int Id { get; internal set; }
            public string Name { get; internal set; }
            public string Menpai { get; internal set; }
            public int Level { get; internal set; }
            public string Kungfu { get; internal set; }
        }
        //主程序中数据:
        var master = new List<MartialArtsMaster>//初始化武林高手
        {
            new MartialArtsMaster(){Id = 1,Name="黄蓉",Age = 18,Menpai = "丐帮",Kungfu = "打狗棒法",Level = 9 },
            new MartialArtsMaster(){Id = 2,Name="洪七公",Age = 70,Menpai="丐帮",Kungfu = "打狗棒法",Level = 10 },
            new MartialArtsMaster(){Id = 3,Name="郭靖",Age = 22,Menpai = "丐帮",Kungfu = "降龙十八掌",Level = 10 },
            new MartialArtsMaster(){Id = 4,Name="任我行",Age = 50,Menpai = "明教",Kungfu = "吸星大法", Level = 1},
            new MartialArtsMaster(){Id = 5,Name="东方不败",Age = 35,Menpai ="明教",Kungfu ="葵花宝典",Level = 10 },
            new MartialArtsMaster(){Id = 6,Name="林平之",Age = 23,Menpai ="华山",Kungfu ="葵花宝典",Level = 7 },
            new MartialArtsMaster(){Id = 7,Name="岳不群",Age = 50,Menpai ="华山",Kungfu ="葵花宝典",Level = 8}
        };
        var kongfu = new List<Kongfu>() //初始化武学
        {
            new Kongfu(){KongfuId = 1,KongfuName ="打狗棒法",Lethality = 90 },
            new Kongfu(){KongfuId = 2,KongfuName ="降龙十八掌",Lethality = 95},
            new Kongfu(){KongfuId = 3,KongfuName = "葵花宝典",Lethality = 100}
        };


    
    
    3、练习:
    
        (1)查询丐帮中武功等级高于8的大侠

        var query1 = from m in master
                     where m.Level > 8 && m.Menpai == "丐帮"
                     select m;
        var query2 = master.Where(m => m.Level > 8 && m.Menpai == "丐帮");
        foreach (var item in query1)
            Console.WriteLine(item.Name + "----" + item.Kungfu);     

   
    
    
        (2)杀伤力>90的武功大侠信息

        var query1 = from m in master
                     join k in kongfu on m.Kungfu equals k.KongfuName
                     where k.Lethality > 90
                     select new { m.Name, m.Kungfu, k.Lethality };//a

        var query2 = from m in master
                     from k in kongfu
                     where k.Lethality > 90 && m.Kungfu == k.KongfuName
                     select new { m.Name, m.Kungfu, k.Lethality };//b

        var query3 = master.Join(kongfu, m => m.Kungfu, k => k.KongfuName, (x, y) => new { x.Name, x.Kungfu, y.Lethality }).Where(z => z.Lethality > 90);//c

        foreach (var item in query3)
            Console.WriteLine(item.Name + "----" + item.Kungfu);   

 
        a处是推荐写法,它是内联产生后过滤。不推荐b处写法,它是叉积产生,效率低下。
        c处虽然正确且不影响效率,但影响阅读,应把x与y与前面的m与k对应写上:
        var query3 = master.Join(kongfu, m => m.Kungfu, k => k.KongfuName, (m, k) => new { m.Name, m.Kungfu, k.Lethality }).Where(z => z.Lethality > 90);//c
    
    
        (3)按武功(等级X杀伤力)降序、年龄与姓名升序,对高手进行排序。

        var query1 = from m in master
                     join k in kongfu on m.Kungfu equals k.KongfuName
                     orderby m.Level * k.Lethality descending, m.Age, m.Name
                     select new { m.Id, m.Age, m.Name, m.Kungfu };//a
        var query3 = master.Join(kongfu, m => m.Kungfu, k => k.KongfuName, (m, k) => new { m.Id, m.Age, m.Name, m.Kungfu, m.Level, k.Lethality }).OrderByDescending(o => o.Level * o.Lethality).ThenBy(o => o.Age).ThenBy(o => o.Name);    
        上面a处在显示时无法区别武功,能不能在orderby加一个武功字段呢?
        可以!用let
        var query1 = from m in master
                     join k in kongfu on m.Kungfu equals k.KongfuName
                     let damage = m.Level * k.Lethality
                     orderby damage descending, m.Age, m.Name
                     select new { m.Id, Damage = damage, m.Age, m.Name, m.Kungfu };//a
        foreach (var item in query1)
            Console.WriteLine(item.Id + "," + item.Damage + "," + item.Age + "," + item.Name + "," + item.Kungfu);


        结果:
            5,1000,35,东方不败,葵花宝典
            3,950,22,郭靖,降龙十八掌
            2,900,70,洪七公,打狗棒法
            1,810,18,黄蓉,打狗棒法
            7,800,50,岳不群,葵花宝典
            6,700,23,林平之,葵花宝典        
        上面只有6个人,因为吸星大法没法计算武功(伤害力)
    
        (4)选用高手等级8级以上、等级降序,且伤害90以上、伤害降序的情况下,按武功(等级X杀伤力)降序,对高手进行排序。

        int i = 1;
        int j = 1;
        var query1 = from m in master
                     where m.Level > 8
                     orderby m.Level descending
                     join k in
                        (from k1 in kongfu
                         where k1.Lethality > 90
                         orderby k1.Lethality descending
                         select k1)
                            on m.Kungfu equals k.KongfuName
                     let damage = m.Level * k.Lethality
                     orderby damage descending
                     select new { m.Id, m.Name, m.Kungfu, Damage = damage, Idx = i++ };
        var query2 = master.Where(m => m.Level > 8).OrderByDescending(o => o.Level).Join(kongfu.Where(k => k.Lethality > 90).OrderByDescending(o => o.Lethality), m => m.Kungfu, k => k.KongfuName, (m, k) => { var damage = m.Level * k.Lethality; return new { m.Id, m.Name, m.Kungfu, Damage = damage, Idx = j++ }; }).OrderByDescending(o => o.Damage);
        foreach (var item in query2)
            Console.WriteLine(item.Id + "," + item.Damage + "," + item.Name + "," + item.Kungfu);   

 
    
    
        (5)按武功分类计数降序排列,将各武功的名称伤害及个数列出

        var master = new List<MartialArtsMaster>//初始化武林高手
        {
            new MartialArtsMaster(){Id = 1,Name="黄蓉",Age = 18,Menpai = "丐帮",Kungfu = "打狗棒法",Level = 9 },
            new MartialArtsMaster(){Id = 2,Name="洪七公",Age = 70,Menpai="丐帮",Kungfu = "打狗棒法",Level = 10 },
            new MartialArtsMaster(){Id = 3,Name="郭靖",Age = 22,Menpai = "丐帮",Kungfu = "降龙十八掌",Level = 10 },
            new MartialArtsMaster(){Id = 4,Name="任我行",Age = 50,Menpai = "明教",Kungfu = "吸星大法", Level = 1},
            new MartialArtsMaster(){Id = 5,Name="东方不败",Age = 35,Menpai ="明教",Kungfu ="葵花宝典",Level = 10 },
            new MartialArtsMaster(){Id = 6,Name="林平之",Age = 23,Menpai ="华山",Kungfu ="葵花宝典",Level = 7 },
            new MartialArtsMaster(){Id = 7,Name="岳不群",Age = 50,Menpai ="华山",Kungfu ="葵花宝典",Level = 8},
            new MartialArtsMaster(){Id = 8,Name="令狐冲",Age = 23,Menpai ="华山",Kungfu ="独孤九剑",Level = 10},
            new MartialArtsMaster(){Id =9,Name="梅超风",Age = 23,Menpai ="桃花岛",Kungfu ="九阴真经",Level = 8},
            new MartialArtsMaster(){Id =10,Name="黄药师",Age = 23,Menpai ="桃花岛",Kungfu ="弹指神通",Level =10},
            new MartialArtsMaster(){Id =11,Name="风清杨",Age = 23,Menpai ="华山",Kungfu ="独孤九剑",Level =10}
        };
        var kongfu = new List<Kongfu>() //初始化武学
        {
            new Kongfu(){KongfuId = 1,KongfuName ="打狗棒法",Lethality = 90 },
            new Kongfu(){KongfuId = 2,KongfuName ="降龙十八掌",Lethality = 95},
            new Kongfu(){KongfuId = 3,KongfuName = "葵花宝典",Lethality = 100},
            new Kongfu(){KongfuId = 4,KongfuName = "独孤九剑",Lethality = 100},
            new Kongfu(){KongfuId = 5,KongfuName = "九阴真经",Lethality = 100},
            new Kongfu(){KongfuId = 6,KongfuName = "弹指神通",Lethality = 100}
        };
        var query1 = from k in kongfu
                     join m in master on k.KongfuName equals m.Kungfu into km
                     orderby km.Count() descending
                     select new { k.KongfuId, k.KongfuName, k.Lethality, Count = km.Count() };
        var query2 = kongfu.GroupJoin(master, k => k.KongfuName, m => m.Kungfu, (k, ms) => new { k, ms }).OrderByDescending(o => o.ms.Count()).Select(s => new { s.k.KongfuId, s.k.KongfuName, s.k.Lethality, Count = s.ms.Count() });
        foreach (var item in query2)
            Console.WriteLine(item.KongfuId + "," + item.KongfuName + "," + item.Lethality + "," + item.Count);    


    
        
        (6)按门派分类统计各有多少人?

        var query1 = from m in master
                     group m by m.Menpai into g
                     orderby g.Key
                     select new { g.Key, Count = g.Count() };
        var query2 = master.GroupBy(g => g.Menpai).OrderBy(o => o.Key).Select(s => new { s.Key, Count = s.Count() });
        foreach (var item in query2)
            Console.WriteLine(item.Key + "," + item.Count);    


    
    
        (7)对高手分页,每页最多3人,显示各页

        //分页
        int pageSize = 3;//每页大小为3个元素.
        int pageNum = (int)Math.Ceiling(master.Count / (double)pageSize);//共有几页
        var query1 = from m in master
                     join k in kongfu on m.Kungfu equals k.KongfuName
                     select new { m.Id, m.Name, m.Menpai };
        for (int i = 0; i < pageNum; i++)
        {
            var query2 = query1.Skip(i * pageSize).Take(pageSize);//a
            foreach (var item in query2)
                Console.WriteLine(item.Id + "," + item.Name + "," + item.Menpai);
            Console.WriteLine();
        }    


        a处,对源数据按每次跳过数量(前面总页数的元素),选取剩下中前3个,即新的一页。
    
 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值