首页 > 代码库 > 第二十一章、使用查询表达式来查询内存中的数据

第二十一章、使用查询表达式来查询内存中的数据

  什么是语言集成查询(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);

            }

结果:EIRNO : 1 Charge : 25 CNTNo : 1
        EIRNO : 2 Charge : 20 CNTNo : 2

 

第二十一章、使用查询表达式来查询内存中的数据