.NET1-异步方法、LINQ

.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 程序必需的 "全套工具包",核心包含:

  1. 编译器
  2. 项目模板
  3. 命令行工具(dotnet CLI)
  4. 基础类库
  5. .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。

  1. 调用泛型方法时,一般在方法前加上 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

  1. async的本质 :编译器会为async方法生成「状态机类」(记录await的暂停 / 恢复),即使仅单纯转发,也会额外生成状态机代码,增加微小的性能开销;
  2. 去掉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.WhenAllTask.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表达式

  1. 系统能根据参数数据类型推断出参数的类型,因此可以省略委托类型,用=>引出来方法体。
  2. 如果委托没有返回值,且方法体只有一行代码,可省略{}
  3. 如果=>之后的方法体中只有一行代码,且方法有返回值,那么可以省略方法体的{}以及return
  4. 如果只有一个参数,参数的()可以省略。

根据这些可以得到:

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 核心前提
  1. 数据源要求 :需实现IEnumerable<T>(内存集合,如数组、List)或IQueryable<T>(外部数据源,如数据库);
  2. 命名空间 :必须引用using System.Linq;
  3. 语法形式 :支持两种写法(功能等价,按需选择):
    • 查询语法:类 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)默认是延迟执行 :即声明查询时不执行,直到枚举查询结果 (如foreachToList()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,在数据源执行
五、注意事项
  1. 必须引用using System.Linq;,否则无法识别 LINQ 方法;
  2. 延迟执行的查询,若数据源变化,多次枚举会得到不同结果;
  3. IQueryable<T>(数据库查询),避免在 Lambda 中使用 "客户端方法"(如自定义函数),否则会导致 "全表扫描";
  4. 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位小数
        }
    }
}
相关推荐
西岸行者2 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意2 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码3 天前
嵌入式学习路线
学习
毛小茛3 天前
计算机系统概论——校验码
学习
babe小鑫3 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms3 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下3 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。3 天前
2026.2.25监控学习
学习
im_AMBER3 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J3 天前
从“Hello World“ 开始 C++
c语言·c++·学习