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

C#中的LINQ查询语言基础

2023-04-152.3k 阅读

LINQ 概述

LINQ(Language - Integrated Query)是 .NET Framework 3.5 引入的一项创新技术,它将查询功能直接集成到 C# 语言中。在 LINQ 出现之前,从不同数据源(如数据库、XML 文件、内存集合等)检索数据需要使用不同的 API 和技术,每种数据源都有其独特的查询方式。例如,查询数据库可能需要编写 SQL 语句并使用 ADO.NET 来执行,而遍历内存中的集合则需要使用循环和条件语句。

LINQ 通过提供一种统一的、基于语言的查询语法,简化了从各种数据源获取数据的过程。无论数据源是 SQL 数据库、XML 文档还是内存中的集合,开发人员都可以使用类似的语法来编写查询。这不仅提高了代码的可读性和可维护性,还减少了学习不同查询技术的成本。

LINQ 的组成部分

  1. LINQ to Objects:用于查询内存中的集合,如 List<T>Array 等。这使得在处理内存数据时可以使用 LINQ 语法,而无需使用传统的循环和条件语句。例如:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = from num in numbers
             where num > 3
             select num;
foreach (var num in result)
{
    Console.WriteLine(num);
}

在上述代码中,我们通过 LINQ to Objects 从 List<int> 中筛选出大于 3 的数字。from 关键字指定数据源,where 进行筛选,select 确定输出结果。

  1. LINQ to SQL:专门用于查询关系型数据库(如 SQL Server)。它允许开发人员使用 LINQ 语法编写数据库查询,而不是直接编写 SQL 语句。LINQ to SQL 会自动将 LINQ 查询转换为相应的 SQL 语句并在数据库中执行。以下是一个简单示例:
using System.Data.Linq;

// 假设我们有一个数据库表对应的数据类 Customer
public class Customer
{
    public int CustomerID { get; set; }
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        DataContext db = new DataContext("connectionString");
        Table<Customer> customers = db.GetTable<Customer>();
        var result = from cust in customers
                     where cust.Name.StartsWith("A")
                     select cust;
        foreach (var cust in result)
        {
            Console.WriteLine(cust.Name);
        }
    }
}

这里,我们使用 LINQ to SQL 从数据库的 Customer 表中筛选出名字以 “A” 开头的客户。

  1. LINQ to XML:用于处理 XML 数据。它提供了一种直观的方式来查询、创建和修改 XML 文档。例如:
using System.Xml.Linq;

class Program
{
    static void Main()
    {
        XDocument doc = XDocument.Load("example.xml");
        var result = from element in doc.Descendants("book")
                     where (string)element.Element("author") == "John Smith"
                     select element.Element("title").Value;
        foreach (var title in result)
        {
            Console.WriteLine(title);
        }
    }
}

上述代码从 XML 文件中查询出作者为 “John Smith” 的书籍标题。Descendants 方法用于获取指定名称的所有后代元素,Element 方法用于获取指定名称的子元素。

LINQ 查询语法基础

查询表达式结构

一个基本的 LINQ 查询表达式由以下几个主要部分组成:

  1. 数据源:通过 from 关键字指定。例如 from num in numbers,这里 numbers 就是数据源,可以是任何实现了 IEnumerable<T> 接口的集合。
  2. 筛选条件:使用 where 关键字。它用于过滤数据源中的元素,只返回满足条件的元素。如 where num > 3,这表示只返回大于 3 的 num
  3. 选择结果:由 select 关键字确定。它指定查询最终返回的结果形式。例如 select num 表示返回经过筛选后的 num

多个查询子句

一个 LINQ 查询表达式可以包含多个子句,以实现更复杂的查询逻辑。

  1. orderby 子句:用于对查询结果进行排序。可以按升序或降序排列。例如:
List<int> numbers = new List<int> { 5, 3, 1, 4, 2 };
var result = from num in numbers
             orderby num ascending
             select num;
foreach (var num in result)
{
    Console.WriteLine(num);
}

上述代码将 numbers 列表中的数字按升序排列。如果要按降序排列,只需将 ascending 改为 descending

  1. group by 子句:用于对查询结果进行分组。例如,假设有一个包含学生成绩的列表,我们想按成绩分组:
class Student
{
    public string Name { get; set; }
    public int Score { get; set; }
}

class Program
{
    static void Main()
    {
        List<Student> students = new List<Student>
        {
            new Student { Name = "Alice", Score = 85 },
            new Student { Name = "Bob", Score = 90 },
            new Student { Name = "Charlie", Score = 85 }
        };
        var result = from student in students
                     group student by student.Score;
        foreach (var group in result)
        {
            Console.WriteLine($"Score: {group.Key}");
            foreach (var student in group)
            {
                Console.WriteLine(student.Name);
            }
        }
    }
}

在这个例子中,我们按学生的成绩对学生进行分组。group by 后面跟着用于分组的字段,这里是 student.Score。分组结果中,group.Key 表示分组的依据(即成绩),group 中包含属于该组的所有学生。

  1. join 子句:用于将两个或多个数据源中的相关数据连接起来。类似于数据库中的 JOIN 操作。例如,有两个列表,一个是订单列表,一个是客户列表,我们想获取每个订单对应的客户信息:
class Order
{
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public string OrderDetails { get; set; }
}

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

class Program
{
    static void Main()
    {
        List<Order> orders = new List<Order>
        {
            new Order { OrderID = 1, CustomerID = 101, OrderDetails = "Order 1 details" },
            new Order { OrderID = 2, CustomerID = 102, OrderDetails = "Order 2 details" }
        };
        List<Customer> customers = new List<Customer>
        {
            new Customer { CustomerID = 101, Name = "Alice" },
            new Customer { CustomerID = 102, Name = "Bob" }
        };
        var result = from order in orders
                     join customer in customers on order.CustomerID equals customer.CustomerID
                     select new { Order = order.OrderDetails, Customer = customer.Name };
        foreach (var item in result)
        {
            Console.WriteLine($"Order: {item.Order}, Customer: {item.Customer}");
        }
    }
}

在上述代码中,join 子句通过 order.CustomerIDcustomer.CustomerIDorders 列表和 customers 列表连接起来,equals 用于指定连接条件。select 部分创建了一个匿名类型,包含订单详情和对应的客户名称。

LINQ 方法语法

除了查询语法,LINQ 还提供了方法语法。方法语法使用扩展方法来实现查询操作,它在某些情况下更加灵活,特别是在链式调用多个查询操作时。

基本方法语法示例

  1. Where 方法:与查询语法中的 where 子句功能类似,用于筛选数据。例如:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = numbers.Where(num => num > 3);
foreach (var num in result)
{
    Console.WriteLine(num);
}

这里 Where 方法接收一个 lambda 表达式作为参数,该表达式定义了筛选条件。num => num > 3 表示只返回大于 3 的 num

  1. Select 方法:类似于查询语法中的 select 关键字,用于选择结果。例如:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = numbers.Select(num => num * 2);
foreach (var num in result)
{
    Console.WriteLine(num);
}

上述代码通过 Select 方法将列表中的每个数字乘以 2 并返回结果。

组合方法语法

方法语法可以很方便地组合多个查询操作。例如,同时进行筛选和排序:

List<int> numbers = new List<int> { 5, 3, 1, 4, 2 };
var result = numbers.Where(num => num > 2)
                    .OrderBy(num => num);
foreach (var num in result)
{
    Console.WriteLine(num);
}

在这个例子中,首先使用 Where 方法筛选出大于 2 的数字,然后使用 OrderBy 方法对筛选后的结果按升序排序。通过链式调用,可以在一行代码中完成复杂的查询操作。

与查询语法的对比

  1. 可读性:查询语法通常更接近 SQL 等声明式查询语言,对于熟悉数据库查询的开发人员来说,可读性更好。例如:
// 查询语法
var result1 = from num in numbers
              where num > 3
              orderby num ascending
              select num;
// 方法语法
var result2 = numbers.Where(num => num > 3)
                     .OrderBy(num => num);

在简单查询中,查询语法更直观,更易于理解。

  1. 灵活性:方法语法在处理复杂的链式操作和动态查询时更具优势。例如,在运行时根据不同条件动态选择查询操作时,方法语法更容易实现。
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
bool condition = true;
IEnumerable<int> result;
if (condition)
{
    result = numbers.Where(num => num > 3)
                    .OrderBy(num => num);
}
else
{
    result = numbers.Where(num => num < 3)
                    .OrderByDescending(num => num);
}
foreach (var num in result)
{
    Console.WriteLine(num);
}

这里根据 condition 的值动态选择不同的查询逻辑,方法语法实现起来更加简洁。

LINQ 中的类型推断和匿名类型

类型推断

在 LINQ 查询中,C# 的类型推断机制起着重要作用。var 关键字常用于 LINQ 查询结果,它允许编译器根据查询结果的实际类型来推断变量的类型。例如:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = from num in numbers
             where num > 3
             select num;
// 这里 result 的实际类型是 IEnumerable<int>,编译器根据查询结果推断得出

使用 var 可以使代码更简洁,特别是在查询结果类型比较复杂时,无需显式指定类型。

匿名类型

匿名类型是 LINQ 中经常使用的一种类型,它允许我们在查询过程中创建临时的、没有命名的类型。例如:

class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

class Program
{
    static void Main()
    {
        List<Product> products = new List<Product>
        {
            new Product { Name = "Product1", Price = 10.99m },
            new Product { Name = "Product2", Price = 15.99m }
        };
        var result = from product in products
                     select new { product.Name, product.Price };
        foreach (var item in result)
        {
            Console.WriteLine($"Name: {item.Name}, Price: {item.Price}");
        }
    }
}

在上述代码中,select new { product.Name, product.Price } 创建了一个匿名类型,该类型包含 NamePrice 两个属性。匿名类型的属性名由源对象的属性名推断而来,属性类型也由源对象的属性类型确定。匿名类型非常适合在查询中临时创建只在查询范围内使用的数据结构。

LINQ 的执行时机

延迟执行

LINQ 查询通常是延迟执行的。这意味着在定义查询表达式时,实际的查询操作并不会立即执行,而是在遍历查询结果时才会执行。例如:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = from num in numbers
             where num > 3
             select num;
// 这里 result 只是一个查询定义,并没有实际执行查询
foreach (var num in result)
{
    // 当遍历 result 时,查询才会执行
    Console.WriteLine(num);
}

延迟执行的好处在于,如果查询结果最终没有被使用,那么查询操作就不会执行,从而节省了资源。而且,如果数据源在查询定义之后发生了变化,遍历查询结果时会反映这些变化。

立即执行

有些 LINQ 方法会导致立即执行,例如 ToList()ToArray()Count() 等。这些方法会立即执行查询并返回结果。例如:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var resultList = (from num in numbers
                  where num > 3
                  select num).ToList();
// 调用 ToList() 方法会立即执行查询并将结果转换为 List<int>
int count = (from num in numbers
             where num > 3
             select num).Count();
// 调用 Count() 方法会立即执行查询并返回满足条件的元素数量

立即执行方法适用于需要获取具体数据结构或统计信息的场景。在使用立即执行方法时,要注意数据源的变化不会影响已经获取的结果,因为查询在调用这些方法时已经执行完毕。

复杂 LINQ 查询示例

多层分组和排序

假设我们有一个包含员工信息的列表,员工信息包括部门、职位和薪资。我们想按部门分组,每个部门内再按职位分组,并且每个分组内按薪资降序排列。

class Employee
{
    public string Department { get; set; }
    public string Position { get; set; }
    public decimal Salary { get; set; }
}

class Program
{
    static void Main()
    {
        List<Employee> employees = new List<Employee>
        {
            new Employee { Department = "HR", Position = "Manager", Salary = 8000m },
            new Employee { Department = "HR", Position = "Assistant", Salary = 4000m },
            new Employee { Department = "IT", Position = "Developer", Salary = 7000m },
            new Employee { Department = "IT", Position = "Manager", Salary = 9000m }
        };
        var result = from emp in employees
                     group emp by emp.Department into departmentGroup
                     select new
                     {
                         Department = departmentGroup.Key,
                         Positions = from empInDepartment in departmentGroup
                                     group empInDepartment by empInDepartment.Position into positionGroup
                                     select new
                                     {
                                         Position = positionGroup.Key,
                                         Employees = positionGroup.OrderByDescending(emp => emp.Salary)
                                     }
                     };
        foreach (var department in result)
        {
            Console.WriteLine($"Department: {department.Department}");
            foreach (var position in department.Positions)
            {
                Console.WriteLine($"  Position: {position.Position}");
                foreach (var employee in position.Employees)
                {
                    Console.WriteLine($"    Name: {employee.Salary}");
                }
            }
        }
    }
}

在这个复杂查询中,首先按部门分组,然后在每个部门组内再按职位分组,最后对每个职位组内的员工按薪资降序排列。通过多层分组和排序,我们可以对复杂的数据结构进行详细的分析和处理。

多数据源联合查询

假设有两个列表,一个是学生列表,包含学生姓名和所在班级编号,另一个是班级列表,包含班级编号和班级名称。我们想获取每个学生所在班级的名称。

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

class Class
{
    public int ClassID { get; set; }
    public string ClassName { get; set; }
}

class Program
{
    static void Main()
    {
        List<Student> students = new List<Student>
        {
            new Student { Name = "Alice", ClassID = 1 },
            new Student { Name = "Bob", ClassID = 2 }
        };
        List<Class> classes = new List<Class>
        {
            new Class { ClassID = 1, ClassName = "Class 1" },
            new Class { ClassID = 2, ClassName = "Class 2" }
        };
        var result = from student in students
                     join @class in classes on student.ClassID equals @class.ClassID
                     select new { student.Name, ClassName = @class.ClassName };
        foreach (var item in result)
        {
            Console.WriteLine($"Student: {item.Name}, Class: {item.ClassName}");
        }
    }
}

此示例展示了如何使用 join 子句将两个不同的数据源(studentsclasses)联合起来,以获取更完整的信息。通过这种联合查询,可以将相关的数据整合在一起,满足实际业务需求。

通过以上对 LINQ 查询语言基础的详细介绍,包括其基本概念、查询语法、方法语法、类型推断、执行时机以及复杂查询示例等方面,相信读者对 C# 中的 LINQ 有了较为深入的理解,能够在实际开发中灵活运用 LINQ 来高效处理各种数据查询和操作任务。