1、LINQ介绍
LINQ(Language Integrated Query,语言集成查询),是c#编程语言中的一种查询语法。有了LINQ,使得以相同的语法访问不同的数据源成为可能。这是因为,LINQ提供了不同数据源的抽象层。
2、LINQ查询基础
本节介绍了一个简单的LINQ查询。在此基础上,着重理解:c#提供了转换为方法调用的集成查询语言。
2.1、准备列表和实体
接下来的几个章节的LINQ查询是在一个锦标赛的集合上进行的。这个集合需要用到.NET的类库:
System
System.Collection.Generic
2.2、Racer类型。
Racer定义了几个属性和一个重载的ToString()方法,该方法以字符串格式显示赛车手。实现了IFormattable接口,以支持字符串的不同变体;还实现了IComparable<Racer>接口,它根据Lastname为一组赛车手排序。为了演示更高级的查询,Racer类还定义了多值属性,如Cars和Years。Years属性列出了赛车手获得冠军的年份(车手可以多次获得冠军)。Cars属性列出了赛车手在获得冠军的年份中使用的所有车型:
1 //这是一个赛车手 2 public class Racer:IComparable<Racer>,IFormattable 3 { 4 public string FirstName { get; } 5 public string LastName { get; } 6 public string Country { get; } 7 public int Starts { get; } 8 public int Wins { get; } 9 public object Years { get; } //包含车手获得冠军的年份(可以多次获得冠军) 10 public object Cars { get; } //车手获得冠军年份中使用的车型。参加不同比赛,使用的车型可能不一样 11 12 public Racer(string firstName,string lastName,string country,int starts,int wins) 13 :this(firstName,lastName,country,starts,wins,null,null){ } 14 15 public Racer(string firstName, string lastName, string country, int starts, int wins, IEnumerable<int> years, IEnumerable<string> cars) 16 { 17 FirstName = firstName; 18 LastName = lastName; 19 Country = country; 20 Starts = starts; 21 Wins = wins; 22 Years = years; 23 Cars = cars; 24 } 25 public override string ToString() => $"{FirstName} {LastName}"; 26 27 int IComparable<Racer>.CompareTo(Racer other) => FirstName.CompareTo(other?.FirstName); 28 29 public string ToString(string format) => ToString(format, null); 30 31 public string ToString(string format, IFormatProvider formatProvider) 32 { 33 switch (format) 34 { 35 case null: 36 case "N": 37 return ToString(); 38 case "F": 39 return FirstName; 40 case "L": 41 return LastName; 42 case "C": 43 return Country; 44 case "S": 45 return Starts.ToString(); 46 case "W": 47 return Wins.ToString(); 48 case "A": 49 return $"姓名:{FirstName} {LastName},国家:{Country}; 开始:{Starts}; 获胜次数:{Wins}"; 50 default: 51 throw new FormatException($"不支持格式:{format}"); 52 } 53 } 54 }
2.3、Team类
这个类仅包含车队冠军的名字和获得冠军的年份。
//包含冠军姓名和获得冠军的年份 public class Team { public string Name { get; } public IEnumerable<int> Years { get; } public Team(string name,params int[] years) { Name = name; Years = years != null ? new List<int>(years) : new List<int>(); } }
2.4、Formulal类
该类的GetChampions()方法返回了一组赛车手。GetConstructorChampions()方法返回所有车队冠军的列表:
public static class Formulal { private static List<Racer> s_racers; public static IList<Racer> GetChampions() => s_racers ?? InitalizeRacers(); private static List<Racer> InitalizeRacers() => new List<Racer> { new Racer("赵", "一","中国",22,55,new int[] {1952,1925}, new string[]{ "宾利", "法拉利"}), new Racer("武", "二","美国",22,55,new int[] {1967,1976}, new string[]{ "丰田","奥迪"}), new Racer("张", "三","日本",22,55,new int[] {1974,1947}, new string[]{ "本田","大众"}), new Racer("李", "四","朝鲜",22,55,new int[] {1951,1915}, new string[]{ "长安","长安"}), new Racer("王", "五","英国",22,55,new int[] {1952,1925}, new string[]{ "奥迪", "马自达"}), new Racer("赵", "六","德国",22,55,new int[] {1945,1954,2054},new string[]{ "大众","迪奥"}), new Racer("孙", "七","法国",22,55,new int[] {1928,1982,2082},new string[]{ "长安","特斯拉"}), new Racer("王", "八","瑞典",22,55,new int[] {1989,1998,2098},new string[]{ "奥迪", "宾利"}), new Racer("刘", "九","印度",22,55,new int[] {1986,1968,2068},new string[]{ "大众","丰田"}), new Racer("Marry","sa","韩国",22,55,new int[] {1966,1966,2066},new string[]{ "长安","本田"}), new Racer("Jack", "jw","中国",22,55,new int[] {1963,1936,2036},new string[]{ "奥迪", "长安"}), new Racer("WC", "sa","美国",22,55,new int[] {1930,1903,2003},new string[]{ "法拉利", "法拉利"}) }; private static List<Team> s_teams; public static IList<Team> GetConstructorChampions() { if(s_racers == null) { s_teams = new List<Team>() { new Team("赵", 1958), new Team("武", 1922,1233,1933), new Team("张", 1932,1941), new Team("李", 1914,1942,1941), new Team("王", 1945), new Team("赵", 1995,1988,1966,1978,1935,1968), new Team("孙", 2015,20145,2058), new Team("王", 2055), new Team("刘", 2001,2015), new Team("Marry",2016,2047), new Team("Jack", 2055,2044), new Team("WC", 2041,2087) }; } return s_teams; } }
2.5、LINQ查询
数据准备完毕,下面开始演示LINQ的应用。这是一个控制台应用程序。
例如,查询来自中国的所有冠军,并按照夺冠次数降序排序:
using System; using System.Collections.Generic; using System.Linq; namespace LINQDemo { class Program { static void Main(string[] args) { LINQQuery(); ExtensionMethod(); } /// <summary> /// Linq /// </summary> static void LINQQuery() { var query = from r in Formulal.GetChampions() where r.Country =="中国" orderby r.Wins descending select r; foreach(var r in query) { Console.WriteLine($"{r:A}"); } } static void ExtensionMethod() { var champions = new List<Racer>(Formulal.GetChampions()); var champion = champions.Where(r => r.Country == "中国") .OrderByDescending(s => s.Wins) .Select(c=>c); foreach(var r in champion) { Console.WriteLine($"{r:A}"); } } } }
运行结果:
其中,LINQQuery()方法实现中主要演示了LINQ查询的表达式:
var query = from r in Formulal.GetChampions() where r.Country =="中国" orderby r.Wins descending select r;
from、where、orderby、descending和select都是预定义的关键字。查询表达式必须以from子句开头,以select或者group子句结束。这两个子句之间,可以使用where、orderby、join、let和其他from子句。
注意:变量query只是指定了LINQ查询,该查询不是通过这个赋值语句执行的,只要使用foreach循环访问查询,该查询就会执行。
ExtensionMethod()方法演示了LINQ的扩展方法的使用。下面重点介绍。
3、扩展方法
定义:扩展方法在静态类中声明,定义为一个静态方法,其中第一个参数定义了它扩展的类型。为了区分扩展方法和一般静态方法,扩展方法需要对第一个参数使用this关键字。
作用:①扩展方法可以将该方法写入到类中(该类最初是没有这个方法的);②开可以把方法添加到实现了某个特定接口的任何类中,这样多个类就可以使用相同的实现代码。
例如,String类没有Foo()方法,并且不能从封闭的类String继承,但是可以创建一个扩展方法:
1 static class StringExtension 2 { 3 public static void Foo(this string s) 4 { 5 Console.WriteLine($"扩展方法Foo被调用:{s}"); 6 } 7 }
使用带string类型的Foo()方法:
1 string s = "haha"; 2 s.Foo();
运行结果:
也许这看起来违反了面向对象的规则,因为给一个类型定义了新方法,但没有改变该类型或它的派生类。但实际上并非如此。扩展方法不能访问它扩展的类型的私有成员。调用扩展方法只是调用静态方法的一种新语法。对于上面例子中的字符串,可以使用如下方式调用Foo()方法,会获得相同的结果:
string s = "haha"; StringExtension.Foo(s);
要调用静态方法,应在类名的后面加上方法名。扩展方法是调用静态方法的另一种形式。不必提供静态方法的类名,相反,编译器调用静态方法是因为它带的参数类型。
上面的例子中,只需要导入包含该类的名称空间,就可以将Foo()扩展方法放在String类的作用域中。
LINQ扩展方法
编译器会转换LINQ查询,以调用方法而不是LINQ查询。定义LINQ扩展方法的一个类是System.Linq名称空间中的Enumeable,它为IEnumerable<T>接口提供了各种扩展方法,以便在实现了该接口的任意集合上使用LINQ。只需要导入这个名称空间,就可以打开这个类的扩展方法的作用域。
下面是Where()扩展方法的实现代码:
1 public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,Func<TSource,bool> predicate) 2 { 3 foreach(TSource item in source) 4 { 5 if (predicate(item)) 6 yield return item; 7 } 8 }
分析:Where()扩展方法的第一个参数包含了this关键字,类型是IEnumerable<T>。这样Where()方法就可以用于实现了IEnumerable<T>接口的每个类型,例如:数组和List<T>类实现了IEnumerable<T>接口。第二个参数是一个Func(T,bool)委托(引用的是输入参数类型为T、返回类型为布尔型的方法)。这个谓词在实现代码中调用,检查IEnumerable<T>源中的项是否应该放在结果集合中。如果委托引用了该方法,yield return 语句就将源中的项返回给结果。
因为Where()作为一个泛型方法实现,所以它可以用于包含在集合中的任意类型。实现了IEnumerable<T>接口的任意集合都支持它。
到现在,就可以使用Enumerable类中的扩展方法Where()、OrderByDescending()和Select()。这些方法都返回IEnumerable<TSource>。通过扩展方法的参数,使用定义了委托参数的实现代码的匿名方法。
1 static void ExtensionMethod() 2 { 3 var champions = new List<Racer>(Formulal.GetChampions()); 4 var champion = champions.Where(r => r.Country == "中国") 5 .OrderByDescending(s => s.Wins) 6 .Select(c=>c); 7 foreach(var r in champion) 8 { 9 Console.WriteLine($"{r:A}"); 10 } 11 }
4、推迟查询的执行
定义查询表达式时,查询并不会执行。查询会在迭代数据项时运行。原因就是扩展方法Where(),它使用yield return返回谓词为true的元素。这时,编译器会创建一个枚举器,在访问枚举中的项后,才返回他们。
这会导致下面例子中有趣的现象,本例中创建一个string元素的集合,初始化它;然后给集合增加元素:
1 static void DeferredQuery() 2 { 3 var name = new List<string> { "a1", "a2", "b", "c", "d" }; 4 var nameWithA = from n in name 5 where n.StartsWith("a") 6 orderby n 7 select n; 8 foreach (var item in nameWithA) 9 { 10 Console.WriteLine($"{item}"); 11 } 12 Console.WriteLine("更改数据源后,再次迭代同样的LINQ语句:"); 13 name.Add("a3"); 14 name.Add("a4"); 15 name.Add("a5"); 16 foreach (var item in nameWithA) 17 { 18 Console.WriteLine($"{item}"); 19 } 20 }
观察结果,发现每次迭代时,可以检测出源数据中的变化:
特殊情况,调用扩展方法ToArray()、ToList()等可以改变上述结果。下面示例中,ToList遍历集合,返回一个实现了IList<string >的集合。然后对返回的列表遍历两次:
1 private static void DeferredQueryDemo() 2 { 3 var name = new List<string> { "a1", "a2", "b", "c", "d" }; 4 var nameWhithA = (from n in name 5 where n.StartsWith("a") 6 orderby n 7 select n).ToList(); 8 Console.WriteLine("第一次迭代:"); 9 foreach(var iten in nameWhithA) 10 { 11 Console.WriteLine(iten); 12 } 13 Console.WriteLine("更改数据源后,再次迭代同样的LINQ语句:"); 14 name.Add("a3"); 15 name.Add("a4"); 16 name.Add("a5"); 17 foreach (var iten in nameWhithA) 18 { 19 Console.WriteLine(iten); 20 } 21 }
观察结果,发现,虽然集合中的元素发生了变化,但是两次迭代之间的的输出保持不变: