MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的LINQ查询语言基础与进阶

2023-05-044.4k 阅读

C# 中的 LINQ 查询语言基础

LINQ 简介

LINQ(Language - Integrated Query)是 .NET Framework 3.5 引入的一项重大创新,它将查询功能无缝集成到 C# 和 VB.NET 等编程语言中。在 LINQ 出现之前,对不同数据源(如数据库、XML 文档、集合等)进行查询需要使用不同的 API 和技术。例如,查询数据库可能需要使用 ADO.NET,查询 XML 文档则要使用 XPath 等。LINQ 统一了这些查询方式,为开发人员提供了一种一致的、以编程语言为基础的查询语法。

LINQ 的核心概念包括查询表达式、查询运算符和数据源。查询表达式是使用类似 SQL 的语法编写的代码片段,用于指定如何从数据源中检索和操作数据。查询运算符是实现 LINQ 功能的方法,它们可以对数据源进行筛选、排序、分组等操作。数据源可以是任何实现了 IEnumerable<T>IQueryable<T> 接口的对象,比如数组、列表、数据库表等。

LINQ 查询表达式基础语法

简单查询

假设我们有一个整数数组,想要找出所有大于 5 的数。以下是使用 LINQ 查询表达式实现的代码:

int[] numbers = { 1, 3, 7, 9, 4, 10 };
var result = from num in numbers
             where num > 5
             select num;
foreach (var num in result)
{
    Console.WriteLine(num);
}

在这段代码中,from num in numbers 表示从 numbers 数组中获取每个元素,并将其命名为 numwhere num > 5 是筛选条件,只选择大于 5 的元素。select num 表示将符合条件的元素作为结果返回。

投影

投影是指从数据源中选择特定的属性或对属性进行转换。例如,假设有一个包含学生信息的类 Student

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

我们有一个 Student 对象的列表,现在只想获取学生的名字:

List<Student> students = new List<Student>()
{
    new Student { Name = "Alice", Age = 20 },
    new Student { Name = "Bob", Age = 22 },
    new Student { Name = "Charlie", Age = 21 }
};
var names = from student in students
            select student.Name;
foreach (var name in names)
{
    Console.WriteLine(name);
}

这里使用 select student.Name 只选择了学生的名字属性,而不是整个 Student 对象。

排序

可以使用 orderby 关键字对查询结果进行排序。例如,按照学生的年龄对上述 students 列表进行升序排序:

var sortedStudents = from student in students
                     orderby student.Age
                     select student;
foreach (var student in sortedStudents)
{
    Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}

如果要进行降序排序,可以在 orderby 后加上 descending 关键字:

var sortedStudentsDesc = from student in students
                         orderby student.Age descending
                         select student;
foreach (var student in sortedStudentsDesc)
{
    Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}

连接操作

内连接

内连接用于从两个或多个数据源中选择匹配的元素。假设我们有两个列表,一个是 students 列表,另一个是包含学生成绩的 scores 列表:

public class Score
{
    public string StudentName { get; set; }
    public int ScoreValue { get; set; }
}
List<Student> students = new List<Student>()
{
    new Student { Name = "Alice", Age = 20 },
    new Student { Name = "Bob", Age = 22 },
    new Student { Name = "Charlie", Age = 21 }
};
List<Score> scores = new List<Score>()
{
    new Score { StudentName = "Alice", ScoreValue = 85 },
    new Score { StudentName = "Bob", ScoreValue = 90 },
    new Score { StudentName = "David", ScoreValue = 78 }
};
var joinedResult = from student in students
                   join score in scores on student.Name equals score.StudentName
                   select new { student.Name, score.ScoreValue };
foreach (var item in joinedResult)
{
    Console.WriteLine($"Name: {item.Name}, Score: {item.ScoreValue}");
}

在这段代码中,join score in scores on student.Name equals score.StudentName 表示根据学生名字将 students 列表和 scores 列表进行内连接。只有名字匹配的学生和成绩记录才会出现在结果中。

分组连接

分组连接用于将一个数据源中的元素按照另一个数据源中的匹配元素进行分组。例如,我们有一个 orders 列表和 customers 列表,想要按照客户对订单进行分组:

public class Customer
{
    public string Name { get; set; }
    public string City { get; set; }
}
public class Order
{
    public string CustomerName { get; set; }
    public decimal Amount { get; set; }
}
List<Customer> customers = new List<Customer>()
{
    new Customer { Name = "Alice", City = "New York" },
    new Customer { Name = "Bob", City = "Los Angeles" }
};
List<Order> orders = new List<Order>()
{
    new Order { CustomerName = "Alice", Amount = 100.0m },
    new Order { CustomerName = "Bob", Amount = 150.0m },
    new Order { CustomerName = "Alice", Amount = 200.0m }
};
var groupedResult = from customer in customers
                    join order in orders on customer.Name equals order.CustomerName into orderGroup
                    select new { customer.Name, Orders = orderGroup };
foreach (var item in groupedResult)
{
    Console.WriteLine($"Customer: {item.Name}");
    foreach (var order in item.Orders)
    {
        Console.WriteLine($"Order Amount: {order.Amount}");
    }
}

这里 join order in orders on customer.Name equals order.CustomerName into orderGroup 将订单按照客户进行分组,并将分组结果命名为 orderGroup

C# 中的 LINQ 查询语言进阶

LINQ 扩展方法

除了查询表达式语法,LINQ 还提供了扩展方法语法。扩展方法是一种特殊的静态方法,它允许在不修改现有类型的情况下为其添加新方法。LINQ 的扩展方法定义在 System.Linq.EnumerableSystem.Linq.Queryable 类中。

筛选扩展方法

Where 方法用于筛选数据源中的元素,其功能与查询表达式中的 where 关键字类似。例如:

int[] numbers = { 1, 3, 7, 9, 4, 10 };
var result = numbers.Where(num => num > 5);
foreach (var num in result)
{
    Console.WriteLine(num);
}

这里 numbers.Where(num => num > 5) 使用了扩展方法语法,num => num > 5 是一个 lambda 表达式,用于定义筛选条件。

排序扩展方法

OrderByOrderByDescending 方法用于对数据源进行排序。例如,对前面的 students 列表按照年龄升序排序:

List<Student> students = new List<Student>()
{
    new Student { Name = "Alice", Age = 20 },
    new Student { Name = "Bob", Age = 22 },
    new Student { Name = "Charlie", Age = 21 }
};
var sortedStudents = students.OrderBy(student => student.Age);
foreach (var student in sortedStudents)
{
    Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}

OrderBy(student => student.Age) 使用扩展方法对 students 列表按照年龄进行升序排序。

聚合扩展方法

聚合方法用于对数据源中的元素进行计算并返回单个值。例如,Sum 方法用于计算整数列表的总和:

int[] numbers = { 1, 3, 7, 9, 4, 10 };
int sum = numbers.Sum();
Console.WriteLine($"Sum: {sum}");

还有 Average 方法用于计算平均值,MinMax 方法用于获取最小值和最大值等。

LINQ to Objects

LINQ to Objects 是 LINQ 针对内存中的集合(如数组、列表等)的实现。由于这些集合实现了 IEnumerable<T> 接口,所以可以直接使用 LINQ 进行查询。

复杂查询示例

假设我们有一个包含员工信息的列表,员工类 Employee 定义如下:

public class Employee
{
    public string Name { get; set; }
    public string Department { get; set; }
    public decimal Salary { get; set; }
}
List<Employee> employees = new List<Employee>()
{
    new Employee { Name = "Alice", Department = "HR", Salary = 5000m },
    new Employee { Name = "Bob", Department = "IT", Salary = 6000m },
    new Employee { Name = "Charlie", Department = "HR", Salary = 5500m },
    new Employee { Name = "David", Department = "IT", Salary = 6500m }
};
// 获取每个部门的平均工资,并按平均工资降序排列
var averageSalaries = employees
   .GroupBy(emp => emp.Department)
   .Select(group => new
    {
        Department = group.Key,
        AverageSalary = group.Average(emp => emp.Salary)
    })
   .OrderByDescending(result => result.AverageSalary);
foreach (var result in averageSalaries)
{
    Console.WriteLine($"Department: {result.Department}, Average Salary: {result.AverageSalary}");
}

在这段代码中,首先使用 GroupBy 方法按部门对员工进行分组,然后使用 Select 方法计算每个部门的平均工资,最后使用 OrderByDescending 方法按平均工资降序排列。

LINQ to SQL

LINQ to SQL 是 LINQ 针对关系型数据库(如 SQL Server)的实现。它提供了一种对象关系映射(ORM)机制,允许开发人员使用 LINQ 查询数据库,而不必编写传统的 SQL 语句。

创建数据库上下文

首先,需要创建一个继承自 System.Data.Linq.DataContext 的数据库上下文类。例如,假设我们有一个名为 Northwind 的数据库,包含 Customers 表:

[Table(Name = "Customers")]
public class Customer
{
    [Column(IsPrimaryKey = true)]
    public string CustomerID { get; set; }
    [Column]
    public string CompanyName { get; set; }
    [Column]
    public string ContactName { get; set; }
}
public class NorthwindDataContext : DataContext
{
    public NorthwindDataContext(string connectionString) : base(connectionString) { }
    public Table<Customer> Customers => GetTable<Customer>();
}

这里使用 TableColumn 特性来映射数据库表和列。

执行查询

使用创建好的数据库上下文进行查询:

string connectionString = "your_connection_string";
NorthwindDataContext db = new NorthwindDataContext(connectionString);
var customers = from customer in db.Customers
                where customer.Country == "USA"
                select customer;
foreach (var customer in customers)
{
    Console.WriteLine($"Customer ID: {customer.CustomerID}, Company Name: {customer.CompanyName}");
}

这段代码从 Northwind 数据库的 Customers 表中选择来自美国的客户。

LINQ to XML

LINQ to XML 用于处理 XML 文档。它提供了一种更直观、更灵活的方式来创建、查询和修改 XML 数据,相比传统的 XML 处理方式(如 DOM 和 SAX)更具优势。

创建 XML 文档

可以使用 LINQ to XML 来创建 XML 文档。例如:

XDocument doc = new XDocument(
    new XDeclaration("1.0", "utf - 8", "yes"),
    new XElement("Books",
        new XElement("Book",
            new XElement("Title", "C# Programming"),
            new XElement("Author", "John Smith")
        ),
        new XElement("Book",
            new XElement("Title", "LINQ in Action"),
            new XElement("Author", "Jane Doe")
        )
    )
);
doc.Save("books.xml");

这段代码创建了一个包含两本书信息的 XML 文档,并保存为 books.xml

查询 XML 文档

假设我们已经有了上述的 books.xml 文件,现在要查询所有书名:

XDocument doc = XDocument.Load("books.xml");
var titles = from book in doc.Root.Elements("Book")
             select book.Element("Title").Value;
foreach (var title in titles)
{
    Console.WriteLine(title);
}

这里使用 LINQ to XML 查询表达式从 XML 文档中选择所有的书名。

延迟执行与立即执行

延迟执行

LINQ 查询通常是延迟执行的。这意味着只有在真正需要结果时(例如通过迭代结果集),查询才会实际执行。例如:

int[] numbers = { 1, 3, 7, 9, 4, 10 };
var result = numbers.Where(num => num > 5);
// 此时查询尚未执行
foreach (var num in result)
{
    Console.WriteLine(num);
    // 当迭代结果集时,查询才会执行
}

延迟执行的好处是可以避免不必要的计算,特别是在查询涉及大量数据或复杂操作时。

立即执行

有些 LINQ 方法会导致立即执行,例如聚合方法 SumAverage 等。例如:

int[] numbers = { 1, 3, 7, 9, 4, 10 };
int sum = numbers.Where(num => num > 5).Sum();
// 这里 Where 方法返回的查询结果立即被 Sum 方法执行并计算总和
Console.WriteLine($"Sum: {sum}");

了解延迟执行和立即执行的特性对于优化 LINQ 查询性能非常重要。

LINQ 性能优化

减少中间结果

尽量避免创建不必要的中间结果集。例如,在连续进行多个操作时,尽量将操作合并。假设我们有一个列表,先筛选出偶数,然后计算它们的平方和:

// 不好的方式,创建了中间结果集
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5, 6 };
var evens = numbers.Where(num => num % 2 == 0);
int sumOfSquares = evens.Select(num => num * num).Sum();

// 好的方式,避免中间结果集
int sumOfSquaresBetter = numbers
   .Where(num => num % 2 == 0)
   .Select(num => num * num)
   .Sum();

合理选择数据源和查询方式

对于内存中的小数据集,LINQ to Objects 通常能满足需求且性能较好。对于数据库查询,要注意数据库的性能特点,合理使用 LINQ to SQL 或其他数据库访问技术。例如,避免在 LINQ to SQL 查询中进行大量的客户端计算,尽量让数据库执行更多的工作。

利用索引

在数据库查询中,确保对经常用于筛选和连接的列创建索引。例如,如果在 LINQ to SQL 查询中经常根据客户名字筛选客户,那么在数据库的 Customers 表的 Name 列上创建索引可以显著提高查询性能。

常见问题与解决方案

类型不匹配问题

在 LINQ 查询中,可能会遇到类型不匹配的问题。例如,在比较不同类型的值时。假设我们有一个包含整数和字符串混合的列表,想要筛选出整数并求和:

List<object> mixedList = new List<object>() { 1, "two", 3, "four", 5 };
// 错误的方式,会导致运行时类型转换异常
// int sum = mixedList.Where(obj => obj is int).Sum((int obj) => (int)obj);
// 正确的方式
int sum = mixedList
   .Where(obj => obj is int)
   .Select(obj => (int)obj)
   .Sum();
Console.WriteLine($"Sum: {sum}");

这里先使用 Where 方法筛选出整数类型的对象,再使用 Select 方法将对象转换为整数类型,最后计算总和。

空引用异常

当数据源可能包含空值时,要注意避免空引用异常。例如,在查询包含学生成绩的列表时,有些学生可能没有成绩(成绩字段为 null):

public class StudentScore
{
    public string StudentName { get; set; }
    public int? Score { get; set; }
}
List<StudentScore> scores = new List<StudentScore>()
{
    new StudentScore { StudentName = "Alice", Score = 85 },
    new StudentScore { StudentName = "Bob", Score = null },
    new StudentScore { StudentName = "Charlie", Score = 90 }
};
// 避免空引用异常
var validScores = scores.Where(score => score.Score.HasValue).Select(score => score.Score.Value);
int sumOfScores = validScores.Sum();
Console.WriteLine($"Sum of valid scores: {sumOfScores}");

这里使用 score.Score.HasValue 来判断成绩是否为空,避免了空引用异常。

通过掌握 LINQ 的基础和进阶知识,开发人员能够更高效地处理各种数据源,写出简洁、可读性强且性能良好的代码。无论是处理内存中的集合、数据库数据还是 XML 文档,LINQ 都提供了强大而统一的查询方式。在实际开发中,不断实践和优化 LINQ 查询,将有助于提升应用程序的整体质量和性能。