C# LINQ(标准询运算符)
标准询运算符
标准查询运算符由一系列API方法组成,API能让我们查询任何NET数组或集合。标准查
询运算符的重要特性如下。
-
标准查询运算符使用方法语法。
-
一些运算符返回lenumerable对象(或其他序列),而其他运算符返回标量。返回标量的
运算符立即执行查询,并返回一个值,而不是一个可枚举类型对象。ToArray()、ToList()
等ToCollection运算符也会立即执行。 -
很多操作都以一个谓词作为参数。谓词是一个方法,它以对象为参数,根据对象是否满
足某个条件而返回true或false。
被查询的集合对象叫作序列,它心须实现IEnumerable接口,其中T是类型。
如下代码演示了sum和Count运算符的使用,它们返回了int。代码需要注意的地方如下所示。 -
用作方法的运算符直接作用于序列对象,在这里就是numbers数组。
-
返回类型不是lenumerable对象,而是int。
class Program
{static int[] numbers=new int[] {2,4,6}static void Main(){int total =numbers.Sum();int howMany=numbers.Count();Console.WriteLine($"Total:{total},Count:{howMany}");}
}
有大量标准查询运算符,可用来操作一个或多个序列。序列是指实现了lenumerable<>接口
的类,包括List<>、Dictionary<>、Stack<>、Array等。标准查词运算符可帮助我们以非常强大
的方式来查询和操纵这些类型的对象。
表20-1列出了这些运算符,并给出了简单的信息以便你了解它们的目的和概念。它们之中
大多数都有一些重载,允许不同的选项和行为。你应该掌握该列表,熟悉这些可以节省大量时间
和精力的强大工具。当需要使用它们的时候,可以查看完整的在线文档。
运算符名 描述
SkipWhile 接受一个谓词,开始迭代,只要谓词对当前项的计算结果为true,就跳过该项。在谓词返回第一个false的时候,该项和其余项都会被选择
Join 对两个序列执行内联结,根据稍后描述联结匹配
GroupJoin 可以产生层次结果的联结,第一个序列中的各个元素都与第二个序列中的元素集合相关联
Concat 连接两个序列
OrderBy/ThenBy 根据一个或多个键对序列中的元素按升序排序
Reverse 反转序列中的元素
GroupBy 分组序列中的元素
Distinct 去除序列中的重复项
Union 返回两个序列的并集
Intersect 返回两个序列的交集
Except 操作两个序列。返回的是第一个序列中不重复的元素减去同样位于第二个序列中的元素
AsEnumerable 将序列作为IEnumerable返回
ToArray 将序列作为数组返回
ToList 将序列作为List返回
ToDictionary 将序列作为Dictionary<TKey, TElement>返回
ToLookup 将序列作为Lookup<TKey, TElement>返回
OfType 将所返回的序列中的元素指定为类型
Cast 将序列中所有的元素强制转换为给定的类型
SequenceEqual 返回一个布尔值,指定两个序列是否相等
First 返回序列中第一个与谓词匹配的元素。如果没有元素与谓词匹配,就抛出InvalidOperationException
FirstOrDefault 返回序列中第一个与谓词匹配的元素。如果没有给出谓词,方法返回序列的第一个元素。如果没有元素与谓词匹配,就使用该类型的默认值
Last 返回序列中最后一个与谓词匹配的元素。如果没有元素与谓词匹配,就抛出InvalidOperationException
LastOrDefault 返回序列中最后一个与谓词匹配的元素。如果没有元素与谓词匹配,就返回默认值
Single 返回序列中与谓词匹配的单个元素。如果没有元素匹配,或多于一个元素匹配,就抛出异常
SingleOrDefault 返回序列中与谓词匹配的单个元素。如果没有元素匹配,或多于一个元素匹配,就返回默认值
ElementAt 给定一个参数n,返回序列中第n+1个元素
ElementAtOrDefault 给定一个参数n,返回序列中第n+1个元素。如果索引超出范围,就返回默认值
DefaultIfEmpty 提供一个在序列为空(empty)时的默认值
Range 给定一个start类型和一个count整数,该方法返回的序列包含count个整型,其中第一个元素的值为start,每个后续元素都比前一个大1
Repeat 给定一个T类型的element和一个count整数,该方法返回的序列具有count个element副本
标准查询运算符的签名
system.Linq.Enumerable类声明了标准查询运算符方法。然而,这些方法不仅仅是普通方法,
它们是扩展了IEnumerab1e泛型类的扩展方法。
第8章和第18章中介绍了扩展方法,但本节却是学习如何使用扩展方法的好机会。本节将
为你提供一个优秀的代码模型,并可以让你更加透彻地理解标准查询运算符。
我们来简单回顾一下。扩展方法是公有的静态方法,尽管定义在一个类中,但目的是为另一
个类(第一个形参)增加功能。该参数前必须有关键字this。
例如,如下是3个标准查询运算符的签名:Count、First和Where。乍看上去很吓人。注意
下面有关签名的事项。
- 由于运算符是泛型方法,因此每个方法名都具有相关的泛型参数(T)。
- 由于运算符是扩展类的扩展方法,它们必须满足下面的语法条件。
- 声明为public和static
- 在第一个参数前有this扩展指示器。
- 把IEnumerable作为第一个参数类型。
为了演示直接调用扩展方法和将其作为扩展进行调用的不同,下面的代码分别用这两种形式
调用标准查询运算符Count和First。这两个运算符都接受一个参数一一一IEnuerable对象的引用。
- Count运算符返回序列中所有元素的个数。
- First运算符返回序列中的第一个元素。
在代码中,前两次使用的运算符都是直接调用的,和普通方法差不多,传人数组的名字作为
第一个参数。然而,之后的两行代码使用扩展方法语法来调用运算符,就好像它们是数组的方法
成员一样。由于.NET的Array类实现了IEnumerable接口,因此是有效的。
注意,这里没有指定参数,而是将数组名称从参数列表中移到了方法名称之前,用起来就好
像它包含了方法的声明一样。
方法语法调用和扩展语法调用在语义上是完全相等的,只是语法不同。
using System.Linq;static void Main()
{int[] intArray=new int[]{3,4,5,6,7,9};var count1=Enmberable.Count(intArray); //方法语法var firstNum1=Enumerable.First(intArray); //方法语法var count2=intArray.Count(); //扩展语法var firstNum2=intArray.First(); //扩展语法Console.WriteLine($"Count:{count1},FirstNumber:{firstNum1}");Console.WriteLine($"Count:{count2},FirstNumber:{firstNum2}");
}
查询表达式和标准查询运算符
标准查询运算符是进行查询的一组方法。如本章开始所讲,每一个查洵表达式还可以使用
带有标准查询运算符的方法语法来编写。编译器把每一个查询表达式翻译成标准查询运算符的形式。
很明显,由于所有查询表达式都被翻译成标准查询运算符,因此运算符可以执行由查询表达
式完成的任何操作,而且运算符还有查词表达式形式所不能提供的附加功能。例如,在之前示例
中使用的sum和count运算符,可以只用方法语法来表示。
然而,查询表达式和方法语法这两种表达式也可以组合。例如,如下代码演示了使用了Count
运算符的查询表达式。注意,在该代码中,查询表达式是圆括号内的一部分,在它之后跟一个点
和方法的名字。
static void Main()
{var numbers=new int[]{2,6,4,8,10};int howMany=(from n in numberswhere n<7select n).Count(); //select n为查询表达式 count()为运算符Console.WriteLine($"Count:{howMany}");
}
将委托作为参数
在前一节中我们已经看到了,每一个运算符的第一个参数是IEnumerable对象的引用,之
后的参数可以是任何类型。很多运算符接受泛型委托作为参数(泛型委托在第18章中解释过)。
关于把泛型委托作为参数,需要了解的最重要事项是:
- 泛型委托用于给运算符提供用户自定义的代码。
为了解释这一点,我们首先从一个演示Count运算符的几种使用方式的示例开始。count运
算符被重载并且有两种形式第一种形式在之前的示例中用过,它有一个参数,返回集合中元素
的个数。签名如下:
public static int Count<T>(this IEnumerable<T>source);
然而,假设我们希望计算数组中奇数元素的总数。要实现这一点,必须为count方法提供检
测整数是否为奇数的代码。
为此,需要使用Count方法的第二种形式,如下所示。它接受一个泛型委托作为其第二个参
数。调用时,我们必须提供一个接受单个T类型的输人参数并返回布尔值的委托对象。委托代码
的返回值必须指定元素是否应包含在总数中。
public static int Count<T>(this IEnumerable<T>source,Func<T,bool>predicate);//Func<T,bool>predicate泛型委托
例如,如下代码使用了第二种形式的Count运算符来只包含奇数值。它通过提供一个Lambda
表达式来实现,这个表达式在输入值是奇数时返回true,否则返回false。(Lambda表达式在第
14章中介绍过。)对于集合的每次遍历,Count调用这个方法(用Lambda表达式表示)并把当前
值作为输入。如果输入的是奇数,方法返回true,count会把这个元素包含在总数中。
static void Main()
{int[] intArray =new int[]{3,4,5,6,7,9};var countOdd=intArray.Count(n=>n%2==1);//n=>n%2==1寻找奇数的Lambda表达式Console.WriteLine($"Count of odd numbers:{countOdd}");
}
LINQ预定义的委托类型
和前面示例中的count运算符差不多,很多LINQ运算符需要我们提供代码来指示运算符如
何执行它的操作。我们通过把委托对象作为参数来实现。
在第14章中,我们把委托对象当作一个包含具有特殊签名和返回类型的方法或方法列表的
对象。当委托被调用时,它包含的方法会被依次调用。
.NET框架定义了两套泛型委托类型来用于标准查询运算符,即Func委托和Action委托,各
有19个成员。(你也可以将它们用在其他地方,而不限于查询运算符)
- 下面用作实参的委托对象必须是这些类型或这些形式之一
- TR代表返回值,并且总是类型参数列表中的最后一个。
下面列出了前4个泛型Func委托。第一个没有方法参数,返回符合返回类型的对象。第一
个接受单个方法参数并且返回一个值,依次类推。
public delegate TR Func<out TR>();
public delegate TR Func<in T1,out TR>(T1 a1);
public delegate TR Func<in T1,in T2,out TR>(T1 a1,T2 a2);
public delegate TR Func<in T1,in T2,in T3,out TR>(T1 a1,T2 a2,T3 a3);//TR返回类型 <in T1,in T2,in T3,out TR>类型参数 (T1 a1,T2 a2,T3 a3)方法参数
注意返回类型参数有一个out关键字,使之可以协变,也就是说可以接受声明的类型或从这
个类型派生的类型。输入参数有一个in关键字,使之可以逆变,也就是你可以接受声明的类型
或从这个类型派生的类型。
知道了这些,如果我们再看一下Count的声(如下所示),可以发现第二个参数必须是委
托对象,它接受单个T类型的值作为方法参数并且返回一个bool类型的值。如本章前面所说,这种形式的委托称为谓词。
public static int Count<T>(this IEnumerable<T> source,Func<T,bool>predicate);//T参数类型 bool返回类型
如下是前4个Action委托。它们和Func委托相似,只是没有返回值,因此也就没有返回值
的类型参数。所有的类型参数都是逆变的。
public delegate void Action();
public delegate void Action<in T1>(T1 a1);
public delegate void Action<in T1,in T2>(T1 a1,T2 a2);
public delegate void Action<in T1,in T2,in T3>(T1 a1,T2 a2,T3 a3);
使用委托参数的示例
既然已经对Count签名以及LINQ对泛型委托参数的使用有了更深入的理解,我们就可以更
好地理解一个完整示例了。
如下代码先声明了IsOdd方法,它接受单个int类型的参数,并且返回表示输人参数是否是
奇数的bool值。Main方法做了如下的事情。
- 声明了int数组作为数据源。
- 创建了一个类型为Func<int,bool>、名称为MyDe1的委托对象,并且使用IsOdd方法来
初始化委托对象。注意,我们不需要声明Func委托类型,因为.NET框架已经预定义了。 - 使用委托对象调用count。
class Program
{static bool IsOdd(int x) //委托对象使用的方法{return x%2==1;//如果x是奇数,返回true}static void Main(){int[] intArray=new int[]{3,4,5,6,7,9};Func<int,bool>myDel=new Func<int,bool>(IsOdd); //委托对象var countOdd=intArray.Count(myDel); //使用委托Console.WriteLine($"Count of odd numbers:{countOdd}");}
}
使用Lambda表达式参数的示例
之前的示例使用独立的方法和委托来把代码附加到运算符上。这需要声明方法和委托对象,
然后把委托对象传递给运算符。如果下面任意一个条件成立,则这种方式就是正确的方式。
- 如果方法还必须在程序的其他地方调用,而不仅仅是用来初始化委托对象的地方。
- 如果函数体中的代码不止有一两条语句。
然而,如果这两个条件都不成立,我们可能需要使用更简洁和更局部化的方法来给运算符提
供代码,那就是使用Lambda表达式。
我们可以使用Lambda表达式来修改之前的示例首先,删除整个IsOdd方法,然后用等价
的Lambda表达式直接替换委托对象的声明。新的代码更短也更简洁,如下所示:
class Program
{static void Main(){int[] intArray=new int[]{3,4,5,6,7,9};var countOdd=int intArray.Count(x=>x%2==2);//Lambda表达式x=>x%2==2Console.WriteLine($"Count of odd numbers:{countOdd}");}
}
和之前的示例一样,这段代码产生了如下的输出:
Count Of Odd numbers:4
如下所示,也可以使用匿名方法来替代Lambda表达式。然而,这种方式比较烦琐,而且
表达式在语义上与匿名方法是完全等价的,并且更简洁,因此没有理由再使用匿名方法了。
class Program
{static void Main(){int[] intArray=new int[] {3,4,5,6,7,9}Func<int,bool>myDel=delegate(int X)//delegate(int x)匿名方法{return x%2==1;}var countOdd=intArray.Count(myDel);Console.WriteLine($"Count of odd numbers:{countOdd}");}
}