c#基础学习文档2-linq

73 阅读42分钟

6、lambda表达式

6.1、匿名类型

匿名类型定义

本质上来说,匿名类型其实也是一个引用类型,通常用 var 来接这个引用,在匿名类型中可以定义一个或者多个属性,不过这些属性在匿名类型中会被自动做成 只读属性,和传统的的 class 相比,匿名类型中不能定义 字段,方法,只能包含只读属性。

因为C#的要求是万物皆对象,对象皆有类,所以每一个对象或者数据都是有类型在背后支撑的。但是有时候会需要一些一次性的只读类型,这时候声明一个完整的类就有点得不偿失了。什么是一次性的只读类型呢,就是我们只关心它有哪些属性,不关心它有什么方法,同时这个类对应的对象只能在初始化的时候给属性赋值其他时候不能重新赋值而且这个类型只在方法内部使用,在这个变量使用完成之后这个类型也失去了意义,这种类型就是我们所说的一次性的只读类型

使用 匿名类型

var author = new
{
  FirstName = "Joydip",
  LastName = "Kanjilal",
  Address = "Hyderabad, INDIA"
};

在上面的代码中,author 就是匿名类型的实例,这个匿名类型中包含三个属性: FirstName,LastName,Address,可以看出这些属性都是 string 类型,有一点挺特别的,在给属性赋值之前你不必给该属性定义类型。

下面的代码段展示了如何输出这个匿名类型的三个属性。

Console.WriteLine("Name: {0} {1}", author.FirstName, author.LastName);
Console.WriteLine("Address: {0}", author.Address);

image-20240412143755663

内嵌匿名类型

匿名类型除了平级定义,还可以嵌套定义,做法就是,在一个匿名类型的属性中再设置一个匿名类型,下面的代码展示了这种情况。

var author = new
{
  FirstName = "Joydip",
  LastName = "Kanjilal",
  Address = new { City = "Hyderabad", Country = "INDIA"}
};

接下来看一下如何去访问嵌套匿名类型中的属性。

Console.WriteLine("Name: {0} {1}", author.FirstName, author.LastName);
Console.WriteLine("Address: {0}", author.Address.City);

完整的代码如下,可供参考。

static void Main(string[] args)
{
  var author = new
  {
     FirstName = "Joydip",
     LastName = "Kanjilal",
     Address = new { City = "Hyderabad", Country = "INDIA"}
  };
  Console.WriteLine("Name: {0} {1}", author.FirstName, author.LastName);
  Console.WriteLine("Address: {0}", author.Address.City);
  Console.Read();
}

image-20240412143913236

6.2、lambda应用

6.2.1、定义

就是匿名函数,我们不声明方法名,只写一个方法体,这个方法体就是lambda表达式

6.2.2、函数式接口

首先,在写lambda表达式之前,需要先了解 两个特殊的类型:FuncAction

在lambda表达式中,当使用的是有返回值的方法体时,如果方法体是个简单的计算式或者说可以在一行内写完(或被编译器认为是一行)的话,可以省略 {}return,直接用 => 标记。

1)Action接口

定义

Action表示一个没有返回值的方法

public void Demo1()
{
    // 一个没有返回值,也没有参数的方法
}
Action act1 = Demo;// 直接给一个方法名
public void Demo2(string name)
{
    //有一个参数,但没有返回值的方法
}
Action<string> act2 = Demo2;

使用

Action act1 = ()=> // lambda 的标志性 声明方式 =>
{
    // 这是一个没有返回值,也没有参数的 lambda表达式
};
Action<int> act2 = (age) => 
{
    // 这是一个 参数为int ,没有返回值的 lambda表达式
};
2)Func接口

定义

其中Func表示一个有返回值的方法

public String Demo3()
{
    // 有返回值,但没有参数的方法
}
Func<string> func1 = Demo3;
​
public int Demo4(double data)
{
    // 返回值是int,有一个参数是double的方法
}
Func<double,int> func2 = Demo4;

使用

Func<string> func1 = () => ""; // 这是一个返回了空字符串的lambda表达式,注意这种写法
Func<string> func2 = () =>
{
    return ""; //与上一个等价
}
​
Func<int,string> func3 = (age) =>
{
    return "我的年纪是:"+age;// 一个参数是int,返回类型是string的lambda表达式
}

7、Linq详解

7.1、Linq基本概念

7.1.1、ling定义

Linq(Language Integrated Query),语言集成查询,是一种使用类似SQL语句操作多种数据源的功能。

在LINQ查询中,始终会用到对象。可以使用相同的基本编码模式来查询和转换xml文档、SQL数据库、.net数据集。

java和c#的矛盾:甲骨文却突然宣布JDK要收费,自从Java被这个冒充数据库公司的律师事务所收购之后,就觉得早晚会出事儿,前阵子跟谷歌打官司就觉得势头不对,果然没多久,又有新状况来了。

7.1.2、ling分类

linq查询包括两种方式,一是语句查询,二是方法查询。

7.2、表达式查询

1)from子句

用来标识查询的数据源。基本语法格式为:

from 迭代变量 in 数据源

首先,通过以下简单示例,给出from用法

static void Main(string[] args)
{
    var nums = new int[] {56,97,98,57,74,86,31,90};
        var queryInfo = from num in nums select num;//num为数据中的每个元素(相当于foreach中迭代变量)
        var numStr = string.Empty;
    foreach (var item in queryInfo) 
    {
        numStr += item + " ";
    }
    Console.WriteLine($"数据元素包括:{numStr}");
}

运行结果如下图1所示:

img

在一个查询表达式中,也可以使用多个from子句,示例如下:

static void Main(string[] args)
{
   var arraySet = new List<string>() {"my favorite hobby","is","painting and coding"};
    var queryInfo = from str in arraySet from item in str select item;//先从arraySet中获取每个分号内的字符串,再分别从字符串中获取每个字符
    var exeCount = 0;
    var res = string.Empty;
    foreach (var item in queryInfo)// 开始执行查询语句
    {
        res += item;
        exeCount++;
    }
    Console.WriteLine($"执行次数为:{exeCount}, 执行结果为:{res}");
}

运行结果如下图:

img

2)select子句

select子句,基于查询结果返回需要的值或字段,并能够对返回值指定类型。对任意想要获取到返回结果的linq语句,必须以select或group结束。示例如下:

// 基础类准备
public class User
{
    public string UserId { get; set; }
    public string UserName { get; set; }
        public string UserPhone { get; set; }
}
public class Order
{
    public string Id { get; set; }
    public string UserId { get; set; }
    public string OrderCode { get; set; }
}
​
​
 static void Main(string[] args)
 {
   var user1 = new User() {UserId = "1", UserName = "张三", UserPhone = "1234567997"};
   var user2 = new User() {UserId = "2", UserName = "李四", UserPhone = "18335789789"};
   var users = new List<User>() { user1, user2 };
   var order1 = new Order {Id = "1", UserId = "1", OrderCode = "orderCode001"};
   var order2 = new Order {Id = "2", UserId = "1", OrderCode = "orderCode002"};
   var orders = new List<Order>{order1, order2};
​
   List<string> queryInfo = (from user in users select user.UserName).ToList();
   foreach (var item in queryInfo)
   {
     Console.WriteLine(item);
   }
​
   var queryInfos = (from user in users select new {name = user.UserName, phone = user.UserPhone}).ToList();
   foreach (var item in queryInfos)
   {
     Console.WriteLine(item);
   }
 }

运行结果如下:

img

3)where子句

where子句用来指定筛选的条件,与sql或mysql查询语句中的where功能一样。通过where子句获取到满足条件的结果。示例如下:

static void Main(string[] args)
{
   var user1 = new User() {UserId = "1", UserName = "张三", UserPhone = "1234567997"};
   var user2 = new User() {UserId = "2", UserName = "李四", UserPhone = "18335789789"};
   var users = new List<User>() { user1, user2 };
​
   var whereQuery = from user in users where user.UserId == "1" select user;
   foreach (var item in whereQuery)
   {
     Console.WriteLine($"id =1 的用户姓名为:{item.UserName},手机号为:{item.UserPhone}");
   }
}

运行结果如下图4:

img

4)order by

orderby用来排序,与sql或mysql中orderby的功能相同,使得返回结果可以根据某字段或某种规则实现升序或降序排列。linq中语句默认展示为升序,降序使用【orderby 表达式 descending】.

static void Main(string[] args)
{
        var scores = new int[] { 56,97,98,57,74,86,31,90};
      //var orderQuery = from score in scores orderby score select score;//升序
        var orderQuery = from score in scores orderby score descending select score;//降序
    var sortScoreStr = string.Empty;
    foreach (var item in orderQuery)
    {
      sortScoreStr += item + " ";
    }
    Console.WriteLine($"排序后的数据元素为:{sortScoreStr}");
}

序列升序或降序排序后结果如下5和6:

img

图5

img

5) group by

group by子句用来对查询结果进行分组。如果结果分为两组,且未指定key的情况下,key取值默认是true和false。如果分为多组,获取数据结果时需要手动遍历key获取对应的value。示例如下:

static void Main(string[] args)
{
    var scores = new int[] { 56,97,98,57,74,86,31,90};
    var result = from score in scores group score by (score < 60);
    foreach (var item in result)
    {
        var str = $"{(item.Key ? "及格: " : "不及格: ")}";
        foreach (var it in item)
        {
            str += it + " ";
        }
        Console.WriteLine(str);
    }
}

运行结果为:

img

对于查询结果分为多组的情况,根据key获取对应value的示例如下:

static void Main(string[] args)
{
    var numbers = new int[]{1,2,3,4,5,6,7,8,9};
    var numQuery = from num in numbers group num by (num%3);
    foreach (var numKey in numQuery)
    {
      var str = string.Empty;
      switch (numKey.Key)
      {
        case 0: str = $"整除3余数为0的元素有:{str}"; break;
        case 1:str = $"整除3余数为1的元素有:{str}"; break;
        case 2:str = $"整除3余数为2的元素有:{str}"; break;
      }
​
      foreach (var item in numKey)
      {
        str += item + " ";
      }
      Console.WriteLine(str);
    }
}

运行结果图:

img

6)join联合查询

join子句用于联合查询,一般会存在两个数据源,且两个数据源中有相同的字段可进行比较。使用格式为【数据源1 join 数据 in 数据源2 on key1 equals key2】

static void Main(string[] args)
{
    var user1 = new User() {UserId = "1", UserName = "张三", UserPhone = "1234567997"};
    var user2 = new User() {UserId = "2", UserName = "李四", UserPhone = "18335789789"};
    var users = new List<User>() { user1, user2};
    var order1 = new Order {Id = "1", UserId = "1", OrderCode = "orderCode001"};
    var order2 = new Order {Id = "2", UserId = "1", OrderCode = "orderCode002"};
    var orders = new List<Order>{order1, order2};
    var joinQuery = from user in users
            join order in orders on user.UserId equals order.UserId
        select new
        {
          userId = user.UserId,
          userName = user.UserName,
          userPhone = user.UserPhone,
          orderCode = order.OrderCode,
        };
    foreach (var item in joinQuery)
    {
      Console.WriteLine(item);
    }
}

运行结果如下图:

img

7.3、方法(流式)查询

详情见:C# 基础知识系列-7 Linq详解 - 掘金 (juejin.cn)

准备两个类

  1. Student/学生类:

    /// <summary>
    /// 学生
    /// </summary>
    public class Student
    {
       /// <summary>
       /// 学号
       /// </summary>
       public int StudentId { get; set; }
       /// <summary>
       /// 姓名
       /// </summary>
       public string Name { get; set; }
       /// <summary>
       /// 班级
       /// </summary>
       public string Class { get; set; }
       /// <summary>
       /// 年龄
       /// </summary>
       public int Age { get; set; }
    }
    
  2. Subject/科目类:

    /// <summary>
    /// 科目
    /// </summary>
    public class Subject
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 年级
        /// </summary>
        public string Grade { get; set; }
        /// <summary>
        /// 学号
        /// </summary>
        public int StudentId { get; set; }
        /// <summary>
        /// 成绩
        /// </summary>
        public int Score { get; set; }
    }
    

Subject 和Student通过学号字段一一关联,实际工作中数据表有可能会设计成这。

那么先虚拟两个数据源:IEnumerable<Student> studentsIEnumerable<Subject> subjects。先忽略这两个数据源的实际来源,因为在开发过程中数据来源有很多种情况,有数据库查询出来的结果、远程接口返回的结果、文件读取的结果等等。不过最后都会整理成IEnumerable<T>的子接口或实现类的对象。

1)Where 过滤

Where 过滤数据,查询出符合条件的结果

where的方法声明:

public IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate)

可以看出不会转换数据类型,通过给定的lambda表达式或者一个方法进行过滤,获取返回true的元素。

示例:

// 获取年纪大于10但不大于12的同学们
List<Student> results = students.Where(t=>t.Age >10 && t.Age<= 12).ToList();

注意在调用ToList之后数据才会实质上查询出来。

2)Group 分组

Group 分组,依照指定内容进行分组

Group的方法声明有很多种:

最常用的一种是:

public static IEnumerable<System.Linq.IGrouping<TKey,TSource>> GroupBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);// TKey:表示根据分组的值

示例:

//将学生按照班级进行分组
List<IGrouping<string,Student>> list = students.GroupBy(p => p.Class).ToList();

3)OrderBy 排序

OrderBy/OrderByDescending 进行排序,按条件升序/降序

它们是一对方法,一个是升序一个降序,其声明是一样的:

常用的是:

public static System.Linq.IOrderedEnumerable<TSource> OrderBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);

示例:

//按年龄的升序排列:
List<Student> results = students.OrderBy(p => p.Age).ToList();
//按年龄的降序排列:
List<Student> results = students.OrderByDescending(p => p.Age).ToList();

4)First/Last

获取满足条件数据源的第一个/最后一个

这组方法有两个常用的重载声明:

First:

// 直接获取第一个
public static TSource First<TSource> (this IEnumerable<TSource> source);
// 获取满足条件的第一个
public static TSource First<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

Last:

// 直接获取最后一个
public static TSource Last<TSource> (this IEnumerable<TSource> source);
// 获取最后一个满足条件的元素
public static TSource Last<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

Student student = students.First();// 等价于 students[0];
Student student = students.First(p=>p.Class == "一班");//获取数据源中第一个一班的同学
​
Student student = students.Last();//最后一个学生
Student student = students.Last(p=>p.Class == "三班");//获取数据源中最后一个三班的同学

注意:

  • 在某些数据源中使用Last会报错,因为对于一些管道类型的数据源或者说异步数据源,程序无法确认最后一个元素的位置,所以会报错。解决方案:先使用OrderBy对数据源进行一次排序,使结果与原有顺序相反,然后使用First获取
  • 当数据源为空,或者不存在满足条件的元素时,调用这组方法会报错。解决方案:调用FirstOrDefault/LastOrDefault,这两组方法在无法查询到结果时会返回一个默认值。

5)Any/All

是否存在/是否都满足

Any:是否存在元素满足条件,只要有一个满足返回true

有两个版本,不过意思可能不太一样:

public static bool Any<TSource> (this IEnumerable<TSource> source);//数据源中是否有数据
//================
//是否存在满足条件的数据
public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

All :是否都满足条件,全部满足条件返回true:

public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

// 是否有学生
bool isAny =  students.Any();
// 是否有五班的同学
bool isFive = students.Any(p=>p.Class == "五班");
// 是否所有学生的年纪都不小于9岁
bool isAll = students.All(p=>p.Age >= 9);

6)Skip略过

Skip 略过几个元素

Skip一共有三个衍生方法:

第一个:Skip 自己: 略过几个元素,返回剩下的元素内容

public static IEnumerable<TSource> Skip<TSource> (this IEnumerable<TSource> source, int count);

第二个:SkipLast,从尾巴开始略过几个元素,返回剩下的元素内容

public static IEnumerable<TSource> SkipLast<TSource> (this IEnumerable<TSource> source, int count);

第三个:SkipWhile,跳过满足条件的元素,返回剩下的元素

public static IEnumerable<TSource> SkipWhile<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

// 不保留前10个学生
List<Student> results = students.Skip(10).ToList();
// 不保留后10个学生
List<Student> results = students.SkipLast(10).ToList();
// 只要非一班的学生
List<Student> results = students.SkipWhere(p=>p.Class=="一班").ToList();
//上一行代码 等价于 = students.Where(p=>p.Class != "一班").ToList();

7)Take 选取

Take 选取几个元素

Take与Skip一样也有三个衍生方法,声明的参数类型也一样,这里就不对声明做介绍了,直接上示例。

//选取前10名同学
List<Student> results = students.Take(10).ToList();
// 选取最后10名同学
List<Student> results = students.TakeLast(10).ToList();
//选取 一班的学生
List<Student> results = students.TakeWhile(p=>p.Class=="一班").ToList();
// 上一行 等价于 = students.Where(p=>p.Class=="一班").ToList();

在使用Linq写分页的时候,就是联合使用Take和Skip这两个方法:

int pageSize = 10;//每页10条数据
int pageIndex = 1;//当前第一页
List<Student> results = students.Skip((pageIndex-1)*pageSize).Take(pageSize).ToList();

其中 pageIndex可以是任意大于0 的数字。Take和Skip比较有意思的地方就是,如果传入的数字比数据源的数据量大,根本不会爆粗,只会返回一个空数据源列表。

8)Select 选取

官方对于Select的解释是,将序列中的每个元素投影到新的表单里。我的理解就是,自己 定义一个数据源单个对象的转换器,然后按照自己的方式对数据进行处理,选择出一部分字段,转换一部分字段。

所以按我的理解,我没找到java8的同效果方法。(实际上java用的是map,所以没找到,:-D)

public static System.Collections.Generic.IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector);

示例:

// 选出班级和姓名
List<object> results = students.Select(p => new
{
    p.Class,
    p.Name
}).ToList();

9)Join 关联

按照一定的逻辑将两个数据源关联到一起,然后选择出需要的数据。

方法有这几个重载版本:

public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector);
​
//
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector, IEqualityComparer<TKey> comparer);

这个方法的参数比较多,我们大概介绍一下这个方法的所有参数:

类型参数

  • TOuter 第一个序列中的元素的类型。
  • TInner 第二个序列中的元素的类型。
  • TKey 选择器函数返回的键的类型。
  • TResult 结果元素的类型。

参数

  • outer IEnumerable 要联接的第一个序列。
  • inner IEnumerable 要与第一个序列联接的序列。
  • outerKeySelector Func<TOuter,TKey> 用于从第一个序列的每个元素提取联接键的函数。
  • innerKeySelector Func<TInner,TKey> 用于从第二个序列的每个元素提取联接键的函数。
  • resultSelector Func<TOuter,TInner,TResult> 用于从两个匹配元素创建结果元素的函数。
  • comparerIEqualityComparer 用于对键进行哈希处理和比较的 IEqualityComparer

示例:

假设前天语文老师组织了一场考试,因为是模拟正式考试,所以答题纸上学生都只写了学号,现在需要把考试成绩和学生们联系在一起

List<object> results = students.Join(subjects,
                                     p => p.StudentId, 
                                     s => s.StudentId, 
                                     (p, s) => new 
                                     {
                                         Student = p, 
                                         Subject = s
                                     }).ToList();
/**
返回一个学生和科目的匿名对象,不过被我用object接了,这里会有一个问题,如果有兴致可以提前了解一下C#的var关键字和匿名对象,这部分将会放在C#基础系列补全篇讲解
*/

10)GroupJoin

基于键值等同性将两个序列的元素进行关联,并对结果进行分组。以上是官方介绍,我在开发过程中并没有使用过这个方法,不过这个方法完全可以认为是Join和Group的组合体,即先进行了一次Join然后又对数据进行一次分组。

方法声明:

// 使用默认比较器
public static IEnumerable<TResult> GroupJoin<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,IEnumerable<TInner>,TResult> resultSelector);
//设置比较器
public static IEnumerable<TResult> GroupJoin<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,IEnumerable<TInner>,TResult> resultSelector, IEqualityComparer<TKey> comparer);

类型参数

  • TOuter 第一个序列中的元素的类型。
  • TInner 第二个序列中的元素的类型。
  • TKey 键选择器函数返回的键的类型。
  • TResult 结果元素的类型。

参数

  • outer IEnumerable 要联接的第一个序列。
  • inner IEnumerable 要与第一个序列联接的序列。
  • outerKeySelector Func<TOuter,TKey> 用于从第一个序列的每个元素提取联接键的函数。
  • innerKeySelector Func<TInner,TKey> 用于从第二个序列的每个元素提取联接键的函数。
  • resultSelector Func<TOuter,IEnumerable,TResult> 用于从第一个序列的元素和第二个序列的匹配元素集合中创建结果元素的函数。
  • comparer IEqualityComparer 用于对键进行哈希处理和比较的 IEqualityComparer

7.4、聚合方法查询

1)Max 最大

Max获取数据源中最大的一个,不过只能是数字类型的,其他类型因为不能直接比较大小所以可以有替代方法,就是先排序取第一个。

以下是Max方法的两个重载版本:

public static int Max (this IEnumerable<int> source);
public static int Max <TSource>(this IEnumerable<TSource> source,Func<TSource,int> selector);

示例:

//查询学生中最大的年纪是多少
int maxAge = students.Select(t=>t.Age).Max();

2)Min 最小

方法类似与Max,不过与之不同的是获取最小的一个,不能应用于非数字类型。

示例:

// 查询学生中最小的年纪是多少
int minAge = students.Select(t=> t.Age).Min();
//=======
int minAge = students.Min(p=>p.Age);

3)Average

与 Max/Min是一样类型的方法,依旧不能应用于非数字类型。

示例:

// 查询学生的评价年纪
int averageAge = students.Select(t=>t.Age).Average();
int averageAge = students.Average(p=>p.Age);

4)Sum 求和

对数据源进行求和或者对数据源的某个字段进行求和,还是不能对非数字类型进行求和

示例:

// 一个没有实际意义的求和,学生的年龄总和
int sumAge = students.Select(t=>t.Age).Sum();
int sumAge = students.Sum(p=>p.Age);

5)Count 个数

Count/LongCount 数量查询

这是一组行为一样的方法,就是对数据源进行计数,不同的是Count返回int,LongCount返回long

它们的声明有以下两种,这里选了Count的声明:

public static int Count<TSource> (this IEnumerable<TSource> source);
​
public static int Count<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

int count = students.Count();//返回一共有多少个学生
int count = students.Count(p=>p.Class=="一班");// 统计一班一共有多少学生

6)Contains

是否包含某个元素

判断数据源中是否包含某个元素,返回一个bool值,如果包含则返回true,如果不包含则返回false。该方法有两个重载版本,一个是使用默认的Equals方法,一个是指定一个相等性比较器实现类。

public static bool Contains<TSource> (this IEnumerable<TSource> source, TSource value);
​
//传入相等性比较器的
public static bool Contains<TSource> (this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer);

值得注意的是,这里的相等比较器是一个接口,也就是说需要使用类来实现这个方法。通常在实际开发过程中,我们会在TSource这个数据源所代表的类上增加 IEqualityCompare的实现。

示例1:

Student student1 = new Student();// 初始化一个学生类
Student student2 = students.First();// 从数据源中取一个
​
bool isContains = students.Contains(student1);// 返回 false,
bool isContains2 = students.Contains(student2);// 返回 true

说明: 类的默认相等比较是比较是否是同一个对象,即返回的对象引用地址

示例2:

创建一个相等性比较器,值得注意的是,相等性比较器有两个方法,一个Equals是比较元素是否相等,一个GetHashCode是返回元素的HashCode,这两个方法必须在判断元素是否相等上保持结果一致。

public class StudentEqualityCompare: IEqualityComparer<Student>
{
    public bool Equals(Student x, Student y)
    {
        // 省略逻辑
    }
​
    public int GetHashCode(Student obj)
    {
        //省略逻辑
    }
}

使用:

StudentEqualityCompare compare = new StudentEqualityCompare();
Student student = students.First();
bool isContains = students.Contains(student, compare);

7.5、其他操作方法

1)Union 合并

Union 合并另一个同类型的数据源,并去除掉重复的数据。

联合另一个数据源,意思就是把两个数据源合并到一个里面,去掉重复的元素,只保留不重复的元素,并返回这个结果集。

与Contains方法差不多,这个方法有两个重载的版本:

public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);
​
public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

先假设一个业务场景:

学校举办运动会,现在教务处收到了田径组 500米跑的报名名单和跳远的报名名单,需要看看一共有哪些学生报名了这两项赛事。

// 省略数据源,田径组的名单
IEnumerable<Student> students1 = new List<Student>();
//省略数据源来源,跳远组的名单
IEnumerable<Student> students2 = new List<Student>();
​
List<Student> all = students1.Union(student2).ToList();

这时候简单统计了一下所有人,但是后来教务处在核对的时候,发现有的人名重复了,需要判断是否是一个人,这时候就必须创建一个相等比较器了。

List<Student> all = students1.Union(student2,compare).ToList();
// 省略compare的实现,具体可参照Contains的比较器

2)Intersect

Intersect 获取两个集合中都存在的数据

获取同时存在于两个集合中的元素,与Union类似。

方法的声明如下:

public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);
​
public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

继续之前的业务场景,现在教务处需要知道有哪些同学同时报名了两个比赛

List<Student> students = students1.Intersect(students2).ToList();

3)Except

Except 获取只在第一个数据源中存在的数据

获取只存在于第一个集合的元素,从第一个集合中去除同时存在与第二个集合的元素,并返回。

方法的声明如下:

public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);
​
public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

继续业务描述,教务处要一份只报名了500米的学生名单:

List<Student> students = students1.Except(students2).ToList();

4)Reverse 翻转

数据源中的元素原本有一定的顺序,这个方法可以将数据源中的顺序翻转过来,原本是最后一个的变成了第一个,第一个变成了最后一个。

简单示例:

char[] apple = { 'a', 'p', 'p', 'l', 'e' };
char[] reversed = apple.Reverse().ToArray();

5)Distinct 去重

对数据源进行去重,然后返回去重之后的结果。同样,这个方法有两个重载版本,一个有比较器,一个没有比较器。

// 不用比较器的
public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source);
// 设置比较器
public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);

示例:

先描述一个可能会出现的场景,每个班级在各个赛事组提交报名信息的时候有点混乱,500米的负责老师把一个班的名单多录了一次,但是学生已经乱序了,现在需要把多录的去掉,也就是对数据进行去重。

List<Student> students = students1.Distinct();

6)case...when

select中使用case when
linq代码:
Products.Select(P => new
{
  ID = P.ProductID,
  Name = P.Name,
  Color = P.Color,
  Price = (P.Color == "Red" ? P.StandardCost : (P.Color == "Black" ? P.StandardCost + 10 : P.ListPrice))
});
​
sql原型:
SELECT ProductID, Name, Color,
    CASE
      WHEN Color = 'Red' THEN StandardCost
      WHEN Color = 'Black' THEN StandardCost + 10
      ELSE ListPrice   
    END Price
FROM SalesLT.Product
 
 
where中使用case when
linq代码:
Products
.Where(P => (P.Color == "Red" ? (P.StandardCost > 100) : (P.Color == "Black" ? P.ListPrice > 100 : P.ListPrice == P.StandardCost)))
.Select(P => new
{
  ID = P.ProductID,
  Name = P.Name,
  Color = P.Color,
  StandardCost = P.StandardCost,
  ListPrice = P.ListPrice
});
sql原型:
SELECT ProductID, Name, Color, StandardCost, ListPrice
FROM SalesLT.Product
WHERE (
  (CASE
    WHEN Color = 'Red' THEN
      (CASE
        WHEN StandardCost > 100 THEN 1
        WHEN NOT (StandardCost > 100) THEN 0
        ELSE NULL
       END)
    WHEN Color = 'Black' THEN
      (CASE
        WHEN ListPrice > 100 THEN 1
        WHEN NOT (ListPrice > 100) THEN 0
        ELSE NULL
       END)
    ELSE
      (CASE
        WHEN ListPrice = StandardCost THEN 1
        WHEN NOT (ListPrice = StandardCost) THEN 0
        ELSE NULL
       END)
   END)) = 1
 
 
 
group by中使用case when
linq代码:
Products.GroupBy(P => P.StandardCost > 1000? P.Color : P.SellStartDate.ToString())
 
sql原型:
-- Region Parameters
DECLARE @p0 Decimal(8,4) = 1000
-- EndRegion
SELECT [t1].[value] AS [Key]
FROM (
  SELECT
    (CASE
      WHEN [t0].[StandardCost] > @p0 THEN CONVERT(NVarChar(MAX),[t0].[Color])
      ELSE CONVERT(NVarChar(MAX),[t0].[SellStartDate])
     END) AS [value]
  FROM [SalesLT].[Product] AS [t0]
  ) AS [t1]
GROUP BY [t1].[value]
GO

8、LInq应用

8.1、Linq查询方式

Linq查询方式分为两种,一种就是方法链的形式,官方的称呼是流式查询;另一种是类似于SQL语句的查询方式,之前叫做类SQL查询方式,不过有的文档称之为查询表达式。

分为流式查询和查询表达式

8.2、查询案例

1)数据源准备

/// <summary>
/// 学生
/// </summary>
public class Student
{
    /// <summary>
    /// 学号
    /// </summary>
    public long StudentId { get; set; }
    /// <summary>
    /// 姓名
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 年龄
    /// </summary>
    public int Age { get; set; }
    /// <summary>
    /// 班级
    /// </summary>
    public string Class { get; set; }
}
/// <summary>
/// 科目
/// </summary>
public class Subject
{
    /// <summary>
    /// 
    /// </summary>
    public long SubjectId { get; set; }
    /// <summary>
    /// 名称
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 年级
    /// </summary>
    public string Grade { get; set; }
    /// <summary>
    /// 教师
    /// </summary>
    public string Teacher { get; set; }
}
​
/// <summary>
/// 考试
/// </summary>
public class Exam
{
    /// <summary>
    /// 考试编号
    /// </summary>
    public long ExamId { get; set; }
    /// <summary>
    /// 科目编号
    /// </summary>
    public long SubjectId { get; set; }
    /// <summary>
    /// 学生编号
    /// </summary>
    public long StudentId { get; set; }
    /// <summary>
    /// 分数
    /// </summary>
    public double Score { get; set; }
    /// <summary>
    /// 考试时间:年-月 如202004 表示2020年04月
    /// </summary>
    public int Time { get; set; }
}

数据源:

Student student1 = new Student(1001,"zhangsan",18,"c001");
Student student2 = new Student(1002,"lisi",23,"c002");
Student student3 = new Student(1003,"wangwu",25,"c003");
List<Student> students = new List<Student> { student1, student2, student3 };// 学生列表,忽略数据来源
// 循环list
foreach (Student student in students)
{
    Console.WriteLine(student.ToString());
}
​
Subject subject1 = new Subject(2001,"语文","一年","T1");
Subject subject2 = new Subject(2003,"数学","二年","T2");
Subject subject3 = new Subject(2002, "日语", "一年", "T3");
Subject subject4 = new Subject(2004, "语文", "三年", "T1");
List<Subject> subjects = new List<Subject> { subject1,subject2,subject3, subject4 };// 科目列表,忽略数据来源
​
​
Exam exam1 = new Exam(21001,2001,1001,76.45,202004);
Exam exam2 = new Exam(21002,2001,1002,38.34,202004);
Exam exam7 = new Exam(21007,2004,1002,36.34,202004);
Exam exam3 = new Exam(21003,2002,1002,65,202005);
Exam exam4 = new Exam(21004,2002,1003,45.98,202005);
Exam exam5 = new Exam(21005,2003,1003,23.6,202005);
Exam exam6 = new Exam(21006,2003,1002,87.45,202005);
List<Exam> exams = new List<Exam> { exam1,exam2, exam3 , exam4, exam5, exam6, exam7 };// 考试列表,忽略数据来源
​
Console.WriteLine(students.Count());// 3

2)简单查询

1.查询班级是三年一班的所有同学

// 1.查询班级是c001的所有同学名称
// 流式查询
List<Student> results = students.Where(t => t.Class == "c001").ToList();
results.ForEach(t => Console.WriteLine(t.Name + " " + t.Class));
// 查询表达式
var resultsSql = from s in students where s.Class == "c001" select new { s.Name, s.Class };
resultsSql.ToList().ForEach(t => Console.WriteLine(t.Name + " " + t.Class));
/*  结果
            zhangsan c001
            zhangsan c001
         */

这两种查询方式结构都是IEnumerable。

2.获取姓张的所有学生的信息

// 获取姓张的所有学生的信息
List<Student> results1 = students.Where(t => t.Name.StartsWith("zhang")).ToList();
results.ForEach(t => Console.WriteLine(t.StudentId + " " + t.Name + " " + t.Age + " " + t.Class));
// 流式查询
var resultsSql1 = from s in students where s.Name.StartsWith("zhang") select s;
resultsSql1.ToList().ForEach(t => Console.WriteLine(t.StudentId + " " + t.Name + " " + t.Age + " " + t.Class));
/*  结果
         *  1001 zhangsan 18 c001
            1001 zhangsan 18 c001
         */

3.按班级进行分组获取每个班的学生信息

// 按班级进行分组获取每个班的学生信息
List<IGrouping<string, Student>> resultsGroupList = students.GroupBy(s =>  s.Class).ToList();
// 遍历list
foreach (var item in resultsGroupList)
{
    // 便利每个分组
    foreach (var t in item)
    {
        // 打印学生信息
        Console.WriteLine(t);
    }
}
// 流式查询
var resultsSqlGroupList = from s in students group s by s.Class into g select g;
// 遍历list
foreach (var item in resultsSqlGroupList)
{
    // 便利每个分组
    foreach (var t in item)
    {
        // 打印学生信息
        Console.WriteLine(t);
    }
}
/*  结果
            Student
            Student
            Student
         */

需要注意的是,如果返回结果是一个分组的结果,那么就不用select了。

4.查询每个班的平均年龄

// 4.查询每个班的平均年龄
// 流式查询
var resultGroupByAvgList = students.GroupBy(t => t.Class)
    .Select(t => new { Class = t.Key, Avg = t.Average(p => p.Age) }).ToList();
// 遍历list
foreach (var resultGroupByAvg in resultGroupByAvgList)
{
    // 循环打印分组
    Console.WriteLine(resultGroupByAvg);
}
// 查询表达式
var resultSqlGroupByAvgList = from s in students group s by s.Class into g select new { 
    Class = g.Key, 
    Age = g.Average(p => p.Age) 
};
// 遍历list
foreach (var resultGroupByAvg in resultSqlGroupByAvgList)
{
    // 循环打印分组
    Console.WriteLine(resultGroupByAvg);
}
/*  结果
            { Class = c001, Avg = 18 }
            { Class = c002, Avg = 23 }
            { Class = c003, Avg = 25 }
            { Class = c001, Age = 18 }
            { Class = c002, Age = 23 }
            { Class = c003, Age = 25 }
         */

查询表达式中没有统计查询的相关关键字,只能通过方法来获取,同时查询表达式返回的是一个集合,所以没法直接通过查询表达式进行求和、求平均等。

5.对所有学生按照年龄大小从大到小进行排序

// 5.对所有学生按照年龄大小从大到小进行排序
// 流式查询
List<Student> resultsOrderByDescList = students.OrderByDescending(t => t.Age).ToList();
// 查询表达式
var resultsSqlOrderByDescList = from s in students orderby s.Age descending select s;
// 遍历表达式结果集
foreach (var student in resultsSqlOrderByDescList)
{
    Console.WriteLine(student.Age);
}
/*  结果
            25
            23
            18
         */

6.对所有学生按照年龄大小从小到大进行排序

// 流方式查询
List<Student> resultsOrderByList = students.OrderBy(t => t.Age).ToList();
// 查询表达式
​
IOrderedEnumerable<Student> resultsSqlOrderByList = from s in students orderby s.Age select s;
// 遍历表达式结果集
foreach (var student in resultsSqlOrderByList)
{
    Console.WriteLine(student.Age);
    
}
/*  结果
           18
            23
            25
         */

7.先按年龄排序再按姓名进行排序

// 流方式查询
List<Student> resultsOrderByThenByList = students.OrderBy(t => t.Age).ThenBy(t => t.Name).ToList();
// 查询表达式
IOrderedEnumerable<Student> resultsSqlOrderByThenByList = from s in students orderby s.Age, s.Name select s;
// 遍历表达式结果集
foreach (var student in resultsSqlOrderByList)
{
    Console.WriteLine(student.Name + " " + student.Age);
}
/*  结果
            zhangsan 18
            lisi 23
            wangwu 25
         */

3)复杂查询

1.查询三年级语文科目在202004月举行的考试的所有成绩

// 1.查询三年级语文科目在202004月举行的考试的所有成绩
List<double> resultsJoinWhereSelect = 
    subjects.Join(exams, s => s.SubjectId, e => e.SubjectId, (s, e) => new
                  {
                      s.Name,
                      s.Grade,
                      e.Score,
                      e.Time
                  }).Where(t => t.Name == "语文" && t.Grade == "三年" && t.Time == 202004).Select(t => t.Score).ToList();
// 打印结果
resultsJoinWhereSelect.ForEach(t => Console.WriteLine(t));
​
// 查询表达式
IEnumerable<double> resultsSqlJoinWhereSelect = from s in subjects
    join e in exams on s.SubjectId equals e.SubjectId
    where s.Name == "语文" && s.Grade == "三年" && e.Time == 202004
    select e.Score;
// 遍历循环打印分数
foreach (var score in resultsSqlJoinWhereSelect) {
    Console.WriteLine(score);
}
/*  结果
            36.34
            36.34
         */

2.按年级进行分组,查询各年级语文分数

// 2.按年级进行分组,查询各年级语文分数
// 流式查询
var resultsGroupJoinWhereSelect = subjects.Where(s => s.Name == "语文")
    .Join(exams, s => s.SubjectId, e => e.SubjectId, (s, e) => new
          {
              s.Name,
              s.Grade,
              e.Score,
              e.Time
          }).GroupBy(s => s.Grade);
foreach (var group in resultsGroupJoinWhereSelect) {
    // Console.WriteLine(group.Key);
    foreach (var t in group) {
        Console.WriteLine(t.Grade +" " + t.Score);
    }
}
// 查询表达式
var resultsSqlGroupJoinWhereSelect = from s in subjects
    join e in exams on s.SubjectId equals e.SubjectId
    where s.Name == "语文"
    group new { s.Grade,e.Score} by s.Grade into g
    select g;
// 遍历每个分组
foreach (var group in resultsSqlGroupJoinWhereSelect)
{
    // Console.WriteLine(group.Key);
    foreach (var t in group)
    {
        Console.WriteLine(t.Grade + " " + t.Score);
    }
}
/*  结果
            一年 76.45
            一年 38.34
            三年 36.34
            一年 76.45
            一年 38.34
            三年 36.34
         */

3.求各年级历次考试各科目分数的平均分和最高分以及最低分

// 3.求各年级历次考试各科目分数的平均分和最高分以及最低分
// 流式查询
var resultGroupJoinSelectGroups = subjects.Join(exams,s => s.SubjectId, e => e.SubjectId, (s,e) => new { 
    s.Name, s.Grade,e.Score, e.Time
}).GroupBy (s => s.Grade).Select(g => new { 
    group = g.Key,
    subjects = g.GroupBy(t => t.Name).Select(g => new { 
        subName = g.Key,
        avgScore = g.Average(score => score.Score),
        maxScore = g.Max(score => score.Score),
        minScore = g.Min(score => score.Score)
    })
});
// 遍历查询
foreach (var group in resultGroupJoinSelectGroups) {
    Console.WriteLine(group.group);
    foreach (var t in group.subjects) {
        Console.WriteLine(t.subName + " " + t.maxScore + " " + t.minScore + " " +t.avgScore);
    }
}
​
// 查询表达式
var resultSqlGroupJoinSelectGroups = from s in subjects
    join e in exams on s.SubjectId equals e.SubjectId
    let o = new { s.Grade, s.Name, e.Score }
group o.Score by new { o.Grade, o.Name } into o
    let p = new
{
    o.Key.Grade,
    Subject = new
    {
        o.Key.Name,
        Max = o.Max(),
        Min = o.Min(),
        Average = o.Average()
    }
}
group p.Subject by p.Grade into g
    select new
{
    Grade = g.Key,
    Subjects = g.AsEnumerable()
};
// 遍历查询
foreach (var group in resultSqlGroupJoinSelectGroups)
{
    Console.WriteLine(group.Grade);
    foreach (var t in group.Subjects)
    {
        Console.WriteLine(t.Name + " " + t.Max + " " + t.Min + " " + t.Average);
    }
}
/*  结果
            一年
            语文 76.45 38.34 57.395
            日语 65 45.98 55.489999999999995
            二年
            数学 87.45 23.6 55.525000000000006
            三年
            语文 36.34 36.34 36.34
            一年
            语文 76.45 38.34 57.395
            日语 65 45.98 55.489999999999995
            二年
            数学 87.45 23.6 55.525000000000006
            三年
            语文 36.34 36.34 36.34
         */

8.3、语句查询

8.3.1、from子句

用来标识查询的数据源。基本语法格式为:

from 迭代变量 in 数据源

首先,通过以下简单示例,给出from用法

static void Main(string[] args)
{
    var nums = new int[] {56,97,98,57,74,86,31,90};
        var queryInfo = from num in nums select num;//num为数据中的每个元素(相当于foreach中迭代变量)
        var numStr = string.Empty;
    foreach (var item in queryInfo) 
    {
        numStr += item + " ";
    }
    Console.WriteLine($"数据元素包括:{numStr}");
}

运行结果如下图1所示:

img

在一个查询表达式中,也可以使用多个from子句,示例如下:

static void Main(string[] args)
{
   var arraySet = new List<string>() {"my favorite hobby","is","painting and coding"};
    var queryInfo = from str in arraySet from item in str select item;//先从arraySet中获取每个分号内的字符串,再分别从字符串中获取每个字符
    var exeCount = 0;
    var res = string.Empty;
    foreach (var item in queryInfo)// 开始执行查询语句
    {
        res += item;
        exeCount++;
    }
    Console.WriteLine($"执行次数为:{exeCount}, 执行结果为:{res}");
}

运行结果如下图:

img

注: 查询表达式在循环访问查询变量时(如上述示例中foreach),才会执行。

8.3.2、select子句

select子句,基于查询结果返回需要的值或字段,并能够对返回值指定类型。对任意想要获取到返回结果的linq语句,必须以select或group结束。示例如下:

// 基础类准备
public class User
{
    public string UserId { get; set; }
    public string UserName { get; set; }
        public string UserPhone { get; set; }
}
public class Order
{
    public string Id { get; set; }
    public string UserId { get; set; }
    public string OrderCode { get; set; }
}
​
​
 static void Main(string[] args)
 {
   var user1 = new User() {UserId = "1", UserName = "张三", UserPhone = "1234567997"};
   var user2 = new User() {UserId = "2", UserName = "李四", UserPhone = "18335789789"};
   var users = new List<User>() { user1, user2 };
   var order1 = new Order {Id = "1", UserId = "1", OrderCode = "orderCode001"};
   var order2 = new Order {Id = "2", UserId = "1", OrderCode = "orderCode002"};
   var orders = new List<Order>{order1, order2};
​
   List<string> queryInfo = (from user in users select user.UserName).ToList();
   foreach (var item in queryInfo)
   {
     Console.WriteLine(item);
   }
​
   var queryInfos = (from user in users select new {name = user.UserName, phone = user.UserPhone}).ToList();
   foreach (var item in queryInfos)
   {
     Console.WriteLine(item);
   }
 }

运行结果如下:

img

8.3.3、where子句

where子句用来指定筛选的条件,与sql或mysql查询语句中的where功能一样。通过where子句获取到满足条件的结果。示例如下:

static void Main(string[] args)
{
   var user1 = new User() {UserId = "1", UserName = "张三", UserPhone = "1234567997"};
   var user2 = new User() {UserId = "2", UserName = "李四", UserPhone = "18335789789"};
   var users = new List<User>() { user1, user2 };
​
   var whereQuery = from user in users where user.UserId == "1" select user;
   foreach (var item in whereQuery)
   {
     Console.WriteLine($"id =1 的用户姓名为:{item.UserName},手机号为:{item.UserPhone}");
   }
}

运行结果如下图4:

img

8.3.4、order by

orderby用来排序,与sql或mysql中orderby的功能相同,使得返回结果可以根据某字段或某种规则实现升序或降序排列。linq中语句默认展示为升序,降序使用【orderby 表达式 descending】.

static void Main(string[] args)
{
        var scores = new int[] { 56,97,98,57,74,86,31,90};
      //var orderQuery = from score in scores orderby score select score;//升序
        var orderQuery = from score in scores orderby score descending select score;//降序
    var sortScoreStr = string.Empty;
    foreach (var item in orderQuery)
    {
      sortScoreStr += item + " ";
    }
    Console.WriteLine($"排序后的数据元素为:{sortScoreStr}");
}

序列升序或降序排序后结果如下5和6:

img

图5

img

8.3.5、 group by

group by子句用来对查询结果进行分组。如果结果分为两组,且未指定key的情况下,key取值默认是true和false。如果分为多组,获取数据结果时需要手动遍历key获取对应的value。示例如下:

static void Main(string[] args)
{
    var scores = new int[] { 56,97,98,57,74,86,31,90};
    var result = from score in scores group score by (score < 60);
    foreach (var item in result)
    {
        var str = $"{(item.Key ? "及格: " : "不及格: ")}";
        foreach (var it in item)
        {
            str += it + " ";
        }
        Console.WriteLine(str);
    }
}

运行结果为:

img

对于查询结果分为多组的情况,根据key获取对应value的示例如下:

static void Main(string[] args)
{
    var numbers = new int[]{1,2,3,4,5,6,7,8,9};
    var numQuery = from num in numbers group num by (num%3);
    foreach (var numKey in numQuery)
    {
      var str = string.Empty;
      switch (numKey.Key)
      {
        case 0: str = $"整除3余数为0的元素有:{str}"; break;
        case 1:str = $"整除3余数为1的元素有:{str}"; break;
        case 2:str = $"整除3余数为2的元素有:{str}"; break;
      }
​
      foreach (var item in numKey)
      {
        str += item + " ";
      }
      Console.WriteLine(str);
    }
}

运行结果图:

img

8.3.6、join联合查询

join子句用于联合查询,一般会存在两个数据源,且两个数据源中有相同的字段可进行比较。使用格式为【数据源1 join 数据 in 数据源2 on key1 equals key2】

static void Main(string[] args)
{
    var user1 = new User() {UserId = "1", UserName = "张三", UserPhone = "1234567997"};
    var user2 = new User() {UserId = "2", UserName = "李四", UserPhone = "18335789789"};
    var users = new List<User>() { user1, user2};
    var order1 = new Order {Id = "1", UserId = "1", OrderCode = "orderCode001"};
    var order2 = new Order {Id = "2", UserId = "1", OrderCode = "orderCode002"};
    var orders = new List<Order>{order1, order2};
    var joinQuery = from user in users
            join order in orders on user.UserId equals order.UserId
        select new
        {
          userId = user.UserId,
          userName = user.UserName,
          userPhone = user.UserPhone,
          orderCode = order.OrderCode,
        };
    foreach (var item in joinQuery)
    {
      Console.WriteLine(item);
    }
}

运行结果如下图:

img

8.4、linq方法查询

详情见:C# 基础知识系列-7 Linq详解 - 掘金 (juejin.cn)

准备两个类

  1. Student/学生类:

    /// <summary>
    /// 学生
    /// </summary>
    public class Student
    {
       /// <summary>
       /// 学号
       /// </summary>
       public int StudentId { get; set; }
       /// <summary>
       /// 姓名
       /// </summary>
       public string Name { get; set; }
       /// <summary>
       /// 班级
       /// </summary>
       public string Class { get; set; }
       /// <summary>
       /// 年龄
       /// </summary>
       public int Age { get; set; }
    }
    
  2. Subject/科目类:

    /// <summary>
    /// 科目
    /// </summary>
    public class Subject
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 年级
        /// </summary>
        public string Grade { get; set; }
        /// <summary>
        /// 学号
        /// </summary>
        public int StudentId { get; set; }
        /// <summary>
        /// 成绩
        /// </summary>
        public int Score { get; set; }
    }
    

Subject 和Student通过学号字段一一关联,实际工作中数据表有可能会设计成这。

那么先虚拟两个数据源:IEnumerable<Student> studentsIEnumerable<Subject> subjects。先忽略这两个数据源的实际来源,因为在开发过程中数据来源有很多种情况,有数据库查询出来的结果、远程接口返回的结果、文件读取的结果等等。不过最后都会整理成IEnumerable<T>的子接口或实现类的对象。

8.4.1、Where 过滤

Where 过滤数据,查询出符合条件的结果

where的方法声明:

public IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate)

可以看出不会转换数据类型,通过给定的lambda表达式或者一个方法进行过滤,获取返回true的元素。

示例:

// 获取年纪大于10但不大于12的同学们
List<Student> results = students.Where(t=>t.Age >10 && t.Age<= 12).ToList();

注意在调用ToList之后数据才会实质上查询出来。

8.4.2、Group 分组

Group 分组,依照指定内容进行分组

Group的方法声明有很多种:

最常用的一种是:

public static IEnumerable<System.Linq.IGrouping<TKey,TSource>> GroupBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);// TKey:表示根据分组的值

示例:

//将学生按照班级进行分组
List<IGrouping<string,Student>> list = students.GroupBy(p => p.Class).ToList();

8.4.3、OrderBy 排序

OrderBy/OrderByDescending 进行排序,按条件升序/降序

它们是一对方法,一个是升序一个降序,其声明是一样的:

常用的是:

public static System.Linq.IOrderedEnumerable<TSource> OrderBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);

示例:

//按年龄的升序排列:
List<Student> results = students.OrderBy(p => p.Age).ToList();
//按年龄的降序排列:
List<Student> results = students.OrderByDescending(p => p.Age).ToList();

8.4.4、First/Last

获取满足条件数据源的第一个/最后一个

这组方法有两个常用的重载声明:

First:

// 直接获取第一个
public static TSource First<TSource> (this IEnumerable<TSource> source);
// 获取满足条件的第一个
public static TSource First<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

Last:

// 直接获取最后一个
public static TSource Last<TSource> (this IEnumerable<TSource> source);
// 获取最后一个满足条件的元素
public static TSource Last<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

Student student = students.First();// 等价于 students[0];
Student student = students.First(p=>p.Class == "一班");//获取数据源中第一个一班的同学
​
Student student = students.Last();//最后一个学生
Student student = students.Last(p=>p.Class == "三班");//获取数据源中最后一个三班的同学

注意:

  • 在某些数据源中使用Last会报错,因为对于一些管道类型的数据源或者说异步数据源,程序无法确认最后一个元素的位置,所以会报错。解决方案:先使用OrderBy对数据源进行一次排序,使结果与原有顺序相反,然后使用First获取
  • 当数据源为空,或者不存在满足条件的元素时,调用这组方法会报错。解决方案:调用FirstOrDefault/LastOrDefault,这两组方法在无法查询到结果时会返回一个默认值。

8.4.5、Any/All

是否存在/是否都满足

Any:是否存在元素满足条件,只要有一个满足返回true

有两个版本,不过意思可能不太一样:

public static bool Any<TSource> (this IEnumerable<TSource> source);//数据源中是否有数据
//================
//是否存在满足条件的数据
public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

All :是否都满足条件,全部满足条件返回true:

public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

// 是否有学生
bool isAny =  students.Any();
// 是否有五班的同学
bool isFive = students.Any(p=>p.Class == "五班");
// 是否所有学生的年纪都不小于9岁
bool isAll = students.All(p=>p.Age >= 9);

8.4.6、Skip略过

Skip 略过几个元素

Skip一共有三个衍生方法:

第一个:Skip 自己: 略过几个元素,返回剩下的元素内容

public static IEnumerable<TSource> Skip<TSource> (this IEnumerable<TSource> source, int count);

第二个:SkipLast,从尾巴开始略过几个元素,返回剩下的元素内容

public static IEnumerable<TSource> SkipLast<TSource> (this IEnumerable<TSource> source, int count);

第三个:SkipWhile,跳过满足条件的元素,返回剩下的元素

public static IEnumerable<TSource> SkipWhile<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

// 不保留前10个学生
List<Student> results = students.Skip(10).ToList();
// 不保留后10个学生
List<Student> results = students.SkipLast(10).ToList();
// 只要非一班的学生
List<Student> results = students.SkipWhere(p=>p.Class=="一班").ToList();
//上一行代码 等价于 = students.Where(p=>p.Class != "一班").ToList();

8.4.7、Take 选取

Take 选取几个元素

Take与Skip一样也有三个衍生方法,声明的参数类型也一样,这里就不对声明做介绍了,直接上示例。

//选取前10名同学
List<Student> results = students.Take(10).ToList();
// 选取最后10名同学
List<Student> results = students.TakeLast(10).ToList();
//选取 一班的学生
List<Student> results = students.TakeWhile(p=>p.Class=="一班").ToList();
// 上一行 等价于 = students.Where(p=>p.Class=="一班").ToList();

在使用Linq写分页的时候,就是联合使用Take和Skip这两个方法:

int pageSize = 10;//每页10条数据
int pageIndex = 1;//当前第一页
List<Student> results = students.Skip((pageIndex-1)*pageSize).Take(pageSize).ToList();

其中 pageIndex可以是任意大于0 的数字。Take和Skip比较有意思的地方就是,如果传入的数字比数据源的数据量大,根本不会爆粗,只会返回一个空数据源列表。

8.4.8、Select 选取

官方对于Select的解释是,将序列中的每个元素投影到新的表单里。我的理解就是,自己 定义一个数据源单个对象的转换器,然后按照自己的方式对数据进行处理,选择出一部分字段,转换一部分字段。

所以按我的理解,我没找到java8的同效果方法。(实际上java用的是map,所以没找到,:-D)

public static System.Collections.Generic.IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector);

示例:

// 选出班级和姓名
List<object> results = students.Select(p => new
{
    p.Class,
    p.Name
}).ToList();

8.4.9、Join 关联

按照一定的逻辑将两个数据源关联到一起,然后选择出需要的数据。

方法有这几个重载版本:

public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector);
​
//
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector, IEqualityComparer<TKey> comparer);

这个方法的参数比较多,我们大概介绍一下这个方法的所有参数:

类型参数

  • TOuter 第一个序列中的元素的类型。
  • TInner 第二个序列中的元素的类型。
  • TKey 选择器函数返回的键的类型。
  • TResult 结果元素的类型。

参数

  • outer IEnumerable 要联接的第一个序列。
  • inner IEnumerable 要与第一个序列联接的序列。
  • outerKeySelector Func<TOuter,TKey> 用于从第一个序列的每个元素提取联接键的函数。
  • innerKeySelector Func<TInner,TKey> 用于从第二个序列的每个元素提取联接键的函数。
  • resultSelector Func<TOuter,TInner,TResult> 用于从两个匹配元素创建结果元素的函数。
  • comparerIEqualityComparer 用于对键进行哈希处理和比较的 IEqualityComparer

示例:

假设前天语文老师组织了一场考试,因为是模拟正式考试,所以答题纸上学生都只写了学号,现在需要把考试成绩和学生们联系在一起

c#复制代码List<object> results = students.Join(subjects,
                                     p => p.StudentId, 
                                     s => s.StudentId, 
                                     (p, s) => new 
                                     {
                                         Student = p, 
                                         Subject = s
                                     }).ToList();
/**
返回一个学生和科目的匿名对象,不过被我用object接了,这里会有一个问题,如果有兴致可以提前了解一下C#的var关键字和匿名对象,这部分将会放在C#基础系列补全篇讲解
*/

8.4.10、GroupJoin

基于键值等同性将两个序列的元素进行关联,并对结果进行分组。以上是官方介绍,我在开发过程中并没有使用过这个方法,不过这个方法完全可以认为是Join和Group的组合体,即先进行了一次Join然后又对数据进行一次分组。

方法声明:

// 使用默认比较器
public static IEnumerable<TResult> GroupJoin<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,IEnumerable<TInner>,TResult> resultSelector);
//设置比较器
public static IEnumerable<TResult> GroupJoin<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,IEnumerable<TInner>,TResult> resultSelector, IEqualityComparer<TKey> comparer);

类型参数

  • TOuter 第一个序列中的元素的类型。
  • TInner 第二个序列中的元素的类型。
  • TKey 键选择器函数返回的键的类型。
  • TResult 结果元素的类型。

参数

  • outer IEnumerable 要联接的第一个序列。
  • inner IEnumerable 要与第一个序列联接的序列。
  • outerKeySelector Func<TOuter,TKey> 用于从第一个序列的每个元素提取联接键的函数。
  • innerKeySelector Func<TInner,TKey> 用于从第二个序列的每个元素提取联接键的函数。
  • resultSelector Func<TOuter,IEnumerable,TResult> 用于从第一个序列的元素和第二个序列的匹配元素集合中创建结果元素的函数。
  • comparer IEqualityComparer 用于对键进行哈希处理和比较的 IEqualityComparer

8.5、聚合方法查询

8.5.1、Max 最大

Max获取数据源中最大的一个,不过只能是数字类型的,其他类型因为不能直接比较大小所以可以有替代方法,就是先排序取第一个。

以下是Max方法的两个重载版本:

public static int Max (this IEnumerable<int> source);
public static int Max <TSource>(this IEnumerable<TSource> source,Func<TSource,int> selector);

示例:

//查询学生中最大的年纪是多少
int maxAge = students.Select(t=>t.Age).Max();

8.5.2、Min 最小

方法类似与Max,不过与之不同的是获取最小的一个,不能应用于非数字类型。

示例:

// 查询学生中最小的年纪是多少
int minAge = students.Select(t=> t.Age).Min();
//=======
int minAge = students.Min(p=>p.Age);

8.5.3、Average

与 Max/Min是一样类型的方法,依旧不能应用于非数字类型。

示例:

// 查询学生的评价年纪
int averageAge = students.Select(t=>t.Age).Average();
int averageAge = students.Average(p=>p.Age);

8.5.4、Sum 求和

对数据源进行求和或者对数据源的某个字段进行求和,还是不能对非数字类型进行求和

示例:

// 一个没有实际意义的求和,学生的年龄总和
int sumAge = students.Select(t=>t.Age).Sum();
int sumAge = students.Sum(p=>p.Age);

8.5.5、Count 个数

Count/LongCount 数量查询

这是一组行为一样的方法,就是对数据源进行计数,不同的是Count返回int,LongCount返回long

它们的声明有以下两种,这里选了Count的声明:

public static int Count<TSource> (this IEnumerable<TSource> source);
​
public static int Count<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

int count = students.Count();//返回一共有多少个学生
int count = students.Count(p=>p.Class=="一班");// 统计一班一共有多少学生

8.5.6、Contains

是否包含某个元素

判断数据源中是否包含某个元素,返回一个bool值,如果包含则返回true,如果不包含则返回false。该方法有两个重载版本,一个是使用默认的Equals方法,一个是指定一个相等性比较器实现类。

public static bool Contains<TSource> (this IEnumerable<TSource> source, TSource value);
​
//传入相等性比较器的
public static bool Contains<TSource> (this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer);

值得注意的是,这里的相等比较器是一个接口,也就是说需要使用类来实现这个方法。通常在实际开发过程中,我们会在TSource这个数据源所代表的类上增加 IEqualityCompare的实现。

示例1:

Student student1 = new Student();// 初始化一个学生类
Student student2 = students.First();// 从数据源中取一个
​
bool isContains = students.Contains(student1);// 返回 false,
bool isContains2 = students.Contains(student2);// 返回 true

说明: 类的默认相等比较是比较是否是同一个对象,即返回的对象引用地址

示例2:

创建一个相等性比较器,值得注意的是,相等性比较器有两个方法,一个Equals是比较元素是否相等,一个GetHashCode是返回元素的HashCode,这两个方法必须在判断元素是否相等上保持结果一致。

public class StudentEqualityCompare: IEqualityComparer<Student>
{
    public bool Equals(Student x, Student y)
    {
        // 省略逻辑
    }
​
    public int GetHashCode(Student obj)
    {
        //省略逻辑
    }
}

使用:

StudentEqualityCompare compare = new StudentEqualityCompare();
Student student = students.First();
bool isContains = students.Contains(student, compare);

8.9、其他操作方法

8.9.1、Union 合并

Union 合并另一个同类型的数据源,并去除掉重复的数据。

联合另一个数据源,意思就是把两个数据源合并到一个里面,去掉重复的元素,只保留不重复的元素,并返回这个结果集。

与Contains方法差不多,这个方法有两个重载的版本:

public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);
​
public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

先假设一个业务场景:

学校举办运动会,现在教务处收到了田径组 500米跑的报名名单和跳远的报名名单,需要看看一共有哪些学生报名了这两项赛事。

// 省略数据源,田径组的名单
IEnumerable<Student> students1 = new List<Student>();
//省略数据源来源,跳远组的名单
IEnumerable<Student> students2 = new List<Student>();
​
List<Student> all = students1.Union(student2).ToList();

这时候简单统计了一下所有人,但是后来教务处在核对的时候,发现有的人名重复了,需要判断是否是一个人,这时候就必须创建一个相等比较器了。

List<Student> all = students1.Union(student2,compare).ToList();
// 省略compare的实现,具体可参照Contains的比较器

8.9.2、Intersect

Intersect 获取两个集合中都存在的数据

获取同时存在于两个集合中的元素,与Union类似。

方法的声明如下:

public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);
​
public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

继续之前的业务场景,现在教务处需要知道有哪些同学同时报名了两个比赛

List<Student> students = students1.Intersect(students2).ToList();

8.9.3、Except

Except 获取只在第一个数据源中存在的数据

获取只存在于第一个集合的元素,从第一个集合中去除同时存在与第二个集合的元素,并返回。

方法的声明如下:

public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);
​
public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

继续业务描述,教务处要一份只报名了500米的学生名单:

List<Student> students = students1.Except(students2).ToList();

8.9.4、Reverse 翻转

数据源中的元素原本有一定的顺序,这个方法可以将数据源中的顺序翻转过来,原本是最后一个的变成了第一个,第一个变成了最后一个。

简单示例:

char[] apple = { 'a', 'p', 'p', 'l', 'e' };
char[] reversed = apple.Reverse().ToArray();

8.9.5、Distinct 去重

对数据源进行去重,然后返回去重之后的结果。同样,这个方法有两个重载版本,一个有比较器,一个没有比较器。

// 不用比较器的
public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source);
// 设置比较器
public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);

示例:

先描述一个可能会出现的场景,每个班级在各个赛事组提交报名信息的时候有点混乱,500米的负责老师把一个班的名单多录了一次,但是学生已经乱序了,现在需要把多录的去掉,也就是对数据进行去重。

List<Student> students = students1.Distinct();

9、linq项目中实现

9.1、左连接

1)左链接,条件查询后取条数

/// <summary>
/// 企業ベスティング未登録の資格喪失事由コード数
/// </summary>
/// <param name="companyOfficeCd">企業・事業所コード</param>s
/// <param name="planCd">プランコード</param>
/// <returns>未登録資格喪失事由コード数</returns>
public async Task<int?> GetQualificationLossReasonCd(string? companyOfficeCd, string? planCd)
{
    ArgumentNullException.ThrowIfNull(companyOfficeCd);
    ArgumentNullException.ThrowIfNull(planCd);
​
    var companyVestings = await this._dbContext.CompanyVesting
        .Where(r => r.CompanyOfficeCd == companyOfficeCd)
        .Where(r => r.PlanCd == planCd)
        .Where(r => r.StartDay <= _dcParkApplicationContext.BusinessDate)
        .Where(r => r.EndDay >= _dcParkApplicationContext.BusinessDate)
        .Where(r => !r.DelFlg)
        .ToListAsync();
​
    var qualificationandcompanyvesting = await this._dbContext.QualificationLossReason
        .Where(r => r.QualificationLossUploadTargetFlg != CodeCategories.QualificationLossUploadTargetFlg0)
        .Where(r => r.VestingRate == null)
        .Where(r => r.BusinessCd != CodeCategories.BusinessCodeParam000)
        .Where(r => !r.DelFlg).GroupJoin(
        companyVestings,
        qualification => new
        {
            qualification.QualificationLossReasonCd
        },
        companyvesting => new
        {
            companyvesting.QualificationLossReasonCd
        },
        (qualification, companyvesting) => new
        {
            qualification,
            companyvesting
        })
        .Where(r => r.companyvesting.Select(s => s.QualificationLossReasonCd).IsNullOrEmpty())
        .CountAsync();
​
    return qualificationandcompanyvesting;
}

2)左连接后取条数

/// <summary>
/// 退職手当移行金管理カウント
/// </summary>
/// <param name="planCd">プランコード</param>
/// <param name="companyOfficeCd">企業・事業所コード</param>
/// <returns>退職手当移行金管理数</returns>
public async Task<int?> CountLeaveCompAllowanceTransMoney(string? planCd, string? companyOfficeCd)
{
    ArgumentNullException.ThrowIfNull(companyOfficeCd);
    ArgumentNullException.ThrowIfNull(planCd);
​
    var countLeaveCompAllowanceTransMoneyResult = await this._dbContext.CompanyPension
        .Where(com => com.PlanCd == planCd
               && com.CompanyOfficeCd == companyOfficeCd
               && com.StartDay <= _dcParkApplicationContext.BusinessDate
               && com.EndDay >= _dcParkApplicationContext.BusinessDate
               && string.Compare(com.SystemTransContribYearMonthCompanyPensionEndYearMonth, string.Format("{0:yyyyMM}", _dcParkApplicationContext.BusinessDate)) > 0
               && com.SendTransSourceSystemSec == CodeCategories.TransferSourceSystemClassification04
               && !com.SystemTransImplementFlg
               && !com.DelFlg)
        .GroupJoin(
        this._dbContext.LeaveCompAllowanceTransMoney
        .Where(r => r.CompanyOfficeCd == null
               && !r.DelFlg),
        com => new
        {
            com.CompanyOfficeCd,
            com.PlanCd,
            com.SystemTransGroupNo
        },
        leaveComp => new
        {
            leaveComp.CompanyOfficeCd,
            leaveComp.PlanCd,
            leaveComp.SystemTransGroupNo
        },
        (com, leaveComp) => new
        { 
            com, leaveComp
        })
        .CountAsync();
​
    return countLeaveCompAllowanceTransMoneyResult;
}

9.2、单表查询

1)查询单表,返回第一条数据

/// <summary>
/// プラン拠出属性取得
/// </summary>
/// <param name="planCd">プランコード</param>
/// <returns>プラン拠出属性モデル</returns>
public async Task<PlanContribAttribute?> GetPlanContribAttribute(string? planCd)
{
    ArgumentNullException.ThrowIfNull(planCd);
​
    var planContribAttributeModelResult = await this._dbContext.PlanContribAttribute
        .Where(r => r.PlanCd == planCd
               && r.ContribSec == CodeCategories.ContributionCategory3
               && string.Compare(r.ContribStartYearMonth, DcParkConstants.MINIMUM_YEAR_MONTH) >= 0
               && string.Compare(r.ContribStartYearMonth, string.Format("{0:yyyyMM}", _dcParkApplicationContext.BusinessDate)) <= 0
               && string.Compare(r.ContribEndYearMonth, string.Format("{0:yyyyMM}", _dcParkApplicationContext.BusinessDate)) >= 0
               && string.Compare(r.ContribEndYearMonth, DcParkConstants.MINIMUM_YEAR_MONTH) <= 0
               && !r.DelFlg)
        .Select(r => new PlanContribAttribute
                {
                    PlanCd = r.PlanCd,
                    ContribStartYearMonth = r.ContribStartYearMonth,
                    ContribEndYearMonth = r.ContribEndYearMonth,
                    ContribSec = r.ContribSec,
                    ContribDataCreateDayCalcBusinessDays = r.ContribDataCreateDayCalcBusinessDays,
                    InstallmentDepositDayWithdrawalDayDD = r.InstallmentDepositDayWithdrawalDayDD,
                    HolidaySec = r.HolidaySec,
                    ContribDayCalcBusinessDays = r.ContribDayCalcBusinessDays
                })
        .FirstOrDefaultAsync();
​
    return planContribAttributeModelResult;
}

9.3、分组查询

1)分组查询

/// <inheritdoc />
public async Task<string?> GetParticipantCd(string? companyOfficeCd, string? empCd)
{
    ArgumentNullException.ThrowIfNull(companyOfficeCd);
    ArgumentNullException.ThrowIfNull(empCd);
​
    var a = await this._dbContext.CorpTypeParticipantMaster
        .Where(r => !r.DelFlg)
        .GroupBy(g => new
                 {
                     g.ParticipantCd
                 })
        .Select(s => new CorpTypeParticipantMaster
                {
                    ParticipantCd = s.Key.ParticipantCd,
                    Num = s.Max(r => r.Num)
                }).ToListAsync();
​
    var b = await _dbContext.CorpTypeParticipantMaster
        .Where(r => r.CompanyOfficeCd == companyOfficeCd
               && r.EmpCd == empCd
               && !r.DelFlg)
        .ToListAsync();
​
    var getParticipantCd = b
        .Join(
        a,
        corpType => new
        {
            ParticipantCd = corpType.ParticipantCd,
            Num = corpType.Num
        },
        corpTypeMaxNum => new
        {
            ParticipantCd = corpTypeMaxNum.ParticipantCd,
            Num = corpTypeMaxNum.Num
        },
        (corpType, corpTypeMaxNum) => new
        {
            corpType,
            corpTypeMaxNum
        })
        .Select(s => s.corpType.ParticipantCd)
        .FirstOrDefault();
​
    return getParticipantCd;
}