• C#高级编程第11版


    导航

    第十二章 Language Integrated Query

    12.1 LINQ 概述

    LINQ(语言集成查询)在C#程序设计语言中集成了查询语法,使得你可以通过相同的语法来访问不同的数据源。LINQ通过提供一个抽象层(offer an abstraction layer)来实现(accomplish)这一点。

    本章将阐述LINQ的核心原理(principle)以及对C#的扩展,使你对C#使用LINQ查询成为可能。

    注意:你可以在第26章,"核心实体框架"中了解到更多关于LINQ操作数据库的细节。关于如何通过LINQ操作XML数据则可以在附赠章节2,"XML和JSON"中获取。

    在深入LINQ本质之前,本章将以一个简单的LINQ查询示例开始讲解。C#语言中提供了集成查询语言,它是以方法调用的形式实现的。本章节将为你演示整个过程是如何转换的,因此你可以使用LINQ的所有功能(all the possibilities of LINQ)。

    12.1.1 列表和实体

    本章中的LINQ查询示例建立在一个包含了1950年到2016年之间所有F1方程式赛车冠军的集合之上。这部分数据可以通过.NET标准库事先准备好。

    关于数据实体,我们首先定义了Racer类型。Racer类定义了若干属性并且重写了ToString方法来正确地显示一个赛车手的信息。它还实现了IFormattable接口来支持显示不同格式的赛车手信息,并且它也实现了IComparable接口,可以用来在一个队列中根据赛车手的姓名进行排序。为了更进一步地演示LINQ查询,Racer类里不仅包括了单值属性,如FirstName,LastName,Wins,Country,Starts等,还包括了两个集合类型的属性Cars和Years。Years属性中包含了取得冠军头衔(title)的年份,有些赛车手不止拥有一个冠军头衔。Cars属性则是赛车手取得冠军时驾驶的赛车信息。整个Racer类如下所示:

    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:
    			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。这个类仅仅包含名称和一个年份数组,用来存储制造商锦标赛的信息(an array of years for constructor championships)。就像车手锦标赛(driver championship)一样,每年也有最佳车队制造商锦标赛(constructor championship for the best team of a year):

    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年所有F1赛事的冠军:

    public static class Formula1
    {
    	private static List < Racer > s_racers;
    	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, 13, 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 Romeo", "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[]
    			{
    				"Matra", "Tyrrell"
    			}),
    			new Racer("Emerson", "Fittipaldi", "Brazil", 143, 14, new int[]
    			{
    				1972, 1974
    			}, new string[]
    			{
    				"Lotus", "McLaren"
    			}),
    			new Racer("James", "Hunt", "UK", 91, 10, new int[]
    			{
    				1976
    			}, new string[]
    			{
    				"McLaren"
    			}),
    			new Racer("Mario", "Andretti", "USA", 128, 12, new int[]
    			{
    				1978
    			}, new string[]
    			{
    				"Lotus"
    			}),
    			new Racer("Jody", "Scheckter", "South Africa", 112, 10, new int[]
    			{
    				1979
    			}, new string[]
    			{
    				"Ferrari"
    			}),
    			new Racer("Alan", "Jones", "Australia", 115, 12, new int[]
    			{
    				1980
    			}, new string[]
    			{
    				"Williams"
    			}),
    			new Racer("Keke", "Rosberg", "Finland", 114, 5, new int[]
    			{
    				1982
    			}, new string[]
    			{
    				"Williams"
    			}),
    			new Racer("Niki", "Lauda", "Austria", 173, 25, new int[]
    			{
    				1975, 1977, 1984
    			}, new string[]
    			{
    				"Ferrari", "McLaren"
    			}),
    			new Racer("Nelson", "Piquet", "Brazil", 204, 23, new int[]
    			{
    				1981, 1983, 1987
    			}, new string[]
    			{
    				"Brabham", "Williams"
    			}),
    			new Racer("Ayrton", "Senna", "Brazil", 161, 41, new int[]
    			{
    				1988, 1990, 1991
    			}, new string[]
    			{
    				"McLaren"
    			}),
    			new Racer("Nigel", "Mansell", "UK", 187, 31, new int[]
    			{
    				1992
    			}, new string[]
    			{
    				"Williams"
    			}),
    			new Racer("Alain", "Prost", "France", 197, 51, new int[]
    			{
    				1985, 1986, 1989, 1993
    			}, new string[]
    			{
    				"McLaren", "Williams"
    			}),
    			new Racer("Damon", "Hill", "UK", 114, 22, new int[]
    			{
    				1996
    			}, new string[]
    			{
    				"Williams"
    			}),
    			new Racer("Jacques", "Villeneuve", "Canada", 165, 11, new int[]
    			{
    				1997
    			}, new string[]
    			{
    				"Williams"
    			}),
    			new Racer("Mika", "Hakkinen", "Finland", 160, 20, new int[]
    			{
    				1998, 1999
    			}, new string[]
    			{
    				"McLaren"
    			}),
    			new Racer("Michael", "Schumacher", "Germany", 287, 91, new int[]
    			{
    				1994, 1995, 2000, 2001, 2002, 2003, 2004
    			}, new string[]
    			{
    				"Benetton", "Ferrari"
    			}),
    			new Racer("Fernando", "Alonso", "Spain", 291, 32, new int[]
    			{
    				2005, 2006
    			}, new string[]
    			{
    				"Renault"
    			}),
    			new Racer("Kimi", "Räikkönen", "Finland", 271, 20, new int[]
    			{
    				2007
    			}, new string[]
    			{
    				"Ferrari"
    			}),
    			new Racer("Lewis", "Hamilton", "UK", 208, 62, new int[]
    			{
    				2008, 2014, 2015, 2017
    			}, new string[]
    			{
    				"McLaren", "Mercedes"
    			}),
    			new Racer("Jenson", "Button", "UK", 306, 16, new int[]
    			{
    				2009
    			}, new string[]
    			{
    				"Brawn GP"
    			}),
    			new Racer("Sebastian", "Vettel", "Germany", 198, 47, new int[]
    			{
    				2010, 2011, 2012, 2013
    			}, new string[]
    			{
    				"Red Bull Racing"
    			}),
    			new Racer("Nico", "Rosberg", "Germany", 207, 24, new int[]
    			{
    				2016
    			}, new string[]
    			{
    				"Mercedes"
    			})
    	};
    	public static IList < Racer > GetChampions() => s_racers ?? (s_racers = InitializeRacers());
        //...
    }
    

    Where查询将通过多个列表来完成,紧接着的GetContructorChampions方法用来返回制造商锦标赛的列表:

    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;
    }
    

    12.1.2 LINQ 查询

    使用前面准备好的列表和对象,你可以开始尝试LINQ查询了。例如,查询所有获得世界冠军的巴西籍选手并根据其胜场由高到低倒序排序。为了实现这个需求,你可以使用List<T>的方法,如FindAll和Sort来实现。然而,如果使用LINQ语法的话,语法要简单得多:

    static void LINQQuery()
    {
    	var query = from r in Formula1.GetChampions()
    	where r.Country == "Brazil"
    	orderby r.Wins descending
    	select r;
    	foreach(var r in query)
    	{
    		Console.WriteLine($"{r:A}");
    	}
    	Console.WriteLine();
    }
    

    输出结果如下所示:

    Ayrton Senna, country: Brazil; starts: 161, wins: 41
    Nelson Piquet, country: Brazil; starts: 204, wins: 23
    Emerson Fittipaldi, country: Brazil; starts: 143, wins: 14
    

    在上面的代码中,表达式:

    from r in Formula1.GetChampions()
    where r.Country == "Brazil"
    orderby r.Wins descending
    select r;
    

    就是一个LINQ查询。查询中的from,where,orderby,descending和select子句(clause)都是预定义好的关键字。

    LINQ查询必须以from子句开始,以select或者group子句结束。在两者之间你可以根据需求使用where,orderby,join,let或者嵌套的from子句等等。

    注意:上面的query变量仅仅只是赋值了一个LINQ查询,而非LINQ的结果集,只有在foreach循环中,才开始访问query结果集。关于这一点我们将在稍后的"延迟(deferred)查询执行"小节中讲解。

    12.1.3 扩展方法

    编译器会将LINQ查询转换成方法调用。LINQ为IEnumerable<T>接口提供了各种各样的扩展方法,因此你可以在任何实现了IEnumerable接口的集合间使用LINQ查询。扩展方法是一种在静态类中声明的,第一个参数是要扩展类型的静态方法。

    扩展方法使得你可以在外部为某个类增加未定义过的方法。你也可以为某个接口创建扩展方法,这样实现了该接口的所有类都可以调用该扩展方法。

    举个例子,让我们为系统定义的String类添加一个Foo方法。因为String类是sealed类,因此你无法继承该类。然而你可以为它创建一个扩展方法,如下所示:

    public static class StringExtension
    {
    	public static void Foo(this string s)
    	{
    		Console.WriteLine($"Foo invoked for {s}");
    	}
    }
    

    Foo方法扩展了string类,如你所见,第一个参数就是string类型的。与普通的静态方法不同的是,扩展方法要求为第一个参数使用this关键字。

    事实上,现在你就可以直接在string类型上调用Foo方法了:

    string s = "Hello";
    s.Foo(); //Foo invoked for Hello
    

    这一点看上去似乎破坏了面向对象的原则,因为新增对象的方法并不是通过修改自身或者继承来实现的。然而,事实并非如此。扩展方法并不能访问它扩展对象的任何私有成员。扩展方法其实是一种语法糖,实际上面例子的调用跟下面是一样的:

    string s = "Hello";
    StringExtension.Foo(s);
    

    这里就是常规地通过类名来调用它的静态方法。扩展方法只是另一种调用静态方法的写法而已,使用扩展方法你不需要书写类名,而是通过编译器来根据参数类型选择合适的静态方法。你只需在要使用扩展方法的作用域中引用扩展方法所在类的命名空间即可。

    其中一个定义了LINQ扩展方法的类是Enumerable,它位于命名空间System.Linq中。你只需要导入这个命名空间就可以开启该类定义的扩展方法。例如下面示例的Where方法就是其中之一:

    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方法的第一个参数包含了this关键字,类型为IEnumerable<T>。这使得Where方法可以被任何实现了该接口的类型调用,譬如数组和List<T>。第二个参数为Func<T, bool>委托,引用了一个参数为T类型返回值为Boolean的方法。这个谓词(predicate)将在方法中被调用,用来判断IEnumerable<T>类型的源中的元素是否应该添加到目标集合中。假如该调用返回值为true,则通过yield return语句将元素从源中取得并返回。

    因为Where方法是一个泛型版本的方法,因此它可以支持任何实现了IEnumerable<T>类型的集合。

    注意:这里的扩展方法定义在System.Linq中,位于System.Core程序集里。

    现在你可以使用Enumerable类中的诸如Where,OrderByDescending或者Select等方法。因为这些方法返回的都是IEnumerable<T>类型,因此可以将前一个调用的结果集作为下一个方法的输入来进行嵌套调用。在下面的例子中,我们使用了匿名委托来作为扩展方法的委托参数:

    static void ExtensionMethods()
    {
    	var champions = new List < Racer > (Formula1.GetChampions());
    	IEnumerable < Racer > brazilChampions = champions.Where(r => r.Country == "Brazil").OrderByDescending(r => r.Wins).Select(r => r);
    	foreach(Racer r in brazilChampions)
    	{
    		Console.WriteLine($"{r:A}");
    	}
    	Console.WriteLine();
    }
    

    12.1.4 延迟查询的执行

    在运行时,查询表达式并不会在定义的时候就马上执行。只有当它的结果集被遍历的时候,查询表达式才开始执行。

    让我们再次回顾一下Where方法:

    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;
    		}
    	}
    }
    

    这个方法中使用了yield return语句来返回predicate为true时的元素。因为使用了yield return语句,只要它们在某处枚举过程(enumeration)中被引用,编译器就会创建一个枚举器并将满足条件的元素返回。

    这一点非常有趣并且有一个非常重要的影响(effect)。在下面的示例中,我们创建了一个用来存储string元素的集合并在其中存入一些姓名。接下来,我们创建了一个查询,用来获取所有J开头的名称,并对其进行排序。此时遍历并未开始,而在接下来的foreach语句中,所有的元素才开始遍历。只有一个元素满足查询的条件:Juan。当控制台输出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;
    	Console.WriteLine("First iteration");
    	foreach(string name in namesWithJ)
    	{
    		Console.WriteLine(name);
    	}
    	Console.WriteLine();
    	names.Add("John");
    	names.Add("Jim");
    	names.Add("Jack");
    	names.Add("Denny");
    	Console.WriteLine("Second iteration");
    	foreach(string name in namesWithJ)
    	{
    		Console.WriteLine(name);
    	}
    	Console.WriteLine();
    }
    

    因为遍历发生在每次foreach调用而非查询定义的时刻,因此你可以看到两次遍历结果的不同:

    First iteration
    Juan
    
    Second iteration
    Jack
    Jim
    John
    Juan
    

    当然,你也必须意识(aware)到,在遍历整个查询的过程中调用的是扩展方法。大多数时候直接遍历查询非常实用(pratical),因为你可以察觉到(detect)源数据的变化。只不过,有的时候,这一点也不太方便。此时你可以通过调用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();
    	Console.WriteLine("First iteration");
    	foreach(string name in namesWithJ)
    	{
    		Console.WriteLine(name);
    	}
    	Console.WriteLine();
    	names.Add("John");
    	names.Add("Jim");
    	names.Add("Jack");
    	names.Add("Denny");
    	Console.WriteLine("Second iteration");
    	foreach(string name in namesWithJ)
    	{
    		Console.WriteLine(name);
    	}
    	Console.WriteLine();
    }
    

    如下所示:

    First iteration
    Juan
    
    Second iteration
    Juan
    

    12.2 标准查询操作符

    Where,OrderByDescending以及Select仅仅只是LINQ定义的一部分操作符。实际上,LINQ查询为大多数通用的操作符都定义了声明式语法。在Enumerable类中你还可以使用更多的查询操作符。下面的表格将列举Enumerable类中定义的标准查询操作符。

    操作符 描述
    Where
    OfType<TResult>
    筛选操作符定义了被返回的元素的约束。
    你可以在Where查询操作符使用谓词,譬
    如使用返回bool的Lambda表达式。
    OfType<TResult>则基于元素的类型,仅
    返回与TResult同类型的元素。
    Select
    SelectMany
    投影(Projection)操作符用来将一种类型转
    换成另外一种不同的类型。Select和
    SelectMany方法定义了投影操作来基于选
    择器的功能来从结果集中选择值。
    OrderBy
    ThenBy
    OrderByDescending
    ThenByDescending
    Reverse
    排序操作符可以修改返回元素的顺序。
    OrderBy是增序排序,OrderByDescending
    则相反。ThenBy和ThenByDescending则是
    在首次排序的基础上进行二次排序。Reverse
    则将翻转整个集合的元素。
    Join
    GroupJoin
    联接运算符用来组合两个可能没有直接关系的
    集合。通过Join运算符,两个集合会基于Key选
    择器进行联接。这点和你熟知的SQL中的Join
    一样。GroupJoin除了联接两个集合之外还会
    对结果进行分组。
    GroupBy
    ToLookup
    分组运算符将数据划分为不同的组。GroupBy运
    算符根据指定Key对元素进行分组。ToLookup则
    是通过创建一个1对多的字典来进行元素的分组。
    Any
    All
    Contains
    量词(Quantifier)运算符返回的是Boolean值,根
    据元素的顺序是否满足指定条件进行判断。Any,
    All以及Contains都是量词运算符。Any判定集合中
    的是否有元素是否都满足谓词条件。All则是对所有
    元素进行判断。Contains则检查集合中是否存在特
    定元素。
    Take
    Skip
    TakeWhile
    SkipWhile
    分区(Partitioning)运算符返回集合的子集。Take,
    Skip,TakeWhile和SkipWhile都是分区运算符。通
    过它们,你可以获得部分结果集。使用Take,你需
    要指定从集合中取出的元素的数量。Skip则是忽略
    指定数量的元素,而取出其余的部分。TakeWhile
    只要条件为真的时候就会取出元素,SkipWhile当条
    件为真时则会忽略元素。
    Distinct
    Union
    Intersect
    Except
    Zip
    集(Set)运算符将返回一个集合。Distinct会移除集合
    中的重复项。除了Distinct之外,其它的操作符都需
    要两个集合作为操作数。Union返回两个集合中所有
    元素,相同元素只出现一次。Intersect则返回两个集
    合中都出现过的元素。Except则返回那些仅在一个集
    合中出现的元素。Zip将两个集合合并成一个。
    First
    FirstOrDefault
    Last
    LastOrDefault
    ElementAt
    ElementAtOrDefault
    Single
    SingleOrDefault
    元素(Element)元素安抚用来返回一个元素。First返回
    的是满足条件的第一个元素。FirstOrDefault和First很
    像,只不过当找不到满足条件的元素时,它会返回指定
    类型的默认元素。Last和LastOrDefault与前两者相似,
    只不过返回的是最后一个元素。通过ElementAt,你可
    以通过指定元素的位置来取得元素。Single返回满足条
    件的唯一元素,如果存在多个,则会抛出异常。所有的
    XXOrDefault和它的前缀方法功能差不多,只不过当找
    不到满足条件的元素时会返回一个缺省值。
    Count
    Sum
    Min
    Max
    Average
    Aggregeate
    聚合(Aggregate)运算符将对集合进行计算并返回一个值。
    通过聚合运算,你可以获取所有值的和,元素的数量,元
    素中的最大值和最小值,平均值等等。
    ToArray
    AsEnumerable
    ToList
    ToDictionary
    Cast<TResult>
    转换运算符将集合转变成一个数组,可能是IEnumerable,
    IList,IDictionary等等。Cast方法则将集合中的每一个元素
    都转换成泛型参数的类型。
    Empty
    Range
    Repeat
    生成(Generation)操作符返回一个新的序列,使用Empty运
    算符得到一个空集合,Range返回指定范围的元素,Repeat
    则返回将某个值重复多次的集合。

    接下来的各小节中我们将提供一系列的示例来演示如何使用这些操作符。对于每个不同的功能,示例程序都提供了通过命令行参数的方式来进行演示。你可以在"项目属性->调试"页面中配置所需的命令行参数,来运行不同章节的示例。你也可以像这样来使用.NET Core命令行功能(utility)来调用这些命令:

    dotnet run -- -f
    

    补充:

    • "-f":如果该项目指定多个框架,则需要使用-f来指定生成的目标框架。
    • "--" :在此分隔符后的所有参数均传递给已运行的应用程序。

    举个例子,你可以像下图这样运行命令:

    直接使用dotnetrun传递参数

    与在调试页中配置应用程序参数是一样的:

    配置应用程序参数

    12.2.1 筛选

    在Where子句中,你可以组合多个表达式:例如,只获取来自巴西和澳大利亚并且胜场在15场以上的赛车手。表达式只需要返回的类型是bool即可传递给where子句,如下所示:

     public static void Filtering()
     {
     	var racers = from r in Formula1.GetChampions()
     	where r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria")
     	select r;
     	foreach(var r in racers)
     	{
     		Console.WriteLine($"{r:A}");
     	}
     }
    

    执行这个查询,你将会看到下面的输出:

    Niki Lauda, country: Austria; starts: 173, wins: 25
    Nelson Piquet, country: Brazil; starts: 204, wins: 23
    Ayrton Senna, country: Brazil; starts: 161, wins: 41
    

    不是所有的查询都可以使用LINQ语法来完成,也不是所有的扩展方法都能映射为LINQ子句。更高级的查询需要直接使用扩展方法。为了能更好的理解复杂查询是如何使用扩展方法的,让我们先看看简单查询是如何映射的。如同下面使用了扩展方法Where和Select的代码所示,它的结果与上面的LINQ查询是一致的:

    public static void Filtering()
    {	
    	var racers = Formula1.GetChampions().Where(r => r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria")).Select(r => r);
    	foreach(var r in racers)
    	{
    		Console.WriteLine($"{r:A}");
    	}
    }
    

    12.2.2 用索引筛选

    其中一种情境是,在你需要使用Where方法某个重载版本的时候,你就无法直接使用LINQ查询语法。在这个重载的Where方法里,你可以传递第二个参数,也就是索引。索引是对筛选结果每一行数据的计数。你可以使用表达式中的索引来执行某些计算。在下面的例子中,Where方法查询的是姓氏以A开头的赛车手,并且只返回索引号为偶数的数据:

    public static void FilteringWithIndex()
    {
    	var racers = Formula1.GetChampions().Where((r, index) => r.LastName.StartsWith("A") && index % 2 != 0);
    	foreach(var r in racers)
    	{
    		Console.WriteLine($"{r:A}");
    	}
    }
    

    尽管姓氏以A开头的赛车手有Alberto Ascari,Mario Andretti,和Fernando Alonso,但是Mario Andretti的索引号是1,为奇数,所以它被排除在结果集之外。结果如下所示:

    Alberto Ascari, country: Italy; starts: 32, wins: 13
    Fernando Alonso, country: Spain; starts: 291, wins: 32
    

    12.2.3 类型筛选

    如果要基于数据类型进行筛选的话,你可以使用OfType扩展方法。下面的例子中,数组包含了string和int类型的数据。使用扩展方法OfType,指定string类型作为泛型参数,将只返回集合中的字符串:

    public static void TypeFiltering()
    {
    	object[] data = {
    		"one", 2, 3, "four", "five", 6
    	};
    	var query = data.OfType < string > ();
    	foreach(var s in query)
    	{
    		Console.WriteLine(s);
    	}
    }
    

    运行程序,结果如下所示:

    one
    four
    five
    

    12.2.4 复合的from子句

    假如你需要进行一个筛选,基于对象中的某个成员,并且该成员是一个序列,你可以使用复合from。Racer类中定义了一个Cars属性,它是一个String数组。为了筛选使用Ferrari赛车参赛并获得冠军的赛车手,你可以像下面这样使用LINQ:

    public static void CompoundFrom()
    {
    	var ferrariDrivers = from r in Formula1.GetChampions()
    	from c in r.Cars
    	where c == "Ferrari"
    	orderby r.LastName
    	select $"{r.FirstName} {r.LastName}";
    	foreach(var racer in ferrariDrivers)
    	{
    		Console.WriteLine(racer);
    	}
    }
    

    第一个from子句用来访问Formula1.GetChampions返回的所有Racer对象。第二个from子句则是从所有Racer类中访问它的Cars属性来得到所有赛车的信息。接下来使用where子句来筛选使用Ferrari赛车参赛的冠军选手。运行程序,结果如下所示:

    Alberto Ascari
    Juan Manuel Fangio
    Mike Hawthorn
    Phil Hill
    Niki Lauda
    Kimi R?ikk?nen
    Jody Scheckter
    Michael Schumacher
    John Surtees
    

    补充:这里你可能会看到"R?ikk?nen"这样的字样而非"Räikkönen",这是因为命令窗口的默认代码页的问题。默认代码页为"936-GBK",我们可以将其修改成"65001-UTF-8"。我们打开注册表,找到"HKEY_CURRENT_USERConsole",可以看到这里有你安装的各种命令行工具,我们找到Visual Studio的那一项,把它的CodePage修改成65001即可。如果你使用的是其它的命令行工具,也可以进行相同的修改。如下图所示:

    修改命令行默认代码页

    修改好之后再次执行,就没有问题了:

    Alberto Ascari
    Juan Manuel Fangio
    Mike Hawthorn
    Phil Hill
    Niki Lauda
    Kimi Räikkönen
    Jody Scheckter
    Michael Schumacher
    John Surtees
    

    C#编译器将LINQ查询中的复合from子句转换成了使用SelectMany扩展方法。SelectMany方法可以用来从一个序列中遍历另一个序列。上面例子中用到的SelectMany方法如下所示:

    public static IEnumerable < TResult > SelectMany < TSource, TCollection, TResult > (this IEnumerable < TSource > source, Func < TSource, IEnumerable < TCollection >> collectionSelector, Func < TSource, TCollection, TResult > resultSelector)
    {
    	if(source == null) throw Error.ArgumentNull("source");
    	if(collectionSelector == null) throw Error.ArgumentNull("collectionSelector");
    	if(resultSelector == null) throw Error.ArgumentNull("resultSelector");
    	return SelectManyIterator < TSource, TCollection, TResult > (source, collectionSelector, resultSelector);
    }
    
    static IEnumerable < TResult > SelectManyIterator < TSource, TCollection, TResult > (IEnumerable < TSource > source, Func < TSource, IEnumerable < TCollection >> collectionSelector, Func < TSource, TCollection, TResult > resultSelector)
    {
    	foreach(TSource element in source)
    	{
    		foreach(TCollection subElement in collectionSelector(element))
    		{
    			yield return resultSelector(element, subElement);
    		}
    	}
    }
    

    第一个参数是隐式参数,用来接收GetChampions方法得到的Racer对象序列。第二个参数collectionSelector则是一个委托,用来定义内部序列。通过Lambda表达式r=>r.Cars,返回了包含所有赛车的集合。第三个参数也是一个委托,它现在将被调用来处理所有赛车的信息,并接收Racer和Car对象。lambda表达式通过Racer类和Car属性创建了一个匿名类型。而通过这个SelectMany方法,Racer和Car紧密结合在一起(the hierarchy of racers and cars is flattened),然后返回了一个包含所有赛车信息的匿名类型的对象集合。

    这个匿名对象集合将传递给Where方法,然后只有驾驶Ferrari赛车的车手会被筛选出来。最后调用了OrderBy和Select方法,如下所示:

    public static void CompoundFromWithMethods()
    {
    	var ferrariDrivers = 
    		Formula1.GetChampions()
    			.SelectMany(r => r.Cars, (r1, cars) => new {Racer1 = r1, Cars1 = cars})
    				.Where(item => item.Cars1.Contains("Ferrari"))
    				.OrderBy(item => item.Racer1.LastName)
    				.Select(item => $"{item.Racer1.FirstName} {item.Racer1.LastName}");
    	
    	foreach(var racer in ferrariDrivers)
    	{
    		Console.WriteLine(racer);
    	}
    }
    

    因为这就是LINQ查询的扩展方法版本,所以执行结果与上面一同无二,这里不再赘述。

    12.2.5 排序

    12.2.6 分组

    12.2.7 LINQ 查询中的变量

    12.2.8 对嵌套的对象分组

    12.2.9 内连接

    12.2.10 左外连接

    12.2.11 组连接

    12.2.12 集合操作

    12.2.13 合并

    12.2.14 分区

    12.2.15 聚合操作符

    12.2.16 转换操作符

    12.2.17 生成操作符

    12.3 并行LINQ

    12.3.1 并行查

    12.3.2 分区器

    12.3.3 取消

    12.4 表达式树

    12.5 LINQ 提供程序

    12.6 小结

  • 相关阅读:
    redis常用方法
    分享朋友圈、qq等思路及代码
    redis 使用例子
    redis使用实例应用
    js对象与jquery对象介绍
    h5网页跳转到小程序
    redis队列思路介绍
    redis队列思路分析
    php原生方法连接mysql数据库
    mysql 原生语句limit 分页
  • 原文地址:https://www.cnblogs.com/zenronphy/p/ProfessionalCSharp7Chapter12.html
Copyright © 2020-2023  润新知