.NET入门: 使用linq,替代for循环遍历

Part 0 为什么要使用linq 🍌

我们来看一个简单的需求:

var numbers = new List<List<int>>{
   new List<int>{1,2,3,4},
   new List<int>{5,6,7,8,9},
   new List<int>{10,11,12}
};

这是一组数字,现在我需要你将里面的所有偶数挑出来,这当然很好实现。
如果你只会使用for循环:

for (int i = 0; i < numbers.Count; i++) {
    for (int j = 0; j < numbers[i].Count; j++) {
        if (numbers[i][j] % 2 == 0) {
            Console.WriteLine(numbers[i][j]);
        }
    }
}

这很简单,任何学过编程的同学都会。
如果你会foreach,那可以更简单:

foreach (var number in numbers) {
    foreach (var item in number) { 
    if (item%2==0) Console.WriteLine(item); 
    }
}

接下来我将告诉你更简便的方法,那就是用linq。


foreach (var number in numbers.SelectMany(x => x).Where(x => x % 2 == 0)) {
    Console.WriteLine(number);
}

这里引用一段🤖GPT对LInq的解释:

C# 中的 LINQ(Language Integrated Query)是一种编程技术,
它允许开发者使用类似 SQL 的查询语法来操作集合、数组、数据库等数据源。
LINQ 提供了一种统一的方式来查询和转换数据,使得代码更加简洁和易于理解。

使用linq可以直接对数据进行查询、处理。而不需要再使用for循环遍历的方式,不仅可以精简代码,
也可减少人为实现查询方法,因为疏忽而导致的错误。

Part 1 linq的简单使用 🖥

linq主要的使用方式分为两种,一种是语句式,一种是函数调用式。
上述例子🌰当中使用的就是函数调用式。
这里让🤖gpt随便生成了一些数据和数据定义

var students = new List<Student>
{
    new Student(1, "Alice", 20, "Sophomore", Gender.Female),
    new Student(2, "Bob", 21, "Junior", Gender.Male),
    new Student(3, "Charlie", 22, "Senior", Gender.Male),
};

public enum Gender
{
    Male,
    Female
   
}
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Grade { get; set; }
    public Gender Gender { get; set; } // 使用Gender枚举类型

    public Student(int id, string name, int age, string grade, Gender gender)
    {
        Id = id;
        Name = name;
        Age = age;
        Grade = grade;
        Gender = gender;
    }
}

生成的数据如下:

IDNameAgeGradeGender
1Alice20SophomoreFemale
2Bob21JuniorMale
3Charlie22SeniorMale

现在我们需要拿出年龄大于等于21岁的所有学生信息,
在学会使用linq之后,我们不再需要遍历整个students,然后判断年龄是否符合条件。

var studentInfo = from stu in students
                  where stu.Age >= 21
                  select stu;    
//或者
var studentInfo = students.Where(stu=>stu.Age>=21);

可能你已经发现 🔍了差异。为什么使用类似sql语句的语句式linq比函数式多了一行select?
🚨 这是因为使用语句式linq必须以select或者group结尾。 🚨
函数式也可以写作:

var studentInfo = students.Where(stu=>stu.Age>=21).Select(stu=>stu);

当然,这并没有什么意义,结果完全一样。
对于不了解lambda表达式的同学,简单解释一下放进函数式括号里面的❓是什么东东。
你大致可以理解为箭头前面的是指代集合列表中的每一个元素,就像foreach一样,箭头后面是返回值。

语句式和sql语句很像,这里需要注意的是函数式的使用。

var studentInfo = students.Where(stu=>stu.Age>=21);
var studentInfo = students.Select(stu=>stu.Age>=21);

这两行执行结果并不一样,where是从集合列表中选出符合条件的元素,返回的是元素的列表。
select选什么就会返回什么。例如在此处的返回结果其实是{false,true,true}。
对应的就是三个学生对象的年龄是否超过21岁。
我们再举个栗子🌰,你就能明白两者的差别:

var studentInfo = students.Select(stu=>stu.Age+1)

你知道返回结果是什么吗?age+1显然不是一个条件。结果是 {21,22,23}。
没错,就是每个元素的age+1。
如果你使用where语句,是不能使用这个条件的,条件的返回值必须得是一个bool值。

学会了linq应该如何使用,我们就可以开始使用一些其他的功能。例如分组、排序。

var studentInfo = from stu in students
                  group stu by stu.Gender into s
                  select s.Key + " :" + s.Count();
                  //into是给查询的结果取一个别名,用于之后的查询
//或者
var studentInfo = students.GroupBy(stu=>stu.Gender).Select(s=>s.Key+":"+s.Count());

上面的例子🌰中我们就得到了男女的人数分别是多少。
需要说明的是,分组之后返回的是一个IGrouping<TKey,TElement>的列表。其中Key就是用于分组的那一项。

同样我们可以用linq排序,而不需要实现一个专门的排序函数。

var studentInfo = students.OrderBy(stu=>stu.Age);
//默认是从小到大排序,如果你需要反序,使用OrderByDescending函数
var studentInfo = from stu in students
                  orderby stu.Age
                  select stu;
//如果你需要反序,在select前stu.Age后,加上Descending关键字

于是我们将学生按照年龄进行了排序,单独实现排序当然也可以实现比较器,然后掉用sort函数。
linq不止可以排序,还可以对排序的结果再进行查询。
关于linq的详细使用,可以参考
官方文档

需要注意的是,linq虽然很像sql,但并不是sql语句。例如在文章一开头提到的🌰
传送门
(关于SelectMany的使用,这里不展开说明。你可以大致理解为,对于多维数组,这个函数可以直接取到最低层的元素本身。而不是每一维度的集合。)
例子中,如果使用语句式应该

var number =from num in numbers
            from n in num
            where n%2==0
            select n;

而不是使用如同sql中的子查询,即查询嵌套。

至此你就已经可以愉快在.NET中使用linq了,每次查询处理集合列表的数据,不再需要使用for循环遍历。

🐞<font color=Red>使用应注意:</font>🐞
在使用linq前,你需要知道的是linq只是一种声明,类似于给集合列表添加某种规则,如果你不把结果foreach或者赋值(任何涉及到需要获取集合列表内容的操作都算,比如调用列表自带的count、get函数),linq本身不会被执行。 所以如果你只是对列表集合使用linq,而不使用返回的查询结果,是没有任何意义的。
例如:

students.Where(s=>s.Age>21);

运行完之后并不会对students进行修改,而是返回一个符合查询结果的List。
此外,即使你使用一个变量接住返回值,如果你不使用这个变量进行其他操作,也依旧不会执行相应的linq语句。

这里给出一个例子🐞

var stus=students.Where(s => s.Age >= 21);
students.Add(new Student(0,"张三",22,"初二",Gender.Male));
foreach (var item in stus)
{
    Console.WriteLine(item.Name);
}

我们在执行完linq之后,再向students添加了一个新的同学张三。似乎不会影响stus的值。
但是,如果你运行了上述代码,就会发现🔍输出结果是

Bob
Charlie
张三

原因就在于linq是在forech拿取stus元素时,换句话说就是在获取stus的内容时才被调用了。stus赋值声明操作并没有真正调用linq。

你可以这么理解linq,你有一个天天宅在宿舍🎮打游戏的同学sunsun🌞。
sunsun同学有个好习惯,那就是老师布置的作业,什么时候要收,才开始写。
如果你不知道sunsun同学的习惯。先布置第一次作业,然后不收。
之后又布置了第二次作业,此时再收第一次作业。
这个时候急急急的😰sunsun同学就会开始补作业,把两次作业一起交了。

上述代码给出的就是这么一种情况,我们先使用linq,查询了Age>21的所有同学。
因为 stus并没有被使用,里面的值不需要被修改或获取。🌞🌞同学就会选择不执行这个查询。
当我们遍历stus时,要用到stus的值了,😰😰linq才会对students进行查询。由于在这个之前,
我们往students里又插入了一条数据,所以也会被linq查询到。

🐞这是你使用时应该注意的。使用完linq后应该对linq的结果直接使用,否则如果你对原先整个集合列表进行其他操作,可能会影响到你之前linq返回的结果。

Part EX linq的常用函数 🎮

linq其实和sql一样,也有join操作。

linq的函数式中有些比较常用的方法,如first,last。获取列表集合的第一个和最后一个元素。
像项目中常用的应该是FirstOrDefault,LastOrDefault方法,避免查询结果为空的情况。

count(//lambda表达式 //)可以获取到符合条件的元素的数量。
Average(//lambda表达式 //)求对应的平均值。
Sum(//lambda表达式 //)同上。
MaxBy(//lambda表达式 //)可以获取对应满足对应key最大值的对象。
MinBy(//lambda表达式 //)同上。
any(//lambda表达式或者空//)判断符合条件的结果是否为空,因为是随机拿第一个值返回,所以效率比count遍历要高。

使用这些方法可以极大的简化查询过程的实现,更多的常用函数可以查看对应的官方文档。
官方文档


wang_hun
9 声望1 粉丝