LINQ详解

LINQ是什么

LINQ (Language Integrated Query) 是微软在 .NET Framework 3.5 中引入的一种功能,它允许开发人员使用类似于SQL的语法来查询内存中的数据集合。LINQ 不仅限于查询数据库,还可以用来查询任何数据源,包括 XML 文档、ADO.NET 数据集、对象集合等。LINQ 的主要优点在于其简洁的语法和与 C# 的深度集成,使得数据查询更加直观和高效。

LINQ 的常用用法

1. 查询语法和方法语法

LINQ 提供了两种查询方式:

查询语法:使用类似 SQL 的语句结构。

方法语法:使用一系列扩展方法链式调用来构建查询。

2. 基本查询操作

Select: 投影元素。

LINQ(Language Integrated Query)的Select方法是一种强大的功能,用于从数据源(如集合、数组或其他可枚举的数据结构)中投影出新的数据集。Select允许你将每个元素转换成一个新的形式,这通常涉及到某种形式的映射或转换逻辑。


基本用法

在C#中,Select方法通常被调用在一个可枚举对象上,它接受一个匿名函数作为参数,这个函数定义了如何转换集合中的每个元素。例如:

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(n => n * n).ToList();

 在这个例子中,Select接收一个lambda表达式n => n * n,它将列表中的每个数字平方。ToList()方法则将结果转换为一个新的List<int>。


更复杂的用法

Select也可以用于创建更复杂的数据结构,比如从一组对象中提取特定属性,或者创建新的匿名类型:

var customers = new List<Customer>
{
    new Customer { Name = "Alice", Age = 30 },
    new Customer { Name = "Bob", Age = 25 }
};

var customerNames = customers.Select(c => c.Name).ToList();

// 或者创建一个匿名类型
var nameAgePairs = customers.Select(c => new { Name = c.Name, Age = c.Age }).ToList();
注意事项

延迟执行:Select操作是延迟执行的,这意味着直到你迭代结果或调用如ToList(), ToArray(), First(), Count()等终结操作时,转换才会实际发生。
性能考虑:对于大型数据集,连续的Select操作可能会导致性能问题,因为每次Select都会创建一个新的枚举器。在可能的情况下,尽量减少嵌套的Select调用。

Where: 过滤元素。

Where 是 LINQ 中的一个关键方法,用于根据指定的条件筛选数据集合。它允许你过滤掉不满足条件的元素,只保留那些符合条件的元素。Where 方法通常与 Select 结合使用,以实现数据的筛选和转换。


基本用法

在 C# 中,Where 方法接受一个谓词(即一个返回布尔值的函数)作为参数,用来判断集合中的每个元素是否应该被包含在最终的结果集中。例如:

var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();

 在这个例子中,Where 方法接收一个 lambda 表达式 n => n % 2 == 0,它检查每个数字是否为偶数。结果 evenNumbers 将只包含原列表中的偶数。


复杂用法

Where 方法可以用于更复杂的条件判断,例如在对象集合中筛选:

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var students = new List<Student>
{
    new Student { Name = "Alice", Age = 20 },
    new Student { Name = "Bob", Age = 22 },
    new Student { Name = "Charlie", Age = 19 }
};

var adultStudents = students.Where(s => s.Age >= 21).ToList();

 在这个例子中,Where 方法筛选出年龄大于等于 21 岁的学生。


注意事项

延迟执行:和 Select 类似,Where 操作也是延迟执行的,意味着它不会立即执行,直到调用如 ToList(), ToArray(), Count() 等终结操作时才真正运行。
性能:连续使用多个 Where 可能会影响性能,因为每次调用都会创建一个新的枚举器。如果可能,应尽量合并条件到单个 Where 谓词中。

OrderBy / OrderByDescending: 对元素排序。

OrderBy 和 OrderByDescending 是 LINQ 中用于对数据集合进行排序的两个重要方法。它们允许你根据一个或多个键来对集合中的元素进行升序或降序排列。


OrderBy

OrderBy 方法按照指定的键对集合进行升序排序。它接受一个 lambda 表达式作为参数,该表达式定义了用于排序的键。


基本用法
var numbers = new List<int> { 5, 3, 7, 1, 2 };
var sortedNumbers = numbers.OrderBy(n => n).ToList();
// sortedNumbers 现在是 { 1, 2, 3, 5, 7 }
对象排序

对于对象集合,你可以选择对象的某个属性作为排序键:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var people = new List<Person>
{
    new Person { Name = "Charlie", Age = 30 },
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bob", Age = 35 }
};

var sortedPeople = people.OrderBy(p => p.Age).ToList();
// sortedPeople 将按年龄从小到大排序

OrderByDescending

OrderByDescending 方法与 OrderBy 类似,但是它按照指定的键进行降序排序。

基本用法
var numbers = new List<int> { 5, 3, 7, 1, 2 };
var sortedNumbers = numbers.OrderByDescending(n => n).ToList();
// sortedNumbers 现在是 { 7, 5, 3, 2, 1 }
对象排序

同样,对于对象集合,你可以选择对象的某个属性作为排序键:

var people = new List<Person>
{
    new Person { Name = "Charlie", Age = 30 },
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bob", Age = 35 }
};

var sortedPeople = people.OrderByDescending(p => p.Age).ToList();
// sortedPeople 将按年龄从大到小排序
链接多个排序规则

你可以链接多个 OrderBy 或 ThenBy(以及它们的降序版本)来实现多列排序:

var people = new List<Person>
{
    new Person { Name = "Charlie", Age = 30 },
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bob", Age = 30 }
};

var sortedPeople = people.OrderBy(p => p.Age)
                         .ThenBy(p => p.Name)
                         .ToList();

在这个例子中,首先按年龄排序,然后在相同年龄的人中按名字排序。

注意事项

延迟执行:OrderBy 和 OrderByDescending 也遵循 LINQ 的延迟执行原则,直到调用终结操作(如 ToList() 或 ToArray())才会执行排序。
性能:排序操作可能比其他 LINQ 操作更耗时,尤其是在大数据集上。因此,在处理大量数据时,优化排序逻辑是很重要的。

GroupBy: 将元素分组。

GroupBy 是 LINQ 中一个非常强大的方法,用于将数据集合按照某个键进行分组。它可以让你基于一个或多个属性或计算结果将数据分类,从而实现更高级的数据分析和处理。


基本用法

GroupBy 方法接受一个函数作为参数,这个函数用于确定每个元素所属的组。函数的输出通常是元素的一个属性或基于元素计算得到的值。例如

var fruits = new List<string> { "Apple", "Banana", "Cherry", "Apricot", "Blueberry" };
var groupedFruits = fruits.GroupBy(f => f[0]).ToList();

foreach (var group in groupedFruits)
{
    Console.WriteLine($"Fruits starting with {group.Key}:");
    foreach (var fruit in group)
    {
        Console.WriteLine(fruit);
    }
}

在这个例子中,GroupBy 根据水果名称的第一个字母进行分组。

分组后的结果

GroupBy 返回的是一个 IGrouping<TKey, TElement> 集合,其中 TKey 是分组键的类型,TElement 是原始集合中元素的类型。每个 IGrouping 实例代表一个组,可以通过 Key 属性访问分组键,通过迭代 IGrouping 实例本身来访问组内的元素。

使用匿名类型进行分组

你还可以使用匿名类型来定义更复杂的分组键:

public class Employee
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Department { get; set; }
}

var employees = new List<Employee>
{
    new Employee { Name = "Alice", Age = 25, Department = "HR" },
    new Employee { Name = "Bob", Age = 30, Department = "IT" },
    new Employee { Name = "Charlie", Age = 25, Department = "HR" }
};

var groupedEmployees = employees.GroupBy(e => new { e.Age, e.Department });

foreach (var group in groupedEmployees)
{
    Console.WriteLine($"Age: {group.Key.Age}, Department: {group.Key.Department}");
    foreach (var employee in group)
    {
        Console.WriteLine(employee.Name);
    }
}

在这个例子中,员工们被按照年龄和部门进行分组。


注意事项

延迟执行:GroupBy 也是延迟执行的,这意味着实际的分组操作在调用终结操作(如 ToList() 或 Count())时才执行。
性能:GroupBy 在大数据集上的性能可能会受到很大影响,因为它需要创建额外的内存结构来存储分组信息。

Join: 连接多个序列。

Join 是 LINQ 中用于连接两个序列的高级方法,类似于 SQL 中的 JOIN 操作。它允许你基于两个序列中元素的共同属性或键来组合数据,产生一个新的序列,其中包含了来自两个序列的匹配项。


基本用法

Join 方法的基本语法如下:

from outer in outerSequence
join inner in innerSequence on outerKeySelector equals innerKeySelector
select new { Outer = outer, Inner = inner };

或者使用方法语法:

outerSequence.Join(
    innerSequence,
    outer => outerKeySelector(outer),
    inner => innerKeySelector(inner),
    (outer, inner) => new { Outer = outer, Inner = inner });

这里,outerSequence 和 innerSequence 是要连接的两个序列;outerKeySelector 和 innerKeySelector 是用于选择每个序列中元素的键的函数;最后的 lambda 表达式定义了如何组合匹配的元素。


示例

假设我们有两个集合,一个是 Customers,另一个是 Orders,我们想要找出每个客户的所有订单:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
}

var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "Alice" },
    new Customer { Id = 2, Name = "Bob" }
};

var orders = new List<Order>
{
    new Order { Id = 101, CustomerId = 1 },
    new Order { Id = 102, CustomerId = 2 },
    new Order { Id = 103, CustomerId = 1 }
};

var joinedData = from c in customers
                 join o in orders on c.Id equals o.CustomerId
                 select new { Customer = c, Order = o };

foreach (var item in joinedData)
{
    Console.WriteLine($"Customer: {item.Customer.Name}, Order ID: {item.Order.Id}");
}

或者使用方法语法:

var joinedData = customers.Join(
    orders,
    c => c.Id,
    o => o.CustomerId,
    (c, o) => new { Customer = c, Order = o });
处理多个匹配项

当一个外键在内序列中有多个匹配项时,Join 返回的将是包含多个内序列元素的 IEnumerable<TInner>。为了处理这种情况,你可以使用 GroupJoin 方法,或者在 Join 后面加上 SelectMany 来扁平化结果。


注意事项

性能:Join 操作可能比其他 LINQ 方法更耗性能,特别是在处理大型数据集时。确保你的键选择器高效,并且在可能的情况下优化数据结构。
延迟执行:和大多数 LINQ 操作一样,Join 也是延迟执行的,直到调用终结操作(如 ToList() 或 Count())时才执行。

Any / All: 测试所有或任意元素是否满足条件。

Any 和 All 是 LINQ 中用于检查集合中元素是否满足某些条件的查询操作符。它们分别用于确定集合中是否存在至少一个元素满足给定条件(Any),以及集合中所有元素是否都满足给定条件(All)。


Any

Any 方法用于检查集合中是否存在至少一个元素满足指定的条件。如果没有提供条件,则Any会检查集合是否为空。

用法示例
var numbers = new List<int> { 1, 2, 3, 4, 5 };

bool containsEven = numbers.Any(n => n % 2 == 0); // true
bool isNotEmpty = numbers.Any(); // true

All

All 方法用于检查集合中所有元素是否都满足指定的条件。如果集合为空,All 默认返回 true,因为没有元素违反条件。


用法示例
var numbers = new List<int> { 2, 4, 6, 8 };

bool allEven = numbers.All(n => n % 2 == 0); // true
bool allPositive = numbers.All(n => n > 0); // true
使用场景

验证数据完整性:在数据处理前,使用 All 和 Any 检查数据是否符合预期的状态或格式。
逻辑判断:在业务逻辑中,基于集合中元素的特性做出决策。
错误处理:在处理数据前,确认数据集中不存在错误或异常情况。


注意事项

性能考量:Any 和 All 方法会在找到第一个不满足条件的元素时停止遍历(短路行为)。因此,对于大的数据集,这两个方法通常比遍历整个集合更高效。
延迟执行:和其它 LINQ 方法一样,Any 和 All 也是延迟执行的,这意味着它们在被调用时才会开始遍历集合。

Count / Sum / Average / Min / Max: 计算聚合值。

Count, Sum, Average, Min, 和 Max 是 LINQ 中用于计算集合中元素的聚合值的一系列方法。这些方法提供了快速且有效的方式来进行数据汇总,是数据分析和统计计算的基础。


Count

Count 方法用于计算集合中满足特定条件的元素数量。如果没有提供条件,则计算集合中所有元素的数量。

示例
var numbers = new List<int> { 1, 2, 3, 4, 5 };
int evenCount = numbers.Count(n => n % 2 == 0); // 2
int totalCount = numbers.Count(); // 5

Sum

Sum 方法用于计算集合中所有元素的总和,或者满足特定条件的元素的总和。

示例
var numbers = new List<int> { 1, 2, 3, 4, 5 };
int totalSum = numbers.Sum(); // 15
int evenSum = numbers.Where(n => n % 2 == 0).Sum(); // 6

Average

Average 方法用于计算集合中所有元素的平均值,或者满足特定条件的元素的平均值。

示例
var numbers = new List<double> { 1.0, 2.0, 3.0, 4.0, 5.0 };
double average = numbers.Average(); // 3.0
double evenAverage = numbers.Where(n => n % 2 == 0).Average(); // 3.0

Min 和 Max

Min 和 Max 方法分别用于查找集合中最小和最大的元素,或者满足特定条件的最小和最大元素。

示例
var numbers = new List<int> { 1, 2, 3, 4, 5 };
int min = numbers.Min(); // 1
int max = numbers.Max(); // 5
int evenMin = numbers.Where(n => n % 2 == 0).Min(); // 2
int evenMax = numbers.Where(n => n % 2 == 0).Max(); // 4
 使用场景

数据汇总:在数据分析中,计算总数、平均值、最小值和最大值是常见的需求。
业务逻辑:在开发应用程序时,这些方法常用于计算指标或进行阈值检查。
性能监控:在系统监控和日志分析中,聚合值用于识别趋势或异常。

注意事项

空值处理:在处理可能包含 null 值的集合时,使用 ?? 运算符或 Nullable 类型的方法来避免 NullReferenceException。
类型兼容性:确保集合中的元素类型与方法兼容,例如 Sum 和 Average 应用于数值类型。

3. 常用的 LINQ 方法

FirstOrDefault / LastOrDefault:

返回第一个或最后一个元素,或者满足条件的第一个或最后一个元素。

FirstOrDefault


功能:

FirstOrDefault 方法返回集合中的第一个元素,或者满足给定条件的第一个元素(如果提供了条件)。如果集合为空或没有元素满足条件,则返回默认值(对于引用类型来说通常是 null)。

语法:
 var firstItem = collection.FirstOrDefault();
  var firstItemByCondition = collection.FirstOrDefault(item => item.Property == value);

LastOrDefault

功能:

LastOrDefault 方法与 FirstOrDefault 类似,但是它返回的是集合中的最后一个元素,或者满足给定条件的最后一个元素。如果集合为空或没有元素满足条件,同样返回默认值。

语法:
  var lastItem = collection.LastOrDefault();
  var lastItemByCondition = collection.LastOrDefault(item => item.Property == value);
  
注意事项

这些方法都是延迟执行的,即它们在被调用时不会立即执行查询,而是在结果被枚举时才执行。
如果你确定集合中至少有一个元素满足条件,可以使用 First 或 Last 方法,它们的行为类似,但在找不到元素时会抛出 InvalidOperationException 异常。
对于可空类型的值,FirstOrDefault 和 LastOrDefault 返回的默认值是 null;对于数值类型如 int,默认值是 0。

示例

假设我们有一个 List<int> 集合:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 获取第一个元素
int? firstNumber = numbers.FirstOrDefault();

// 获取第一个偶数元素,如果没有则返回 null
int? firstEvenNumber = numbers.FirstOrDefault(n => n % 2 == 0);

// 获取最后一个元素
int? lastNumber = numbers.LastOrDefault();

// 获取最后一个偶数元素,如果没有则返回 null
int? lastEvenNumber = numbers.LastOrDefault(n => n % 2 == 0);

SingleOrDefault: 返回唯一元素或默认值。

SingleOrDefault 是 LINQ 中的一个扩展方法,用于从序列中获取唯一匹配指定条件的元素,或者当序列为空时返回默认值。这个方法结合了 First 和 FirstOrDefault 的特性,但增加了对结果唯一性的检查。


功能

SingleOrDefault 返回

序列中满足条件的唯一元素,如果序列为空或没有元素满足条件,则返回默认值。
如果找到一个以上的元素满足条件,SingleOrDefault 将抛出 InvalidOperationException 异常。

语法
var singleItem = collection.SingleOrDefault();
var singleItemByCondition = collection.SingleOrDefault(item => item.Property == value);
使用场景

SingleOrDefault 最适用于以下情况:
当你期望序列中有零个或一个元素满足条件时。
当你想要避免在序列中有多个元素满足条件的情况,从而确保数据的一致性和正确性。

示例

假设我们有一个 List<Person> 集合,其中 Person 类有 Name 和 Age 属性:

List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 }
};

// 获取年龄为 30 的人,如果没有则返回 null
Person personWithAge30 = people.SingleOrDefault(p => p.Age == 30);

// 尝试获取年龄为 25 和 30 的人,这将抛出异常
try
{
    Person multipleAges = people.SingleOrDefault(p => p.Age == 25 || p.Age == 30);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine("Multiple items found: " + ex.Message);
}

ElementAt / ElementAtOrDefault: 返回指定索引处的元素。

Concat: 合并两个序列。

Concat 是 LINQ 中的一个扩展方法,用于将两个或多个序列合并成一个新的序列。这对于需要将多个数据源组合在一起进行处理的场景非常有用。

功能

Concat 方法接收一个或多个序列作为参数,并返回一个新的序列,该序列包含了所有输入序列中的元素,按照它们在原序列中的顺序排列。

语法
IEnumerable<T> concatenatedSequence = sequence1.Concat(sequence2);
IEnumerable<T> concatenatedSequence = sequence1.Concat(sequence2).Concat(sequence3);

 使用场景

合并多个数据源: 当你需要从多个数据库表、文件或其他数据源读取数据并将其合并到一个序列中进行处理时。
组合不同类型的序列: 只要序列中的元素类型相同,你可以使用 Concat 来合并不同类型的序列,例如列表、数组或任何实现了 IEnumerable<T> 接口的对象。
动态构建序列: 在某些情况下,你可能需要根据运行时的条件动态地添加元素到序列中,Concat 可以帮助你轻松实现这一点。


示例

假设我们有两个 List<int> 集合:

List<int> list1 = new List<int> { 1, 2, 3 };
List<int> list2 = new List<int> { 4, 5, 6 };

// 使用 Concat 合并两个列表
IEnumerable<int> combinedList = list1.Concat(list2);

// 输出合并后的序列
foreach (int number in combinedList)
{
    Console.WriteLine(number);
}
// 输出: 1, 2, 3, 4, 5, 6

 

Distinct: 移除重复元素。

Distinct 是 LINQ 中的一个扩展方法,用于从序列中删除重复的元素,只保留唯一的元素。这对于数据清洗、去重以及确保数据集中的元素唯一性非常有用。

功能

Distinct 方法通过比较序列中元素的相等性来过滤掉重复项。默认情况下,它使用对象的 Equals 方法来判断元素是否相等。对于自定义类型,你可能需要重写 Equals 方法或提供一个自定义的 IEqualityComparer<T> 实现来改变元素的比较方式。

语法
IEnumerable<T> distinctSequence = source.Distinct();
使用场景

数据去重: 当你从数据库、文件或其他数据源读取数据时,可能会遇到重复的记录。使用 Distinct 可以确保最终数据集中不会有重复的元素。
优化性能: 在处理大量数据时,去除重复元素可以减少内存占用和后续处理的时间。
数据验证: 确保数据集中的元素是唯一的,这对于一些业务逻辑(如用户ID、订单号等)至关重要。

示例

假设我们有一个包含重复元素的 List<int> 集合: 

List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 4, 5 };

// 使用 Distinct 去除重复元素
IEnumerable<int> uniqueNumbers = numbers.Distinct();

// 输出去重后的序列
foreach (int number in uniqueNumbers)
{
    Console.WriteLine(number);
}
// 输出: 1, 2, 3, 4, 5

对于自定义类型,例如 Person 类,如果需要基于某个属性(如 Name)去重,可以这样做:

List<Person> people = new List<Person>
{
    new Person { Name = "Alice" },
    new Person { Name = "Bob" },
    new Person { Name = "Alice" }
};

// 使用自定义比较器去重
var comparer = EqualityComparer<Person>.Default;
var uniquePeople = people.Distinct(comparer);

// 或者使用匿名函数
var uniquePeopleByNames = people.Distinct(person => person.Name);

Reverse: 反转序列。

Skip / Take: 跳过或获取序列的一部分。

ToArray / ToList: 将序列转换为数组或列表。

未完~待续~

上一篇:如何在 Vue 项目中优雅地使用图标


下一篇:重庆交通大学数学与统计学院携手泰迪智能科技共建的“智能工作室”