.net是什么?
它是一个开发平台,主要编程语言是C#。
.net主要生态:
|----------------|---------------------------|
| .NET Framework | Windows 专属的老版框架 |
| .NET Core | 跨平台的新版轻量框架 |
| .NET Standard | 统一工具规范(不是框架),编写通用类库(复用代码) |
类库就是编程里的 "调料包":把验证手机号、计算订单金额、操作数据库这些通用功能,提前写好打包,其他项目(Web、桌面、小程序后端)不用重复写,直接 "引用" 就能用。
ASP.NET Core,是 微软基于.NET Core(新版.NET)打造的、跨平台的服务器端 Web 开发框架就等于 "用.NET 做 Web 后端的框架
.net开发环境配置
配置VS功能组件
在电脑设置应用中找到VS点击修改,再勾选.net开发组件,之后点击右下角修改,等待下载完成。

SDK 是 "软件开发工具包" 的缩写,类比成 **"做蛋糕的全套材料 + 工具包"**:
它不仅包含 "做蛋糕的原料"(比如.NET 的基础类库、API 接口,相当于现成的代码逻辑),
还包含 "做蛋糕的工具"(比如.NET 的编译器、项目模板、打包工具,相当于把代码变成可运行程序的能力)。
具体到 .NET SDK,就是开发.NET 程序必需的 "全套工具包",核心包含:
- 编译器
- 项目模板
- 命令行工具(dotnet CLI)
- 基础类库
- .NET 运行时
命令行写.net代码
也就是.NET CLI 是 .NET Command-Line Interface(.NET 命令行界面)
常见命令:
dotnet new console:在当前文件夹中创建.net项目
dotnet run:运行项目
可以去官方文档查看更多命令:
VS常见项目类型
VS常见项目类型:
|---------|--------------------|---------------------------------------------------|
| 桌面 | 开发带图形界面的桌面应用 | Windows 端软件(如办公工具、本地管理程序,用 WinForm/WPF/MAUI 实现) |
| 移动 | 开发跨平台移动应用 | iOS/Android/App 端程序(用.NET MAUI 实现,替代旧版 Xamarin) |
| UWP | 开发 Windows 生态跨设备应用 | Windows 10 + 的电脑 / 平板 / Xbox 统一应用(微软已逐步用 MAUI 替代) |
|---------|-------------------|---------------------------------------------------------------|
| 控制台 | 开发无图形界面的命令行程序 | 本地脚本工具、后台批处理任务(如日志分析脚本、数据导出工具) |
| 服务 | 开发后台常驻程序 | 系统级后台任务(如定时备份服务、消息队列消费服务,用 Worker Service/Windows Service 实现) |
| 云 | 开发云原生服务 | Azure 云函数(无服务器接口)、云作业(如 Azure Functions、WebJob) |
|--------|---------------------|----------------------------------------|
| 库 | 开发可复用的通用代码包 | 封装工具类、业务逻辑(如 "数据验证类库""订单计算类库",供其他项目引用) |
| 测试 | 开发代码测试用例 | 验证业务逻辑正确性(如单元测试、集成测试,用 xUnit/NUnit 框架) |
| 扩展 | 开发 Visual Studio 插件 | 增强 VS 功能(如自定义代码片段、工具窗口) |
改项目框架

项目结构与发布
查看文件代码的装逼方法
命令行打开一个记事本,将文件拖到记事本中就可以直接看它代码


.net项目文件
.net Core的项目文件中,不会写包含了哪些类等等,只会写你移走了的文件(将本属于此项目的文件移走了,这个文件名会记录到项目文件中)
而.net framework相反
发布※
右键项目名,点击发布





到这一步只是创建好了文件夹,接下来是配置:是否包含运行环境、或特殊的运行环境:
比如部署模式选择独立,那么无需对方有运行环境就能直接运行,因为发布里的内容包含了环境。而选择了依赖框架就要求对方必须有运行环境才能运行,并且还可以选择可运行的系统,可移植性就是支持所有系统。

微软提供的2个类似虚拟机的东西,可以用来测试发布的内容去其他机器上运行的效果:
模拟linux:WSL
windowns:SandBox
包商店--NuGet
NuGet:.NET 的 "代码组件商店",用于下载 / 管理通用类库(如数据库、日志工具),避免重复写代码。

第一种可以打开控制台,里面输入命令行用于安装 包。
在官网NuGet Gallery | Home找到需要的包,会提供命令行
注意不要复制错的命令行,有多种场景
第二种是图形化安装,找到自己要的,点击安装即可。
命令行管理包
写命令行有两种方式:1、电脑命令行 2、包管理控制台命令行。这里展示都是2
安装:Install-Package XXX。-Version 指定版本。
卸载:Uninstall-Package XXX3)
更新到最新版:Update-Package XXX
点击项目,项目文件里会有已安装了包的名字(直接删除那行代码,效果就是删除那个包)
异步编程
异步的核心是「不浪费 "等待时间",释放资源做其他事」,用餐馆场景类比:
- 同步(传统阻塞式):餐馆只有 1 个服务员,规则是 "必须等一桌顾客吃完、结账、清桌,才能接待下一桌"。哪怕顾客只是坐着看菜单、没点菜(对应程序里的 "IO 等待",比如查数据库、调网络接口的等待时间),服务员也只能站在旁边空等,哪怕还有空桌子,也没法服务新顾客 ------ 结果就是资源闲置,1 小时能接待的顾客总量(吞吐量)很低。
- 异步(非阻塞式):还是 1 个服务员,规则是 "把菜单给顾客后,不用原地等,直接去接待下一桌,等顾客喊点菜 / 结账时再回来处理"。顾客看菜单的空闲时间里,服务员能充分利用时间服务更多桌,不用浪费在等待上 ------ 最终不仅不会拒绝新顾客,还能提升 1 小时的总翻台数(程序吞吐量),核心是让资源(服务员 / CPU 线程)不再空等,利用率拉满。
- 注意:异步提升整体的吞吐量,但不会提升单个线程的速度
A 城市和 B 城市之间的铁路,假设固定只有 2 条轨道(对应程序的硬件资源上限,比如 CPU 核心、线程池数量,不会额外增加),高铁就是要处理的任务(比如查数据库、调接口),每趟高铁跑完全程的总耗时(比如 1 小时)始终不变 ------ 同步和异步的核心差异,不在于轨道数量,而在于「轨道的调度规则」:
同步(阻塞式):调度规则是 "一辆高铁必须从头跑到尾,哪怕中途停靠装卸货(对应程序里的 IO 等待,比如等数据库返回数据),也得占着轨道不动"。后面的高铁只能排队等前面的车完全离开轨道,才能发车。结果就是轨道经常被 "闲置的高铁" 占用,1 小时能跑的总车次(吞吐量)很低,资源利用率浪费严重。
异步(非阻塞式):调度规则优化成 "高铁中途停靠装卸货时,先临时移出轨道(释放资源),让后面的高铁先上轨道跑;等装卸货完成,再把这趟高铁重新安排回空闲轨道继续行驶"。不用加轨道,只是避免了轨道被 "等待的高铁" 占用,让现有轨道的空闲时间全用来跑更多车次。结果就是 1 小时能跑的总车次(吞吐量)大幅提升,但每趟高铁的总耗时依然是 1 小时 ------ 核心是通过优化调度,把闲置的资源利用起来,而不是增加新资源。
简单总结:同步是 "占着资源等任务完成",异步是 "任务等待时释放资源,让其他任务先用";两者都不改变单个任务的耗时,但异步能通过优化资源调度,大幅提升单位时间内的总处理量(吞吐量)。
- 轨道 = 线程池 / CPU 核心(固定硬件资源);
- 高铁 = 业务任务(比如处理一个用户请求);
- 高铁停靠装卸货 = IO 等待(查数据库 / 调接口);
- 同步:高铁占着轨道等装卸货,后面的车只能排队;
- 异步:高铁卸货时临时让出轨道,后面的车先跑,卸货完成后再回轨道继续走。
传统的多线程是很麻烦的,但引进了async、await就方便了很多。
传统的多线程编程确实很麻烦 ------ 需要手动创建线程、管理线程的启动 / 停止、处理线程同步和锁问题,还得担心线程安全(比如多个线程同时修改一个变量),代码又复杂又容易出 bug。而 async/await 并没有替代多线程,而是提供了一种 "更优雅的异步编程模型":它会帮我们自动管理异步操作的 "等待 - 恢复" 流程,不用手动触碰线程相关的底层逻辑,写出来的代码像同步代码一样直观,却能实现异步非阻塞的效果,大幅降低了异步编程的复杂度。
异步方法
1)异步方法的返回值一般是 Task<T>,T 是真正的返回值类型,Task<int>。
惯例:异步方法名字以 Async 结尾。
2)即使方法没有返回值,也最好把返回值声明为非泛型的 Task。
- 调用泛型方法时,一般在方法前加上 await 关键字,这样拿到的返回值就是泛型指定的 T 类型;
4)异步方法的 "传染性":一个方法中如果有 await 调用,则这个方法也必须修饰为 async
异步方法的特征:有async,或有Task
cs
// 用同步、异步实现往文件中添加字符串,之后又读出字符串
namespace awaitasyncs1
{
internal class Program
{
static async void Main(string[] args)
{
// 同步方法
//string filename = @"E:\a\1.txt";
//File.WriteAllText(filename, "hello");
//string s = File.ReadAllText(filename);
//Console.WriteLine(s);
// 异步方法
string filename = @"E:\a\1.txt";
await File.WriteAllTextAsync(filename, "hello");//这个await作用是等,让这段代码彻底执行完,才能执行下面代码。
//如果不写的话,并且写入数据量大,耗时长。那么这行代码没执行完,就会执行下一行代码,导致报错。因为对一个文件同时读写肯定不对
string s = await File.ReadAllTextAsync(filename);//这里await作用可以将异步方法的返回值真正的数据类型提取出来
Console.WriteLine(s);
}
}
}
编写异步方法
有的方法不能声明为异步方法,而它方法体中又要写异步方法 (原规定只有此方法调用了异步,那么它也必须声明为异步方法):如此的话就去调整方法体中调用异步的地方,不在调用的异步方法前加await,而是在其后面加Result(),如果是无参的话加Wait()。
但尽量不要用,有死锁风险
cs
using System;
using System.Threading.Tasks;
// 1. 无返回值的异步方法
async Task PrintLogAsync()
{
await Task.Delay(1000);
Console.WriteLine("异步日志打印完成");
}
// 2. 有返回值的异步方法
async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "异步获取的业务数据";
}
// 假设这个方法因框架/接口限制,不能加async(比如接口要求必须是void/string返回值的同步方法)
static void SyncMethodCannotBeAsync()
{
// 场景1:调用无返回值异步方法 → 用.Wait()阻塞等待完成
PrintLogAsync().Wait();
// 场景2:调用有返回值异步方法 → 用.Result获取返回值
string data = GetDataAsync().Result;
}
很多方法都支持异步:Main、WinForm事件处理函数
委托 lamda 表达式中写异步方法,要求 lamda 表达式为异步 lamda 表达式,就是在前面加 async
async、await 原理揭秘
结论:看似等待,实质不等,执行其他事去了。
比如高铁中途卸货,看似停了,其实,此时轨道(进程)允许其它高铁运行了,实质整体看起来没等。
至于此高铁后续安排,卸完货(也就是异步方法结束),如果之前轨道空闲了就会拿过来用,其他的空闲轨道也是能用的,主要看线程池调度
用 ILSpy 编译dll (不是编译.exe,它是Windows专属,信息量不多)
反编译后,就能看到容易理解的底层代码。
可以观察到"await、async" 是 "语法糖",最终编译成 "状态机调用"。
总结:async 的方法会被 C# 编译器编译成一个类,会主要根据 await 调用进行切分为多个状态,对 async 方法的调用会被拆分为对 MoveNext 的调用。用 await 看似是 "等待",经过编译后,其实没有 "wait"。
用到await,这行代码就会暂停,不会继续往下执行此方法了。看起来是在等,但实际上它是去执行其他事情去了,但这边看起来确实是在等。但要懂的是:它是干其他事情去了。等这行代码彻底执行完,就会继续推进此方法。
async 背后的线程切换
比如高铁中途卸货,看似停了,其实,此时轨道(进程)允许其它高铁运行了,实质整体看起来没等。
至于此高铁后续安排,卸完货(也就是异步方法结束),如果之前轨道空闲了就会拿过来用,其他的空闲轨道也是能用的,主要看线程池调度
这里面就是会发生线程切换
尽量避免线程切换,比较损耗性能
异步方法不等于多线程
结论:异步方法不会自动在新线程中执行,只用手动实现,下面是:将异步代码以委托的显示传给Task.Run(),这样就会从线程池中取出一个新线程执行委托

为什么有的异步方法没标 async
------ 加 async 会生成状态机,反而增加额外开销,直接返回 Task 更高效。
能不加async就不加,不加就会被当成普通方法,减少开销。
如果方法内有任何额外逻辑,就必须保留async
- 带
async的本质 :编译器会为async方法生成「状态机类」(记录await的暂停 / 恢复),即使仅单纯转发,也会额外生成状态机代码,增加微小的性能开销;- 去掉
async的本质 :直接把GetDataAsync()返回的Task<string>"透传" 出去,跳过状态机生成,减少内存分配和 GC 压力,且调用方用await调用时,行为和改造前完全一致。
cs
async Task<string> ForwardGetDataAsync()
{
// 只有"return await 异步方法",无任何额外处理
return await GetDataAsync();
}
改成不带async:
Task<string> ForwardGetDataAsync() // ① 去掉async
{
return GetDataAsync(); // ② 去掉await,直接返回Task<string>
}
不要用 Sleep
Thread.Sleep()是阻塞线程(线程空等,浪费资源);异步场景要改用Task.Delay()(非阻塞,释放线程去做其他事),避免资源闲置。
sleep就是占着线程,即使使用不到,也不释放
取消运行--CancellationToken
有时需要提前终止任务,比如:请求超时、用户取消请求。很多异步方法都有 CancellationToken 参数,用于获得提前终止执行的信号。
CancellationToken 是个结构体
None:空
bool IsCancellationRequested 是否取消
ThrowIfCancellationRequested () 如果任务被取消,执行到这句话就抛异常。
CancellationTokenSource
CancelAfter (int) 超时后取消信号
Cancel () 发出取消信号
CancellationToken Token
为 "下载一个网址 N 次" 的方法增加取消功能。分别用 GetStringAsync + IsCancellationRequested、GetStringAsync + ThrowIfCancellationRequested ()、带 CancellationToken 的 GetAsync () 分别实现。取消分别用超时、用户敲按键(不能 await)实现。
WhenAll
Task.WhenAll 和 Task.WhenAny 都是.NET 中处理多个异步任务 的核心静态方法,核心差异在于 "等待的条件"------WhenAll 等所有任务完成 ,WhenAny 等任意一个任务完成
| 方法 | 核心逻辑 | 返回值类型 | 核心耗时 |
|---|---|---|---|
Task.WhenAll |
等待所有传入的异步任务全部完成 | 无返回值任务:Task; 有返回值任务:Task<TResult[]>(结果数组与任务顺序一致) |
≈ 耗时最长的单个任务 |
Task.WhenAny |
等待任意一个异步任务完成(谁快等谁) | Task<Task>(无返回值);Task<Task<TResult>>(有返回值) |
≈ 耗时最短的单个任务 |
异步其他问题
1、不要同步、异步方法混用,尽量都用异步
2、老版本以及包括WPF Winform是涉及到SynchronizationContext、ConfigureAwait(false)
3、async 是提示编译器为异步方法中的await 代码进行分段处理的,而一个异步方法是否修饰了 async 对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为 async。
4、yield return 不仅能够简化数据的返回,而且可以让数据处理 "流水线化",提升性能。
如下两个方法效果相同。但是法2底层是将其分割为三块,并不需要完全执行完整个方法才开始输出结果,而是执行一点就直接输出了,不必一直占用资源,提升了性能
cs
static IEnumerable<string> Test()
{
List<string> list = new List<string>();
list.Add("hello");
list.Add("yck");
list.Add("youzack");
return list;
}
cs
static IEnumerable<string> Test()
{
yield return "hello";
yield return "yck";
yield return "youzack";
}
5、在旧版 C# 中,async 方法中不能 yield。从 C#8.0开始,把返回值声明为 IAsyncEnumerable,就可以在async 方法中使用 await foreach () 语句。
cs
static async Task Main(string[] args)
{
await foreach(var s in Test())
{
Console.WriteLine(s);
}
}
static async IAsyncEnumerable<string> Test()
{
yield return "hello";
yield return "yck";
yield return "youzack";
}
Linq
Linq作用是:让数据处理变得简单
学Linq前提得会委托和lambda表达式
委托
1、委托是可以指向方法的类型,调用委托变量时执行的就是变量指向的方法。
2、.NET 中定义了泛型委托 Action (无返回值) 和 Func (有返回值),所以一般不用自定义委托类型。
委托就是一种数据类型,和int一样
cs
int i = 3;
委托 a = add方法;
只不过委托变量存的是方法,使用这个变量就会执行所存的方法
看以下代码理解:
cs
namespace NetLearn
{
class Program
{
//首先得声明一个委托,无参的
delegate void D1();
//声明一个有参委托
delegate int D2(int i, int j);
//写个方法,用于存到委托中
//无参方法
static void F1()
{
Console.WriteLine("我是方法1");
}
//有参方法
static int Add(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
//声明一个D1委托变量,并指向F1方法,之后可以直接使用这个方法
D1 d = F1;
d();
//有参委托,同理使用
D2 d2 = Add;
Console.WriteLine(d2(3, 4));
//到这里会发现,咋和int不一样,还需要自己去声明一个委托数据类型才能用,就像我还得手搓 一个int才能用int
//其实系统里是有的,如果是无参委托就用Action声明,有参则用Func,注意:Func中最后一个参数类型是指方法返回值类型
Action a = F1;
a();
Func<int, int, int> f = Add;
Console.WriteLine(f(3, 4));
}
}
}

Lambda表达式
委托变量不仅可以指向普通方法,还能指向匿名方法,而匿名方法可以写成lambda表达式
以下就是委托指向匿名方法:
cs
Func<int, int, string> f1 =
delegate (int i1, int i2)
{
return $"{i1}+{i2}={i1 + i2}";
};
关键:如何将匿名方法变成lambda表达式
- 系统能根据参数数据类型推断出参数的类型,因此可以省略委托类型,用
=>引出来方法体。 - 如果委托没有返回值,且方法体只有一行代码,可省略
{}。 - 如果
=>之后的方法体中只有一行代码,且方法有返回值,那么可以省略方法体的{}以及return。 - 如果只有一个参数,参数的
()可以省略。
根据这些可以得到:
cs
Func<int, int, string> f1 =
(i1, i2)=> $"{i1}+{i2}={i1 + i2}";
Linq
LINQ 中提供了很多集合的扩展方法,配合 lambda 能简化数据处理。
cs
namespace Linq1
{
class Program
{
static void Main(string[] args)
{
int[] nums = new int[] {3,5,3453,33,2,9,35};
IEnumerable<int> result = nums.Where(a=>a>10);
//Where 方法会遍历集合中每个元素,对于每个元素
// 都调用 a=>a>10 这个表达式判断一下是否为 true
// 如果为 true,则把这个放到返回的集合中
foreach(int i in result)
{
Console.WriteLine(i);
}
}
}
}
可以使用 var 让编译器的 "类型推断"来简化类型的声明。在 LINQ 中常用。C# 的 var 和 JavaScript 的 var 不一样,仍然是强类型的。(*) C# 中的弱类型是 dynamic。
.NET 中 LINQ(Language Integrated Query,语言集成查询)是用于统一查询 / 处理各种数据源(集合、数据库、XML 等)的语法,核心是通过 "类 SQL 语法" 或 "链式方法调用" 简化数据操作。以下是常用知识总结:
一、LINQ 核心前提
- 数据源要求 :需实现
IEnumerable<T>(内存集合,如数组、List)或IQueryable<T>(外部数据源,如数据库); - 命名空间 :必须引用
using System.Linq;; - 语法形式 :支持两种写法(功能等价,按需选择):
- 查询语法:类 SQL 的声明式语法(易读);
- 方法语法:链式调用扩展方法(灵活,配合 Lambda)。
二、常用扩展方法(方法语法)
LINQ 提供了大量集合扩展方法,配合 Lambda 表达式使用,以下是高频方法:
1. 筛选:Where
根据条件过滤元素(返回符合条件的子集)。
csharp
运行
int[] nums = { 3, 99, 88, 7, 15 };
// 筛选出大于10的元素
var result = nums.Where(n => n > 10);
// 结果:99、88、15
2. 投影:Select
将元素转换为其他类型 / 结构(类似 "列选择")。
csharp
运行
// 将数字转换为字符串格式
var strNums = nums.Select(n => $"数值:{n}");
// 结果:"数值:3"、"数值:99"...
// 投影为匿名类型(临时封装多字段)
var userInfos = new[] { new { Id=1, Name="张三" }, new { Id=2, Name="李四" } }
.Select(u => new { 编号=u.Id, 姓名=u.Name });
3. 排序:OrderBy/OrderByDescending + ThenBy
OrderBy:升序排序;OrderByDescending:降序排序;ThenBy:主排序后,按第二个条件二次排序。
csharp
运行
// 先按数值升序,再按奇偶(偶数在前)
var sorted = nums.OrderBy(n => n)
.ThenBy(n => n % 2 == 0 ? 0 : 1);
4. 聚合:Count/Sum/Max/Min/Average
统计集合的汇总结果(立即执行,非延迟)。
csharp
运行
int count = nums.Count(n => n > 10); // 大于10的元素数量:3
int sum = nums.Sum(); // 总和:3+99+88+7+15=212
int max = nums.Max(); // 最大值:99
5. 分组:GroupBy
按条件将元素分组(返回IGrouping<TKey, TElement>集合)。
cs
// 按"奇偶"分组
var groups = nums.GroupBy(n => n % 2 == 0 ? "偶数" : "奇数");
// 遍历分组
foreach (var group in groups)
{
Console.WriteLine($"分组:{group.Key}");
foreach (var num in group) Console.WriteLine(num);
}
GroupBy是 LINQ 给集合(如数组)提供的分组扩展方法 ,作用是:遍历集合中的每个元素,按指定 "规则" 给元素分配一个 "分组键",最终将相同键的元素归为一组。外层遍历:
foreach (var group in groups)
groups是 "分组的集合",group代表其中一个分组 (比如先遍历到"奇数"分组,再遍历到"偶数"分组)内层遍历:
foreach (var num in group)
group本身是一个 "包含多个元素的分组"(实现了IEnumerable<int>),num代表当前分组中的单个元素;- 作用:遍历并输出该分组下的所有数字(比如
"奇数"分组下的 3、99、77...)。
6. 元素操作:First/Single/Last
获取集合中单个元素 (无匹配时抛异常,建议用FirstOrDefault等 "OrDefault" 重载)。
csharp
运行
int first = nums.First(n => n > 10); // 第一个大于10的元素:99
int firstOrDefault = nums.FirstOrDefault(n => n > 100); // 无匹配时返回默认值(int默认0)
7. 集合操作:Distinct/Union/Intersect
Distinct:去重;Union:合并两个集合(去重);Intersect:取两个集合的交集。
csharp
运行
int[] nums2 = { 7, 15, 20 };
var distinct = nums.Distinct(); // 去重后:3、99、88、7、15
var union = nums.Union(nums2); // 合并去重:3、99、88、7、15、20
8. 连接:Join
类似 SQL 的 "内连接",关联两个集合(按共同键匹配)。
csharp
运行
// 数据源1:用户
var users = new[] { new { Id=1, Name="张三" }, new { Id=2, Name="李四" } };
// 数据源2:订单
var orders = new[] { new { UserId=1, No="O001" }, new { UserId=2, No="O002" } };
// 按UserId关联用户和订单
var userOrders = users.Join(
inner: orders,
outerKeySelector: u => u.Id, // 用户的关联键
innerKeySelector: o => o.UserId,// 订单的关联键
resultSelector: (u, o) => new { u.Name, o.No } // 结果结构
);
// 结果:{ Name="张三", No="O001" }、{ Name="李四", No="O002" }
三、查询语法(类 SQL)
与方法语法功能等价,更适合复杂查询(编译器会自动转换为方法语法)。
csharp
运行
int[] nums = { 3, 99, 88, 7, 15 };
// 查询语法:筛选>10的元素,按升序排序
var query = from n in nums
where n > 10
orderby n ascending
select n;
四、关键概念
1. var的使用
C# 的var是强类型(仅让编译器自动推断类型),在 LINQ 中常用以简化复杂类型声明:
csharp
运行
// 无需写IEnumerable<int>,编译器自动推断
var result = nums.Where(n => n > 10);
⚠️ 注意:var≠JavaScript 的弱类型,C# 的弱类型是dynamic。
2. 延迟执行
LINQ 查询(如Where/Select)默认是延迟执行 :即声明查询时不执行,直到枚举查询结果 (如foreach、ToList()、Count())时才执行。
csharp
运行
var query = nums.Where(n => n > 10); // 此时未执行
var list = query.ToList(); // 调用ToList()时才执行查询
- 优点:可拼接多个查询操作(如先 Where 再 OrderBy),最终一次执行;
- 坑点:多次枚举会多次执行查询,建议用
ToList()/ToArray()缓存结果。
3. IEnumerable<T> vs IQueryable<T>
| 类型 | 适用场景 | 执行方式 |
|---|---|---|
IEnumerable<T> |
内存集合(LINQ to Objects) | 在客户端内存中执行查询 |
IQueryable<T> |
外部数据源(如数据库) | 将查询转换为 SQL,在数据源执行 |
五、注意事项
- 必须引用
using System.Linq;,否则无法识别 LINQ 方法; - 延迟执行的查询,若数据源变化,多次枚举会得到不同结果;
- 对
IQueryable<T>(数据库查询),避免在 Lambda 中使用 "客户端方法"(如自定义函数),否则会导致 "全表扫描"; - 用
FirstOrDefault/SingleOrDefault替代First/Single,避免无匹配时抛异常。
链式调用
有一些方法需要数组或者 List 类型参数,我们可以用ToArray()方法和ToList方法把 IEnumerable<T>转换为数组和 List<T>类型。
IEnumerable<T>类型的方法可以连续调用,这叫 "链式调用"。
因为每个方法的返回值还是 IEnumerable<T>类型。
比如:获取 Id>2 的数据,然后按照 Age 分组,并且把分组按照 Age 排序,然后取出前 3 条,最后再投影取得年龄、人数、平均工资
cs
//IEnumerable<IGrouping<int, Employee>>
list.Where(e => e.Id > 2).GroupBy(e => e.Age).OrderBy(g =>g.Key).Take(3)
.Select(g => new { NL=g.Key, RS=g.Count(), PJ=g.Average(e=>e.Salary)});
Linq练习题
1、有一个用逗号分隔的表示成绩的字符串,如"61,90,100,99,18,22,38,66,80,93,55,50,89",计算这些成绩的平均值。
cs
using System;
using System.Linq; // 必须引用LINQ命名空间
namespace LinqLx1225
{
internal class Program
{
static void Main(string[] args)
{
string s = "61,90,100,99,18,22,38,66,80,93,55,50,89";
// 1. 分割字符串为数组 → 转换为int类型数组
int[] scores = s.Split(',') // 按逗号分割字符串,得到字符串数组(如["61","90"...])
.Select(x => int.Parse(x)) // 将每个字符串转换为int类型
.ToArray(); // 转换为int数组
// 2. 用LINQ的Average方法计算平均值
double average = scores.Average();
// 输出结果
Console.WriteLine($"这些成绩的平均值是:{average:F2}"); // F2表示保留2位小数
}
}
}


