在C#中处理数组、List或者自定义对象集合时,如果每次都手写foreach加if条件,代码会变得冗长,筛选逻辑一多还容易出错。LINQ提供了一种声明式的数据查询方式,开发者只需描述“要什么数据”,而不必关心“怎么遍历和判断”。本文围绕LINQ查询内存集合数据的常用写法展开,帮助读者快速上手。

一、LINQ的两种语法:查询表达式与方法语法
LINQ在C#中有两种等价的书写方式。查询表达式使用from、where、orderby、select等关键字,形式上接近SQL;方法语法则直接调用Where、Select、OrderBy等扩展方法,并通过链式调用组合多个操作。两种写法最终都会转换成方法调用,因此底层行为完全一致。
先看一个针对整数集合的简单例子。下面的代码分别用两种语法筛选大于8的数字,并按降序输出。使用LINQ前需要引入System.Linq命名空间,否则扩展方法无法使用。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 3, 10, 7, 20, 5, 15 };
// 查询表达式写法
var queryResult = from n in numbers
where n > 8
orderby n descending
select n;
// 方法语法写法
var methodResult = numbers.Where(n => n > 8).OrderByDescending(n => n);
Console.WriteLine("查询表达式结果:");
foreach (var item in queryResult)
{
Console.WriteLine(item);
}
Console.WriteLine("方法语法结果:");
foreach (var item in methodResult)
{
Console.WriteLine(item);
}
}
}
查询表达式在涉及join、group等复杂逻辑时,结构更加直观;方法语法则更容易与链式API配合,也便于动态拼接查询条件。对于入门阶段,建议先把方法语法练熟,因为它在实际项目中出现的频率更高。LINQ可以作用于任何实现了IEnumerable<T>接口的对象,数组、List<T>、字典的值集合等都属于这一类。
二、核心操作符:Where、Select和OrderBy
Where是最常用的筛选操作符,它接收一个Func<T, bool>委托,只为返回true的元素放行。下面以商品列表为例,筛选价格大于200的商品。先定义一个简单的Product类。
using System;
using System.Collections.Generic;
using System.Linq;
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
class Program
{
static void Main()
{
List<Product> products = new List<Product>
{
new Product { Id = 1, Name = "机械键盘", Price = 399m, Category = "外设" },
new Product { Id = 2, Name = "显示器", Price = 1299m, Category = "显示设备" },
new Product { Id = 3, Name = "鼠标", Price = 199m, Category = "外设" },
new Product { Id = 4, Name = "USB集线器", Price = 89m, Category = "配件" }
};
var expensive = products.Where(p => p.Price > 200).ToList();
foreach (var product in expensive)
{
Console.WriteLine($"{product.Name} - {product.Price}");
}
}
}
Select用于投影,它可以把集合中的元素转换成另一种形状。例如只想获取商品名称和价格,可以返回一个匿名类型,避免创建多余的DTO类。OrderBy与OrderByDescending用于排序,如果存在多个排序条件,可以用ThenBy或ThenByDescending继续追加。
var namesAndPrices = products
.Select(p => new { p.Name, p.Price });
var sortedProducts = products
.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price);
foreach (var item in sortedProducts)
{
Console.WriteLine($"{item.Category} - {item.Name} - {item.Price}");
}
这些操作符返回的是IEnumerable<T>,并且大部分都具有延迟执行的特点。也就是说,定义查询时不会立刻遍历集合,只有真正需要结果时才开始计算。这样做的好处是可以把多次操作合并成一次遍历,坏处是如果多次枚举同一个查询,每次都会重新执行逻辑。若希望保存一次查询结果,应在末尾调用ToList或ToArray。
三、GroupBy分组与聚合统计
GroupBy可以按照某个键把集合划分成多个分组。比如将商品按类目分组,观察每个类别下有哪些商品。分组后每一个组的Key就是分组键,组内元素仍可继续遍历。
var groups = products.GroupBy(p => p.Category);
foreach (var group in groups)
{
Console.WriteLine($"分类:{group.Key}");
foreach (var product in group)
{
Console.WriteLine($" {product.Name}");
}
}
聚合操作符用来做统计计算。Count统计数量,Sum求和,Average求平均值,Min和Max取最小值和最大值。它们会立即执行查询,而不是返回延迟序列。例如统计所有商品的平均价格、最贵商品价格和商品总数,可以写成下面这样。
int totalCount = products.Count();
decimal averagePrice = products.Average(p => p.Price);
decimal maxPrice = products.Max(p => p.Price);
Console.WriteLine($"总数:{totalCount}");
Console.WriteLine($"平均价格:{averagePrice:F2}");
Console.WriteLine($"最高价格:{maxPrice}");
此外,Any用于判断集合中是否存在满足条件的元素,All检查是否全部满足条件,Contains判断是否包含指定元素。这些量词操作在编写业务校验逻辑时非常实用。比如判断是否存在价格大于1000的商品,只需products.Any(p => p.Price > 1000),比手写循环直观得多。延迟执行与立即执行的区分在实际排错中也很重要:如果在调试时查看查询结果,记得先调用ToList,否则可能看到的是查询表达式本身,而不是数据。
四、组合使用与常见注意事项
把筛选、排序和投影串在一起,就能形成一条清晰的查询管道。下面这段代码从商品列表中筛选出外设类目且价格超过100元的商品,按价格降序排列,最后只投影名称和价格。整个流程没有手写任何foreach,可读性明显更好。
var result = products
.Where(p => p.Category == "外设" && p.Price > 100)
.OrderByDescending(p => p.Price)
.Select(p => new { p.Name, p.Price });
foreach (var item in result)
{
Console.WriteLine($"{item.Name} 售价 {item.Price} 元");
}
实际使用中还要注意空集合和不存在元素的情况。First和Single在找不到匹配项时会抛出异常,若不能保证元素一定存在,应当优先使用FirstOrDefault和SingleOrDefault。下面的例子先判断返回结果是否为null,再输出名称,避免空引用异常。
var first = products.FirstOrDefault(p => p.Category == "外设");
if (first != null)
{
Console.WriteLine(first.Name);
}
另一个容易忽略的问题是源集合为null。如果传入的集合本身是null,调用Where等扩展方法会直接抛出ArgumentNullException。在方法入口处应该先做判空处理,或者使用空集合作为默认值。总体而言,LINQ的价值不只是让代码变短,更重要的是把循环思维转换为查询思维。当需要处理的数据筛选条件越来越多时,这种声明式写法能显著降低维护成本。
掌握了上述基础用法后,可以继续了解Join连接多个集合、SelectMany展开嵌套集合,以及IQueryable在数据库查询中的差异。对于内存集合数据,LINQ to Objects已经能覆盖大部分日常开发场景,熟练运用查询操作符会对提升开发效率很有帮助。