LINQ(Language Integrated Query,语言集成查询)在C#编程语言中集成了查询语法,可以用相同的语法访问不同的数据源。LINQ提供了不同数据源的抽象层,所以可以使用相同的语法。
本章介绍LINQ的核心原理和C#中支持C# LINQ Query的语言扩展。
在介绍LINQ的特性之前,本节先介绍一个简单的LINQ查询。C#提供了转换为方法调用的集成查询语言。下面说明这个转换的过程,以便用户使用LINQ的全部功能。
列表和实体
示例的LINQ查询在一个包含1950-2016年一级方程式锦标赛的集合上进行。这些数据需要使用.NET标准库中的类和列表来准备。
这个库使用了如下名称空间:
System
System.Collections.Generic
对于实体,定义类型Racer。Racer定义了几个属性和一个重载的ToString()方法,该方法以字符串格式显示赛车手。这个类实现了IFormattable接口,以支持格式字符串的不同变体,这个类还实现了IComparable<Racer>接口,它根据LastName为一组赛车手排序。为了执行更高级的查询,Racer类不仅包含单值属性,如FirstName、LastName、Wins、Country和Starts,还包含多值属性,如Cars和Years。Years属性列出了赛车手获得冠军的年份。一些赛车手曾多次获得冠军。Cars属性用于列出赛车手在获得冠军的年份中使用的所有车型。
public class Racer : IComparable<Racer>, IFormattable {
public Racer(string firstName,string lastName,string country,int starts,int wins)
: this(firstName,lastName,country,starts,wins,null,null){}
public Racer (string firstName, string lastName, string country, int starts,
int wins, IEnumerable<int> years, IEnumerable<string> cars) {
FirstName = firstName;
LastName = lastName;
Country = country;
Starts = starts;
Wins = wins;
Years = years != null ? new List<int> (years) : new List<int> ();
Cars = cars != null ? new List<string> (cars) : new List<string> ();
}
public string FirstName { get; }
public string LastName { get; }
public int Wins { get; }
public string Country { get; }
public int Starts { get; }
public IEnumerable<string> Cars { get; }
public IEnumerable<int> Years { get; }
public override string ToString()=>$"{FirstName} {LastName}";
public int CompareTo(Racer other)=>LastName.CompareTo(other?.LastName);
public string ToString(string format)=>ToString(format,null);
public string ToString(string format,IFormatProvider formatProvider)
{
switch(format)
{
case null:
return null;
case "N":
return ToString();
case "F":
return FirstName;
case "L":
return LastName;
case "C":
return Country;
case "S":
return Starts.ToString();
case "W":
return Wins.ToString();
case "A":
return $"{FirstName},{LastName},{Country}; starts:{Starts}, wins:{Wins}";
default:
throw new FormatException($"Format {format} not supported");
}
}
第二个实体类是Team。这个类仅包含车队冠军的名字和获得冠军的年份。与赛车手冠军类似,针对一年中最好的车队也有一个冠军奖项:
public class Team
{
public Team(string name,params int[] years)
{
Name = name;
Years = years != null ?new List<int>(years):new List<int>();
}
public string Name{get;}
public IEnumerable<int> Years{get;}
}
Formula1类在GetChampions()方法中返回一组赛车手。这个列表包含了1950-2016年之间的所有一级方程式冠军。
public class Formula1
{
private static List<Racer> s_racers;
public static IList<Racer> GetChampions()=>
s_racers??(s_racers = InitializeRacers());
private static List<Racer> InitializeRacers()=>
new List<Racer>()
{
new Racer("Nino","Farina","Italy",33,5,new int[]{1950},new string[] {"Alfa Romeo"}),
new Racer("Alberto","Ascari","Italy",32,10,new int[]{1952,1953},new string[]{"Ferrari"}),
new Racer("Juan Manuel","Fangio","Argentina",51,24,new int[]{1951,1954,1955,1956,1957},
new string[]{"Alfa Remeo","Maserati","Mercedes","Ferrari"}),
new Racer("Mike","Hawthorn","UK",45,3,new int[]{1958},new string[]{"Ferrari"}),
new Racer("Phil","Hill","USA",48,3,new int[]{1961},new string[]{"Ferrari"}),
new Racer("John","Surtees","UK",111,6,new int[]{1964},new string[]{"Ferrari"}),
new Racer("Jim","Clark","UK",72,25,new int[]{1963,1965},new string[]{"Lotus"}),
new Racer("Jack","Brabham","Australia",125,14,new int[]{1959,1960,1966},new string[]{"Cooper","Brabham"}),
new Racer("Denny","Hulme","New Zealand",112,8,new int[]{1967},new string[]{"Brabham"}),
new Racer("Graham","Hill","UK",176,14,new int[]{1962,1968},new string[]{"BRM","Lotus"}),
new Racer("Jochen","Rindt","Austria",60,6,new int[]{1970},new string[]{"Lotus"}),
new Racer("Jackie","Stewart","UK",99,27,new int[]{1969,1971,1973},new string[]{"Metra","Tyrrell"})
};
}
对于后面在多个列表中执行的查询,GetConstructorChampions()方法返回所有的车队冠军列表。车队冠军是从1958年开始设立的。
private static List<Team> s_teams;
public static IList<Team> GetConstructorChampions()
{
if(s_teams == null)
{
s_teams = new List<Team>()
{
new Team("Vanwall",1958),
new Team("Cooper",1959,1960),
new Team("Ferrari",1961,1964,1975,1976,1977,1979,1982,1983,1999,2000,
2001,2002,2003,2004,2007,2008),
new Team("BRM",1962),
new Team("Lotus",1963,1965,1968,1970,1972,1973,1978),
new Team("Brabham",1966,1967),
new Team("Matra",1969),
new Team("Tyrrell",1971),
new Team("Mclaren",1974,1984,1985,1988,1989,1990,1991,1998),
new Team("Williams",1980,1981,1986,1987,1992,1993,1994,1996,1997),
new Team("Benetton",1995),
new Team("Renault",2005,2006),
new Team("Brawn GP",2009),
new Team("Red Bull Racing",2010,2011,2012,2013),
new Team("Mercedes",2014,2015,2016,2017)
};
}
return s_teams;
}
LINQ查询
演示LINQ的示例应用程序是一个控制台应用程序,使用了如下名称空间:
System
System.Collections.Generic
System.Linq
在以前创建的库中,使用这些准备好的列表和实体,进行LINQ查询,例如,查询来自意大利的所有世界冠军,并按照夺冠次数排序。为此可以使用List<T>类的方法,如FindAll()和Sort()方法。而使用LINQ的语法非常简单:
public static void LINQQuery () {
var query = from r in Formula1.GetChampions ()
where r.Country == "Italy"
orderby r.Wins descending
select r;
foreach (Racer r in query) {
System.Console.WriteLine($"{r:A}");
}
}
这个查询的结果显示了来自意大利的所有世界冠军,并排好序:
Alberto,Ascari,Italy; starts:32, wins:10
Nino,Farina,Italy; starts:33, wins:5
表达式
var query = from r in Formula1.GetChampions ()
where r.Country == "Italy"
orderby r.Wins descending
select r;
是一个LINQ查询。子句from、where、orderby、descending和select都是这个查询中预定义的关键字。
查询表达式必须以from子句开头,以select或group子句结束。在这两个子句之间,可以使用where、orderby、join、let和其他from子句。
注:变量query只指定了LINQ查询。该查询不是通过这个赋值语句进行的,只要使用foreach循环访问查询,该查询就会执行。
扩展方法
编译器会转换LINQ查询,以调用方法而不是LINQ查询。LINQ为IEnumerable<T>接口提供了各种扩展方法,以便用户在实现了该接口的任意集合上使用LINQ查询。扩展方法在静态类中声明,定义为一个静态方法,其中第一个参数定义了它扩展的类型。
扩展方法可以将方法写入最初没有提供该方法的类中。还可以把方法添加到实现某个特定接口的任何类中,这样多个类就可以使用相同的实现代码。
例如,String类没有Foo()方法。String类是密封的,所以不能从这个类中继承。但可以创建一个扩展方法,如下所示:
public static class StringExtension {
public static void Foo (this string s) {
System.Console.WriteLine ($"Foo invoked for {s}");
}
}
Foo()方法扩展了String类,因为它的第一个参数定义为String类型。为了区分扩展方法和一般的静态方法,扩展方法还需要对第一个参数使用this关键字,现在就可以使用带string类型的Foo()方法了:
string s = "Hello";
s.Foo();
结果在控制台上显示"Foo Invoked for Hello",因为Hello是传递给Foo()方法的字符串。
也许这看起来违反了面向对象的规则,因为给一个类型定义了新方法,但没有改变该类型或派生自它的类型。但实际上并非如此。扩展方法不能访问它扩展的类型的私有成员。调用扩展方法只是调用静态方法的一种新语法。对于字符串,可以用如下方式调用Foo()方法,获得相同的结果:
string s = "Hello";
//s.Foo();
StringExtension.Foo(s);
要调用静态方法,应在类名的后面加上方法名。扩展方法是调用静态方法的另一种方式。不必提供定义了静态方法的类名,相反,编译器调用静态方法是因为它带的参数类型。只需要导入包含该类的名称空间,就可以将Foo()扩展方法放在String类的作用域中。
定义LINQ扩展方法的一个类是System.Linq名称空间中的Enumerable。只需要导入这个名称空间,就可以打开这个类的扩展方法的作用域。下面列出了Where()扩展方法的实现 代码。Where()扩展方法的第一个参数包含了this关键字,其类型是IEnumerable<T>。这样,Where()方法就可以用于实现IEnumerable<T>的每个类型。例如,数组和List<T>类实现了IEnumerable<T>接口。第二个参数是一个Func<T,bool>委托,它引用了一个返回布尔值、参数类型为T的方法。这个谓词在实现代码中调用,检查IEnumerable<T>源中的项是否应放在目标集合中。如果委托引用了该方法,yield return语句就将源中的项返回给目标。
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,
Func<TSource,bool> predicate)
{
foreach(TSource item in source)
{
if(predicate(item))
yield return item;
}
}
因为Where()作为一个泛型方法实现,所以它可以用于包含在集合中的任意类型。实现了IEnumerable<T>接口的任意集合都支持它。
注:这里的扩展方法在System.Core程序集的System.Linq名称空间中定义。
现在就可以使用Enumerable类中的扩展方法Where()、OrderByDescending()和Select()。这些方法都返回IEnumerable<TSource>,所以可以使用前面的结果依次调用这些方法。通过扩展方法的参数,使用定义了委托参数的实现代码的匿名方法。
static void ExtensionMethods()
{
var champions = new List<Racer>(Formula1.GetChampions());
IEnumerable<Racer> italyChampions = champions.Where(r=>r.Country == "Italy")
.OrderByDescending(r=>r.Wins)
.Select(r=>r);
foreach(Racer r in italyChampions)
{
System.Console.WriteLine($"{r:A}");
}
}
推迟查询的执行
在运行期间定义查询表达式时,查询就不会运行。查询会在迭代数据项时运行。
再看看扩展方法Where()。它使用yield rerurn语句返回谓词为true的元素。因为使用了yield return语句,所以编译器会创建一个枚举器,在访问枚举中的项后,就返回它们。
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,
Func<TSource,bool> predicate)
{
foreach(TSource item in source)
{
if(predicate(item))
yield return item;
}
}
这是一个非常有趣也非常重要的结果。在下面的例子中,创建了String元素的一个集合,用名称填充它。接着定义一个查询,从集合中找出以字母J开头的所有名称。集合也应是排好序的。在定义查询时,不会进行迭代。相反,迭代在foreach语句中进行,在其中迭代所有的项。集合中只有一个元素Juan满足where表达式的要求,即以字母J开头。迭代完成后,将Juan写入控制台。之后在集合中添加4个新名称,再次进行迭代。
static void DeferredQuery()
{
var names = new List<string> {"Nino","Alberto","Juan","Mike","Phil"};
var namesWithJ = from n in names
where n.StartsWith("J")
orderby n
select n;
System.Console.WriteLine("First iteration");
foreach(string name in namesWithJ)
{
System.Console.WriteLine(name);
}
names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
System.Console.WriteLine("Second iteration");
foreach(string name in namesWithJ)
{
System.Console.WriteLine(name);
}
}
因为迭代在查询定义时不会进行,而是在执行每个foreach语句时进行,所以可以看到其中的变化,如应用程序的结果所示:
First iteration
Juan
Second iteration
Jack
Jim
John
Juan
当然,还必须注意,每次在迭代使用查询时,都会 调用扩展方法。大多数情况下,这是非常有效的,因为我们可以检测出源数据中的变化。但在一些情况下,这是不可行的。调用扩展方法ToArray()、ToList()等可以改变这个操作。在示例中,ToList遍历集合,返回一个实现了IList<string>的集合。之后对返回的列表遍历两次,在两次迭代之间,数据源得到了新名称。
static void DeferredQuery()
{
var names = new List<string> {"Nino","Alberto","Juan","Mike","Phil"};
var namesWithJ = (from n in names
where n.StartsWith("J")
orderby n
select n).ToList();
System.Console.WriteLine("First iteration");
foreach(string name in namesWithJ)
{
System.Console.WriteLine(name);
}
names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
System.Console.WriteLine("Second iteration");
foreach(string name in namesWithJ)
{
System.Console.WriteLine(name);
}
}
在结果中可以看到,在两次迭代之间输出保持不变,但集合中的值改变了。