C#中的LINQ查询语言基础
LINQ 概述
LINQ(Language - Integrated Query)是 .NET Framework 3.5 引入的一项创新技术,它将查询功能直接集成到 C# 语言中。在 LINQ 出现之前,从不同数据源(如数据库、XML 文件、内存集合等)检索数据需要使用不同的 API 和技术,每种数据源都有其独特的查询方式。例如,查询数据库可能需要编写 SQL 语句并使用 ADO.NET 来执行,而遍历内存中的集合则需要使用循环和条件语句。
LINQ 通过提供一种统一的、基于语言的查询语法,简化了从各种数据源获取数据的过程。无论数据源是 SQL 数据库、XML 文档还是内存中的集合,开发人员都可以使用类似的语法来编写查询。这不仅提高了代码的可读性和可维护性,还减少了学习不同查询技术的成本。
LINQ 的组成部分
- 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
确定输出结果。
- 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” 开头的客户。
- 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 查询表达式由以下几个主要部分组成:
- 数据源:通过
from
关键字指定。例如from num in numbers
,这里numbers
就是数据源,可以是任何实现了IEnumerable<T>
接口的集合。 - 筛选条件:使用
where
关键字。它用于过滤数据源中的元素,只返回满足条件的元素。如where num > 3
,这表示只返回大于 3 的num
。 - 选择结果:由
select
关键字确定。它指定查询最终返回的结果形式。例如select num
表示返回经过筛选后的num
。
多个查询子句
一个 LINQ 查询表达式可以包含多个子句,以实现更复杂的查询逻辑。
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
。
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
中包含属于该组的所有学生。
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.CustomerID
和 customer.CustomerID
将 orders
列表和 customers
列表连接起来,equals
用于指定连接条件。select
部分创建了一个匿名类型,包含订单详情和对应的客户名称。
LINQ 方法语法
除了查询语法,LINQ 还提供了方法语法。方法语法使用扩展方法来实现查询操作,它在某些情况下更加灵活,特别是在链式调用多个查询操作时。
基本方法语法示例
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
。
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
方法对筛选后的结果按升序排序。通过链式调用,可以在一行代码中完成复杂的查询操作。
与查询语法的对比
- 可读性:查询语法通常更接近 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);
在简单查询中,查询语法更直观,更易于理解。
- 灵活性:方法语法在处理复杂的链式操作和动态查询时更具优势。例如,在运行时根据不同条件动态选择查询操作时,方法语法更容易实现。
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 }
创建了一个匿名类型,该类型包含 Name
和 Price
两个属性。匿名类型的属性名由源对象的属性名推断而来,属性类型也由源对象的属性类型确定。匿名类型非常适合在查询中临时创建只在查询范围内使用的数据结构。
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
子句将两个不同的数据源(students
和 classes
)联合起来,以获取更完整的信息。通过这种联合查询,可以将相关的数据整合在一起,满足实际业务需求。
通过以上对 LINQ 查询语言基础的详细介绍,包括其基本概念、查询语法、方法语法、类型推断、执行时机以及复杂查询示例等方面,相信读者对 C# 中的 LINQ 有了较为深入的理解,能够在实际开发中灵活运用 LINQ 来高效处理各种数据查询和操作任务。