第二十一章、使用查询表达式来查询内存中的数据
什么是语言集成查询(LINQ)
对从应用程序代码中查询数据的机制进行了“抽象”。这个功能称为“语言集成查询”(Language Integrated Query)。
LINQ的设计者大量借鉴了关系数据库管理系统(例如Microsoft SQL Server)的处理方式,将“数据库查询语句”与“数据在数据库中的内部格式”分隔开。LINQ的语法和语义和SQL很像,具有许多相同的优势。要查询的数据的内部结构发生改变后,不必修改查询代码。注意,虽然LINQ和SQL看起来很像,但LINQ更加灵活,而且能处理范围更大的逻辑数据结构。
LINQ要求数据用实现了IEnumerable或IEnumerable接口的数据结构进行存储。具体使用什么数据结构不重要。可以是数组、HashSet、Queue或者其他任何集合类型(甚至可自己定义)。唯一的要求就是这种类型是“可枚举”的。
客户信息
地址信息
假定客户和地址信息存储在如下的customers和addresses数组中。
var customers = new[] {
new {CustomerID = 1,FirstName = "Kim",LastName = "Abercrombie",CompanyName = "Alpine Ski House"},
new {CustomerID = 2,FirstName = "Jeff",LastName = "Hay",CompanyName = "Coho Winery"},
new {CustomerID = 3,FirstName = "Charlie",LastName = "Herb",CompanyName = "Alpine Ski House"},
new {CustomerID = 4,FirstName = "Chris",LastName = "Preston",CompanyName = "Trey Research"},
new {CustomerID = 5,FirstName = "Dave",LastName = "Barnett",CompanyName = "Wingtip Toys"},
new {CustomerID = 6,FirstName = "Ann",LastName = "Beebe",CompanyName = "Coho Winery"},
new {CustomerID = 7,FirstName = "John",LastName = "Kane",CompanyName = "Wingtip Toys"},
new {CustomerID = 8,FirstName = "David",LastName = "Simpson",CompanyName = "Trey Research"},
new {CustomerID = 9,FirstName = "Greg",LastName = "Chapman",CompanyName = "Wingtip Toys"},
new {CustomerID = 10,FirstName = "Tim",LastName = "Litton",CompanyName = "Wide World Importers"},
};
var addresses = new[] {
new {CompanyName = "Alpine Ski House", City = "Berne", Country = "Switzerland"},
new {CompanyName = "Coho Winery", City = "San Francisco", Country = "United States"},
new {CompanyName = "Trey Research", City = "New York", Country = "United States"},
new {CompanyName = "Wingtip Toys", City = "Landon", Country = "United Kingdom"},
new {CompanyName = "Wide World Importers", City = "Tetbury", Country = "United Kingdom"},
} ;
查询数据
为了显示由customers数组中每个客户的名字(FirstName)组成的列表,可以写一下代码:
IEnumerable customerFirstNames = customers.Select(cust => cust.FirstName);
foreach(string name in customerFirstNames )
{
Console.WriteLine(name);
}
Select方法允许从数组获取特定信息。传给Select方法的参数实际上是另一个方法,该方法从customers数组中获取一行,并返回从那一行选择的数据。可用自定义的方法执行这个任务,但最简单的机制是用Lambda表达式定义匿名方法,就像上例展示的那样。目前要注意以下3个重点:
1、cust变量是传给方法的参数。可认为cust是customers数组中的每一行的别名。
2、Select方法目前还没开始获取数据;相反,它只是返回一个“可枚举”对象。稍后遍历它时,才会真正获取Select方法指定的数据。
3、Select其实不是Array类型的方法。它是Enumerable类的扩展方法。Enumerable类位于System.Linq命名空间,它提供了大量静态方法来查询实现了泛型IEnumerable接口的对象。
Select方法返回基于某具体类型的可枚举集合。如果希望枚举器返回多个数据项,例如返回每个客户的名字和姓氏,至少有以下两个方案:
1、可以在Select方法中,将名字和姓氏连接成单独的字符串。实例如下:
IEnumerable customerNames = customers.Select(cust => String.Format("{0}{1}",cust.FirstName,cust.LastName));
2、可定义新类型来封装姓名和姓氏,并用Select方法构造这个类型的实例。例如:
class FullName
{
public string FirstName{get;set;}
public string LastName{get;set;}
}
.....
IEnumerable customerName = customers.Select(cust => new FullName
{
FirstName = cust. FirstName,
LastName = cust.LastName
});
第二个选项本来应该是首选的。但如果FullName类型的作用仅限于此,就可考虑使用匿名类型,而不是专门为一个操作定义一个新类型。下面是匿名类型的例子:
var customerName = customers.Select(cust => new{FirstName = cust. FirstName,LastName = cust.LastName});
注意,这里使用var关键字定义可枚举的类型。集合中的对象类型是匿名的,所以不知道集合中的对象的具体类型。
筛选数据
Select方法允许“指定”(用更专业的术语来说,就是“投射”)想包含到可枚举集合中的字段。然而,有时希望对可枚举集合中包含的进行限制。例如,为了列出address数组中地址在美国的所有公司的名称,可以像下面这样使用Where方法。
IEnumerable usCompanies =
addresses.Where(addr => String.Equals(addr.Country,"United States"))
.Select(usComp => usComp.CompanyName);
foreach(string name in usCompanies )
{
Console.WriteLine(name); //Coho Winery Trey Research
}
首先应用Where方法,从而筛选出行;再应用Select方法,从而指定(或者说投射)其中特定的字段。
排序、分组和聚合数据
按特定顺序获取数据要使用OrderBy方法。与Select和Where方法相似,OrderBy也要求以一个方法作为实参。该方法标识了对数据进行排序的表达式
IEnumerable companyNames =
addresses.OrderBy(addr => addr.CompanyName).Select(comp => comp.CompanyName);//升序
foreach(string name in companyNames )
{
Console.WriteLine(name);
}
要求降序枚举数据,可以换用OrderByDescending方法。要按多个键排序,可以在OrderBy或OrderByDescending之后使用ThenBy或ThenByDescending。
要按一个或多个字段中共同的值对数据进行分组,可以使用GroupBy方法。下例展示了如何按照国家对addresses数组中的公司进行分组。
var companiesGroupedByCountry =
addresses.GroupBy(addrs => addrs.Country);
foreach(var companiesPerCountry in companiesGroupedByCountry )
{
Console.WriteLine("Country: {0}\t{1} companies", companiesPerCountry.Key, companiesPerCountry.Count())
foreach(var companies in companiesPerCountry )
{
Console.WriteLine("\t{0}", companies.CompanyName);
}
}
GroupBy方法不需要同Select方法将字段投射到结果。
可直接为Select方法的结果使用许多汇总方法,例如Count,Max和Min等。例如:
int numberOfCompanies = addresses.Select(addr => addr.CompanyName).Count();
Console.WriteLine("Number of companies:{0}", numberOfCompanies );
可用Distinct方法来删除重复
int numberOfCountries = addresses.Select(addr => addr.Country).Distinct().Count();
Console.WriteLine("Number of countries:{0}", numberOfCountries );
联接数据
和SQL一样,LINQ也允许根据一个或多个匹配键(common Key)字段来联接多个数据集。下例展示了如何显示每个客户的名字和姓氏,同时显示他们所在国家的名称:
var companiesAndCustomers = customers.Select(c => new {c.FirstName,c.LastName,c.CompanyName})
.Join(addresses, cust =>cust.CompanyName, addrs =>addrs.CompanyName,
(custs,addrs) => new {custs.FirstName,custs.LastName,addrs.Country });
foreach(var row in companiesAndCustomers )
{
Console.WriteLine(row);
}
使用查询操作符
C#的设计者为语言添加了一系列查询操作符,允许开发人员使用与SQL更相似的语法来使用LINQ功能。
var customerFirstNames = from cust in customers
select cust.FirstName;
编译时,C#编译器将上述表达式解析成对应的Select方法。from操作符为来源集合定义了别名,select操作符利用该别名指定了要获取的字段。
var customerNames = from c in customers
select new {c.FirstName,c.LastName};
where:
var usCompanies = from a in addresses
where String.Equals(a.Country,"United States")
select a.CompanyName;
orderby:
var companyNames = from a in address
orderby a.CompanyName
select a.CompanyName;
group by:
var companiesGoupedByCountry = from a in addresses
group a by a.Country;
注意,和前面用GroupBy方法对数据进行分组的例子一样,这里不需要提供select操作符,而且可以和以前一样的代码遍历结果:
foreach(var companiesPerCountry in companiesGroupedByCountry )
{
Console.WriteLine("Country: {0}\t{1} companies", companiesPerCountry.Key, companiesPerCountry.Count())
foreach(var companies in companiesPerCountry )
{
Console.WriteLine("\t{0}", companies.CompanyName);
}
}
可为返回的可枚举集合调用各种汇总函数,例如Count方法:
int numberOfCompanies = (from a in addresses
select a.CompanyName).Count();
int numberOfCountries = (from a in addresses
select a.Country).Distinct().Count();
join:
var citiesAndCustomers = from a in addresses
join c in customers
on a.CompanyName equals c.CompanyName
select new{c.FirstName,c.LastName,a.Country};
LINQ和推迟求值
使用LINQ定义可枚举集合时,不管是使用LINQ扩展方法,还是使用查询操作符,都应该记住这样一点:LINQ扩展方法执行时,应用程序不会真正构建集合;只有在遍历集合时,才会对集合进行枚举。也就是说,从执行一个LINQ查询之后,到取回这个查询所标识的数据之前,原始集合中的数据可能发生改变。但是,获取的始终是最新的数据。例如:
var usCompanies = from a in addresses
where String.Equals(a.Country,"United States")
select a.CompanyName;
除非使用以下代码遍历usCompanies 集合,否则addresses数据中数据不会获取,Where筛选器中指定的条件也不会求值:
foreach(string name in usCompanies )
{
Console.WriteLine(name);
}
从定义usCompanies 集合到遍历这个集合,在此期间如果对addresses数组中的数据进行修改,就会看到新的数据。这个策略就是所谓的推迟求值。
可在定义LINQ查询时强制求值,从而生成一个静态的、缓存的集合。这个集合是原始数据的拷贝。如果原始数据发生改变,这个拷贝中的数据是不会相应改变的。LINQ提供了ToList方法来构建静态List对象以包含数据的缓存拷贝。如下:
var usCompanies = from a in addresses.ToList()
where String.Equals(a.Country,"United States")
select a.CompanyName;
分组查询:
var addresses = new[] {
new {EIRNo = 1, Charge = 20, CNTNo = "1"},
new {EIRNo = 2, Charge = 10, CNTNo = "2"},
new {EIRNo = 1, Charge = 5, CNTNo = "1"},
new {EIRNo = 2, Charge = 10, CNTNo = "2"},
};
var query = from c in addresses.AsEnumerable()
group c by new
{
c.EIRNo,
c.CNTNo
}
into s
select new
{
EIRNo = s.Key.EIRNo,
CNTNo = s.Key.CNTNo,
Charge = s.Sum(p => p.Charge)
};
foreach(var q in query)
{
Console.WriteLine("EIRNO : {0} Charge : {1} CNTNo : {2}",q.CNTNo,q.Charge,q.CNTNo);
}