ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# LINQ入门实战:VS Code环境搭建与查询语法避坑指南

C# LINQ入门实战:VS Code环境搭建与查询语法避坑指南 先说结论这个Demo我重新整理完之后最大的感受是——LINQ真的不难难的是环境先把人劝退了。VS Code里写C#练LINQ搭环境这一步就挡了不少人缺using、编码乱码、延迟执行的坑一个接一个。这篇文章把已经修正、可直接运行的练习Demo完整拆开怎么搭环境、数据源怎么设计、常用查询怎么写、哪些问题我踩过且已经修好全部对照代码讲清楚。适合这么几类人看刚接触LINQ搞不懂查询语法和方法语法到底用哪个的新手在VS Code里配C#环境一直报错、项目建不起来的人已经有了点基础想看一个完整、能跑、带注释的Demo通过改代码加深理解的朋友。下面直接进入正题。1. Demo定位与整体设计思路1.1 为什么选VS Code当LINQ练习场很多人一提C#开发第一反应就是打开Visual Studio。VS Studio功能确实全但对我想跑个LINQ小例子这种需求来说启动慢、安装体积大多少有点杀鸡用牛刀的意思。VS Code轻量、跨平台、启动速度快配上C# Dev Kit插件之后写代码时的智能提示、语法高亮、调试功能都够用而且配合终端里的.NET CLI建项目、编译、运行一条龙整个链路非常透明。我自己练LINQ的过程就是改一行代码终端敲一下dotnet run看输出再改。这个循环越快学习效果越好。VS Code的启动速度加上.NET CLI的增量编译让这个循环基本无等待。1.2 这个修正版Demo具体修了什么先说清楚已经修正四个字的分量。最早的版本其实是从一些教程片段里拼出来的问题不少我梳理了一下主要有四类第一类是编译层面的缺using System.Linq;在旧版.NET项目里直接报CS0103错误。第二类是逻辑层面的GroupBy之后不知道用group.Key去取分组的值反而试图去访问原始属性编译过不去。第三类是连接查询Join的参数顺序问题外层和内层的键选择器写反了导致查出来的数据配不上对。第四类是延迟执行造成的灵异现象查询定义完之后数据源又在后面被修改了最后输出结果跟预想完全不同。这次的修正版把上述问题全部处理掉了每个示例独立成块注释和输出说明都补齐了你照着敲一遍、跑一遍基本就能绕开这些坑。2. 环境准备与工程创建2.1 环境清单和版本选择开始之前先把环境统一一下避免后面跑不通。我用的组合是工具版本建议用途VS Code1.85以上编辑器.NET SDK8.0 LTS编译和运行C#代码C# Dev Kit插件最新版提供项目支持和调试功能.NET SDK安装完成后打开终端验证dotnet --version能看到类似8.0.100的版本号说明环境OK。如果提示command not found别急去.NET官网下载对应系统的SDK包重装装完重开一个终端窗口再试。VS Code插件方面装C# Dev Kit就够它会把编译、调试、项目加载这些能力一起带进来。第一次打开项目时VS Code右下角会提示是否加载项目依赖点确认就行。2.2 从零建一个可运行的控制台项目打开VS Code内置终端依次执行mkdir LinqPractice cd LinqPractice dotnet new console -n LinqPractice cd LinqPractice code .第一条命令创建目录dotnet new console会生成一个最小控制台项目里面默认有一个Program.cs内容是Console.WriteLine(Hello, World!);直接运行验证一下编译链路dotnet run终端打印出Hello, World!说明整个环境已经通了接下来就可以把Program.cs替换成LINQ练习代码。注意项目目录下会生成obj和bin两个文件夹一个是临时编译文件一个是编译输出不用关心。如果后面要用Git管理代码记得加.gitignore把它们忽略掉。3. LINQ核心知识拆解先弄懂再写代码3.1 查询语法与方法语法到底什么关系LINQ有两种写法这是新手最纠结的地方。查询语法Query Syntax长这样var result from s in students where s.Score 80 select s.Name;方法语法Method Syntax长这样var result students.Where(s s.Score 80) .Select(s s.Name);我经常拿点菜和进厨房自己配来类比查询语法更像照着菜单点where、select这些关键字就是菜单上的步骤方法语法则是直接拿Lambda表达式这个食材手动组合灵活性更强。实际上查询语法在编译时会被编译器转换成方法语法调用两者最终生成的IL代码基本一样。所以不存在哪个性能更好的问题只有哪种写起来更顺手的区别。我的经验是简单的筛选和投影用方法语法涉及到多个from比如跨集合查询或者想要可读性更强的时候用查询语法更直观。好消息是这两种写法可以随意混用。比如先用查询语法筛一遍再用方法语法调用OrderBy进行排序完全合法。3.2 延迟执行是好事也是坑LINQ查询分为延迟执行和立即执行两类。Where、Select、OrderBy这些返回IEnumerableT的操作都是延迟的——查询语句本身只是定义了一个查询计划真正执行发生在你开始遍历结果的那一刻。而ToList()、ToArray()、Count()、Sum()、First()这些会立刻执行返回具体数据。延迟执行的好处是性能好你可以在这个IEnumerable基础上继续叠加多个条件不会产生中间集合。坏处就是如果在查询定义之后、遍历之前修改了数据源那么查询结果会跟着变。我早先就栽过这个跟头示例代码里定义了一个查询后面又往学生列表里添加了新数据结果输出结果里多了一个不该出现的学生。排查了半天才发现是延迟执行在作怪。修正方法很简单如果希望查询结果在定义那一刻就固定下来用ToList()把它物化。3.3 必学的几个核心操作LINQ操作符非常多但练习阶段真正高频的就那么几个掌握之后大部分场景都能覆盖筛选Where按条件过滤投影Select把集合中的每个元素映射成新形状排序OrderBy、OrderByDescending以及多条件排序用的ThenBy分组GroupBy按某个键把数据分堆连接Join将两个集合同某个字段关联起来类似SQL内连接聚合Count、Sum、Average、Max、Min量词Any、All判断是否存在、是否全部满足分区Take、Skip取前N条、跳过前N条去重Distinct单条获取FirstOrDefault、SingleOrDefault下面的Demo代码就是围绕这套核心操作展开的每一个都有对应示例。4. Demo代码逐段解析核心环节全拆开4.1 数据源先定义好实体和测试数据LINQ练习最麻烦的是没数据可查。这个Demo里我设计了两个类学生Student和班级ClassInfo。// 学生实体 public class Student { public int Id { get; set; } public string Name { get; set; } string.Empty; public int ClassId { get; set; } public int Score { get; set; } } // 班级实体 public class ClassInfo { public int ClassId { get; set; } public string ClassName { get; set; } string.Empty; }初始化数据var classes new ListClassInfo { new() { ClassId 1, ClassName 软件工程一班 }, new() { ClassId 2, ClassName 软件工程二班 }, new() { ClassId 3, ClassName 计算机科学班 } }; var students new ListStudent { new() { Id 1, Name 张三, ClassId 1, Score 88 }, new() { Id 2, Name 李四, ClassId 1, Score 59 }, new() { Id 3, Name 王五, ClassId 2, Score 93 }, new() { Id 4, Name 赵六, ClassId 2, Score 72 }, new() { Id 5, Name 孙七, ClassId 3, Score 65 }, new() { Id 6, Name 周八, ClassId 3, Score 80 } };数据设计上我特意让一个数据覆盖多种查询场景有超过90的高分王五有不及格的59分李四三个班各有两个学生方便后面演示排序、分组和连接。实际写代码时用new() {}这种目标类型写法是C# 9之后才支持的SDK 8.0完全没问题。这里有个小提醒在.NET 6的顶级语句Top-Level Statements程序里类定义要放在所有可执行代码的下面编译器允许这样组织代码不影响运行。4.2 筛选与投影最常用的两个操作先看筛选和投影的组合这是LINQ最基础的用法// 示例1查询成绩大于等于80分的学生姓名 Console.WriteLine( 示例1成绩80分的学生 ); var highScoreStudents students .Where(s s.Score 80) .Select(s new { s.Name, s.Score }); foreach (var item in highScoreStudents) { Console.WriteLine(${item.Name}{item.Score}); }输出 示例1成绩80分的学生 张三88 王五93 周八80这段代码里用到了匿名类型new { s.Name, s.Score }这是LINQ里非常常见的投影技巧。好处是不用额外定义一个类就能把结果塑造成想要的形状。Select做的事情就是映射把每个Student对象映射成一个只包含名字和分数的新对象。对应的查询语法是var highScoreStudents2 from s in students where s.Score 80 select new { s.Name, s.Score };运行效果一模一样。我自己写投影比较多因为查询语法在单表筛选这种场景下稍微啰嗦一点。4.3 排序与多条件排序OrderBy和ThenBy的配合排序的关键点是OrderBy只做一次排序如果还要按第二关键字排就必须用ThenBy。// 示例2先按班级升序再按分数降序排列 Console.WriteLine( 示例2按班级升序分数降序 ); var sortedStudents students .OrderBy(s s.ClassId) .ThenByDescending(s s.Score) .Select(s ${s.ClassId}班 {s.Name} {s.Score}分); foreach (var item in sortedStudents) { Console.WriteLine(item); }输出 示例2按班级升序分数降序 1班 张三 88分 1班 李四 59分 2班 王五 93分 2班 赵六 72分 3班 周八 80分 3班 孙七 65分注意如果这里用两个OrderBy链式调用第二段OrderBy会覆盖第一段的排序导致班级顺序正确但分数顺序错乱。这也是很多新手踩过的坑OrderBy加OrderBy不等于多条件排序必须用ThenBy或ThenByDescending。那个覆盖问题我当年调试过整整一晚上。4.4 分组与聚合GroupBy的正确打开方式分组是LINQ里最容易写错的地方关键是要搞清楚IGroupingTKey, TElement这个结构——分组的每个元素都带有.Key属性访问分组键用group.Key并不是去访问数据源原始属性。// 示例3按班级分组统计每班人数和平均分 Console.WriteLine( 示例3按班级分组统计 ); var groupResult students .GroupBy(s s.ClassId) .Select(g new { ClassId g.Key, Count g.Count(), AvgScore g.Average(s s.Score) }); foreach (var item in groupResult) { Console.WriteLine($班级{item.ClassId}{item.Count}人平均分{item.AvgScore:F2}); }输出 示例3按班级分组统计 班级12人平均分73.50 班级22人平均分82.50 班级32人平均分72.50GroupBy(s s.ClassId)返回的是一组组每个组里面装着属于这个组的学生对象。g.Key就是班级ID而g本身可以继续做聚合操作比如Count()数人数、Average(s s.Score)算平均分。掌握了Key的用法分组就没那么吓人了。聚合函数也可以单独用在整张表上// 示例4全体学生的总分和平均分 Console.WriteLine( 示例4全体统计 ); Console.WriteLine($学生总数{students.Count()}); Console.WriteLine($总分{students.Sum(s s.Score)}); Console.WriteLine($平均分{students.Average(s s.Score):F2}); Console.WriteLine($最高分{students.Max(s s.Score)}); Console.WriteLine($最低分{students.Min(s s.Score)});4.5 连接查询Join的参数顺序别搞反连接是LINQ里最接近SQL的玩法也是我当初最先写错的地方。// 示例5连接班级表和学生表显示班级名称 Console.WriteLine( 示例5学生与班级连接查询 ); var joinResult students.Join( classes, // 要连接的目标集合 student student.ClassId, // 左边(students)的匹配键 classInfo classInfo.ClassId, // 右边(classes)的匹配键 (student, classInfo) new // 结果投影 { student.Name, classInfo.ClassName, student.Score }); foreach (var item in joinResult) { Console.WriteLine(${item.ClassName} - {item.Name}{item.Score}分); }输出 示例5学生与班级连接查询 软件工程一班 - 张三88分 软件工程一班 - 李四59分 软件工程二班 - 王五93分 软件工程二班 - 赵六72分 计算机科学班 - 孙七65分 计算机科学班 - 周八80分Join一共有四个参数后两个才是容易错的。第二、第三个参数分别是左右两个集合的键选择器——第一个student student.ClassId是从学生集合里取班级ID第二个classInfo classInfo.ClassId是从班级集合里取班级ID。我早先把它们写反过编译居然也能通过但结果数据全部错位人在控制台前懵了很久。这种错误编译器不会提示全靠对照输出检查。4.6 量词、分区和去重常用但容易忽略的操作量词Any和All经常被忽略但写实际项目时特别有用// 示例6是否存在不及格的学生 bool hasFailed students.Any(s s.Score 60); Console.WriteLine($是否存在不及格学生{hasFailed}); // 是否所有学生都及格 bool allPassed students.All(s s.Score 60); Console.WriteLine($是否全部及格{allPassed});分区Take和Skip在处理分页场景时是标配// 示例7取分数最高的前2名学生 Console.WriteLine( 示例7分数最高的前2名 ); var top2 students .OrderByDescending(s s.Score) .Take(2) .Select(s ${s.Name}{s.Score}分); foreach (var item in top2) { Console.WriteLine(item); }去重Distinct也顺手提一下假设有个包含重复数据的集合Distinct可以去掉重复项// 示例8去重 var classIds new[] { 1, 1, 2, 2, 3, 3, 3 }; var distinctIds classIds.Distinct(); Console.WriteLine($去重后{string.Join(,, distinctIds)});5. 直接运行全流程与修正记录5.1 一节完整的Program.cs示例把上面所有模块整合成一个完整的、可直接运行的Program.cs结构是数据准备、示例模块、类型声明三个部分。下面是一个浓缩版的完整代码适合作为练习起点using System; using System.Collections.Generic; using System.Linq; // 数据准备 var classes new ListClassInfo { new() { ClassId 1, ClassName 软件工程一班 }, new() { ClassId 2, ClassName 软件工程二班 }, new() { ClassId 3, ClassName 计算机科学班 } }; var students new ListStudent { new() { Id 1, Name 张三, ClassId 1, Score 88 }, new() { Id 2, Name 李四, ClassId 1, Score 59 }, new() { Id 3, Name 王五, ClassId 2, Score 93 }, new() { Id 4, Name 赵六, ClassId 2, Score 72 }, new() { Id 5, Name 孙七, ClassId 3, Score 65 }, new() { Id 6, Name 周八, ClassId 3, Score 80 } }; // 示例筛选 排序 Console.WriteLine( 成绩80的学生按分数降序 ); var result students .Where(s s.Score 80) .OrderByDescending(s s.Score) .Select(s ${s.ClassId}班 {s.Name}{s.Score}分); foreach (var line in result) { Console.WriteLine(line); } // 示例分组统计 Console.WriteLine( 各班平均分 ); var groupStats students .GroupBy(s s.ClassId) .Select(g $班级{g.Key}平均分{g.Average(s s.Score):F2}); foreach (var line in groupStats) { Console.WriteLine(line); } // 示例连接查询 Console.WriteLine( 学生-班级连接 ); var joinData students.Join( classes, student student.ClassId, classInfo classInfo.ClassId, (student, classInfo) ${classInfo.ClassName} - {student.Name}); foreach (var line in joinData) { Console.WriteLine(line); } // 类型定义放文件末尾顶级语句规则 public class Student { public int Id { get; set; } public string Name { get; set; } string.Empty; public int ClassId { get; set; } public int Score { get; set; } } public class ClassInfo { public int ClassId { get; set; } public string ClassName { get; set; } string.Empty; }把这段代码粘贴到Program.cs里保存然后执行dotnet run如果一切正常就能看到筛选、分组、连接三类查询的输出结果。5.2 我修正过的那些问题这次整理Demo时我对照旧代码一条条排查把发现的问题和修正方式记录如下问题现象原因修正方式编译报CS0103缺少using System.Linq;文件顶部补上using System.Linq;dotnet run后中文乱码终端代码页与UTF-8不一致在Program.cs开头加Console.OutputEncoding System.Text.Encoding.UTF8;GroupBy后访问g.ClassId报错分组集合没有原始类的属性应该用g.Key把g.ClassId改成g.KeyJoin结果数据错位内外的键选择器参数写反确认第一个键选择器针对外层集合第二个针对内层集合查询结果里出现意外学生延迟执行数据源在遍历前被修改在需要固定结果时使用ToList()物化结果匿名类型写错属性名new { s.Name, s.Score }属性名与预期不符投影时显式指定别名new { name s.Name, score s.Score }这些问题每一件都是我在实际练习中遇到的没有一个是从文档里抄来的。其中延迟执行导致的结果异常最隐蔽一旦数据源较大你可能会以为是自己筛选条件写错了压根想不到问题出在执行时机上。6. 常见问题与避坑指南6.1 高频问题速查表现象可能原因解决建议dotnet run提示未找到项目当前终端不在项目目录下cd进到含.csproj的目录再运行C#文件打开后没有智能提示插件未加载项目等待VS Code右下角提示点加载或Restore输出结果顺序每次不一样LINQ延迟执行 数据源枚举顺序不定需要稳定顺序时使用OrderByFirstOrDefault()返回null集合中不存在满足条件的元素先判断是否为空再访问属性匿名类型属性名被编译器改成Item1等在投影时没有给属性命名写new { 自定义名 表达式 }两个集合Join后输出行数变多存在一对多匹配确认键是否唯一必要时用GroupJoin6.2 我在实际练习中的几点体会第一把每个查询独立成一个方法或独立模块不要全部堆在Main里。我最初是把八个示例全写在一个顺序结构里输出全混在一起排查问题非常痛苦。修正版里每个示例用Console.WriteLine分隔思路清晰多了。第二Debug调试比加一堆打印高效。VS Code里按F5启动调试在LINQ查询语句上打断点单步执行时可以看到IEnumerable的展开内容比盲目加打印强太多。尤其是GroupBy和Join看调试器里的Results View能直观理解分组和连接的结构。第三刻意练习延迟执行。这是我认为学习LINQ必须过的一关。写代码时主动制造一个场景定义一个查询不ToList()修改数据源再遍历结果观察输出变化。这个实验做完你对LINQ执行机制的理解会上升一个层次。第四遇到错误先读编译器输出。VS Code的终端会显示完整的编译错误列表带行号和错误码。比如CS1061表示类型不包含某个定义看到错误码再去查比盯着代码猜要快得多。6.3 后续还能扩展什么这个Demo练完之后你可以往这些方向继续扩展增加一个SelectMany的示例处理一对多关系时非常有用比如查询所有班级里所有学生的名字用LINQ to JSON或LINQ to Objects配合System.Text.Json做数据解析练习真实业务场景试试Zip操作符它可以把两个集合按位置一一配对适合处理成对数据把查询结果放到HashSet里做集合运算比如交集、并集、差集Intersect、Union、Except都是面试高频题。另外如果之后接触EF Core你会发现数据库查询的写法跟这个Demo里的Queryable扩展方法几乎一模一样现在练好LINQ基础后面学ORM会顺畅很多。个人体会LINQ的语法学习其实一晚上就能看懂真正有价值的是把查询思维练出来——拿到一个数据操作需求能迅速判断是该用Where、GroupBy还是Join。这个只能靠多写。这个Demo就是给你一个安全、可复现的练习场随便改、随便跑改坏了重来就是一条命令的事。
返回列表