LINQ 是.Net 3.5 中最重要的新功能,LinQ集成了C#编程语言中的查询语法,可以用相同的语法访问不同的数据源。LinQ提供了不同数据源的抽象层,所以可以使用相同的语法。
演变
下面是一组示例,这些示例都是基于一级方程式世界冠军。
获得国家是巴西的世界冠军,并按获胜的次数降序。
使用List<T>的查询
private static void TraditionalQuery() { //首先,需要用 GetChampions()静态方法获得对象列表。该列表放在泛型类 //List<T>中。这个类的 FindAll()方法接收一个 Predicate<T>委托,该委托可以实现为一个匿名方法。只返回 //Country 属性设置为 Brazil 的赛手。 List<Racer> champions = new List<Racer>(Formula1.GetChampions()); List<Racer> brazilChampions = champions.FindAll( delegate(Racer r) { return r.Country == "Brazil"; }); //接着,用 Sort()方法给得到的列表排序。不应按照 Lastname 属性排序, //因为这是Racer 类的默认排序方式,而可以传送一个类型为 Comparison<T>的委托,该委托也实现为一个匿名 //方法,来比较夺冠次数。使用 r2 对象,与 r1 比较,根据需要进行降序排序。 brazilChampions.Sort( delegate(Racer r1, Racer r2) { return r2.Wins.CompareTo(r1.Wins); }); //foreach 语句最终迭代已排序的 //集合中的所有 Racer 对象。 foreach (Racer r in brazilChampions) { Console.WriteLine("{0:A}", r); } }
扩展方法
扩展方法可以将方法写入最初没有提供该方法的类中。还可以把方法添加到实现某个接口的任何类中(这句话的意思是说如果
你实现了某个接口的扩展方法,那么他的所有实现类也会具有此方法),这样多个类就可以使用相同的实现代码。
例如,String 类没有 Foo()方法。String 类是密封的(扩展方法什么都可以扩展),所以不能从这个类中继承。但可以执行一个扩展方
法,如下所示:
public static class StringExtension //类的名字无所谓
{
public static void Foo(this string s) //s只是方法局部变量没有特定含义
{
Console.WriteLine("Foo invoked for {0}", s);
}
}
扩展方法在静态类中声明,定义为一个静态方法,其中第一个参数定义了它扩展的类型。Foo()方法扩展
了 string 类,因为它的第一个参数定义了 string 类型。为了区分扩展方法和一般的静态方法,扩展方法还需
要给第一个参数使用 this 关键字。
现在就可以使用带string 类型的 Foo()方法了:
string s = "Hello"; //s 是被扩展对象本身,同时又是被传递的参数
s.Foo(); //用被扩展类直接使用静态方法
结果在控制台上显示 Foo invoked for Hello,因为 Hello 是传送给 Foo()方法的字符串。
也许这看起来违反了面向对象的规则,因为给一个类型定义了新方法,但没有改变该类型。(这个思想得转变一会才行)
但实际上并非如此。扩展方法不能访问它扩展的类型的私有成员。调用扩展方法只是调用静态方法的一种新语法。
对于字符串,可以用如下方式调用 Foo()方法,获得相同的结果:
string s = "Hello";
StringExtension.Foo(s); //用扩展类直接使用
要调用静态方法,应在类名的后面加上方法名。扩展方法是调用静态方法的另一种方式。不必提供定义了
静态方法的类名,调用静态方法是因为它的参数类型,使用参数的类型在调用,而参数的值就调用对象本身的内容。
只需导入包含该类的命名空间,就可以将 Foo()扩展方法放在 string 类的作用域中,也就是说string 可以使用的地方。
定义 LINQ 扩展方法的一个类是 System.Linq 命名空间中的 Enumerable。只需导入这个命名空间,就打
开了这个类的扩展方法的作用域。为所有类提供扩展方法
下面列出了 Where()扩展方法的实现代码。Where()扩展方法的第一个参数包含了 this 关键字,
其类型是 IEnumerable<T>。这样,Where 方法就可以用于实现了 IEnumerable<T> 的每个类
型,(继承他的所有实现类和子类)。
为IEnumerable 扩展了Where方法
例如数组和 List<T> 实现了 IEnumerable<T>。
第二个参数是一个 Func<T,bool>委托,它引用了一个返回布尔值、参数类型为 T 的方法。
这个谓词在实现代码中被调用,用于实现谓词判断,也就是条件谓词,
这个谓词去实现检查 IEnumerable<T>源中的项是否放在目标集合中。这个谓词当然是为了自己定义判断谓词准备的
委托中定义了谓词,程序中引用了这个委托,最后 yield return 语句就将源中的项返回给新的复合条件的目标列表序列枚举。
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (T item in source)
if (predicate(item)) //谓词
yield return item;
}
因为Where()实现为一个泛型方法,所以可以用于包含在集合中的任意类型。实现了 IEnumerable<T>的集
合都支持它。
提示:这里的扩展方法在程序集 System.Core 的 System.Linq 命名空间中定义。
现在就可以使用 Enumerable 类中的扩展方法 Where()、OrderbyDescending()和 Select()了。
这些方法都返回 IEnumerable<TSource>(源类型集合),所以可以使用前面的结果依次调用这些方法。
通过扩展方法的参数和使用定义了委托参数的实现代码的匿名方法。
private static void ExtensionMethods() { List<Racer> champions = new List<Racer>(Formula1.GetChampions()); IEnumerable<Racer> brazilChampions = champions.Where( //champions 是IEnumerable<T> 参数1 //基于谓词筛选序列,源是brazilChampions,谓词条件是类型是Racer 值是Racer的r.Countray = "Brazil" 这个谓词条件 delegate(Racer r) //Racer 是匿名委托的第一个 T ,返回Bool,遍历每个元素时候所使用的判断方法委托,用于给其做判断时候调用,r,就是遍历中的每个元素 { return r.Country == "Brazil"; }).OrderByDescending( //对筛选后的集合,根据键值降序排序 delegate(Racer r) //而这个委托是用于该方法遍历的时候进行调用比较时候所使用的条件式,使用那个属性 { return r.Wins; }).Select( //重新生成集合给最终结果,当然如果不加这句完全没有问题 delegate(Racer r) { return r; }); //foreach 语句最终迭代已排序的 //集合中的所有 Racer 对象。 foreach (Racer r in brazilChampions) { Console.WriteLine("{0:A}", r); } }
λ表达式
C# 3.0 给匿名方法提供了一个新的语法——λ表达式。
使用λ表达式很容易的把匿名方法传送给 Where()、OrderbyDescending()和Select()方法。
这里把上面的例子改为使用λ表达式。这样语法比较短,也更容易理解了,
因为删除了return语句、参数类型和花括号。
λ表达式参见第 7 章。λ表达式在 LINQ 中非常重要,所以下面复习一下该语法。详细信息可参见第 7章。
比较λ表达式和匿名委托,会发现许多类似之处。
λ运算符=>的左边参数(左值),不需要添加参数类型,因为它们是由编译器解析的(就是扩展方法被扩展对象本身)。
λ运算符=>的右边参数(右值), 定义了执行代码(匿名委托中的谓词表达式)。
在匿名方法中,需要花括号和 return 语句(也就是=>右侧内容,表示参数类型及参数本身对应)。
在λ表达式中,不需要这些语法元素,因为它们是由编译器处理的。
如果λ运算符右边有多个语句,也可以使用花括号和return 语句。
提示:使用无参的 λ 表达式时,return 语句和花括号是可选的。但在 λ 表达式中仍可以使用这些语言结
构。详见第 7 章。
private static void LambdaExpressions() { List<Racer> champions = new List<Racer>(Formula1.GetChampions()); IEnumerable<Racer> brazilChampions = champions. Where(r => r.Country == "Brazil"). // 还有点的,r 的类型被省略了由解析器负责,=>的左边和匿名委托中执行的语句一样但是return省略了 OrderByDescending(r => r.Wins). //参照匿名委托 Select(r => r); foreach (Racer r in brazilChampions) { Console.WriteLine("{0:A}", r); } }
LINQ 查询
最后一个需要修改的是用新的LINQ 查询记号定义查询。
语句
from r in Formula1.GetChampions()
Where r.Country == "Brazil"
orderby r.Wins descending
select r; //虽然分行了,但还是表示只有一句,只有一个分号
是一个 LINQ 查询,子句 from、where、orderby、descending 和 select 都是这个查询中的预定义关键字。
编译器把这些子句映射到扩展方法上。这里的语法是
使用扩展方法 Where()、Orderby- Descending()和 Select()。λ表达式传送给参数。
Where r.Country == "Brazil" 转换到 Where(r => r.Country == "Brazil") (Lambda表达式表示的扩展方法)
.orderby r.Wins descending 转换到 OrderByDescending(r => r.Wins)
LinQ把方法本身的括号也去掉了,点也去掉了
提示:LINQ 查询是 C#语言中的一个简化查询记号,
编译器会编译查询表达式,调用扩展方法。查询表达式只是 C# 中的一个语法,但不需要修改底层的 IL 代码。
1.查询表达式必须以from 子句开头,以 select 或 group 子句结束。
2.在这两个子句之间,可以使用 where、orderby、join、let 和其他 from 子句。
private static void LinqQuery() { List<Racer> champions = new List<Racer>(Formula1.GetChampions()); var query = from r in Formula1.GetChampions() //from 开始 where r.Country == "Brazil" orderby r.Wins descending select r; //select 结尾 foreach (Racer r in query) { Console.WriteLine("{0:A}", r); } }
注意,变量 query 只指定了 LINQ 查询。
该查询不是通过这个赋值语句执行的,只要使用 foreach 循环访问查询,该查询就会执行。详见后面的内容。
意思就是说,Linq就好象一个方法一样,而query就好象调用这个方法的调用方法,调用就会执行。而不是执行一次获得了结果。
在前面的示例中,学习了 C# 3.0 语言特性,以及它们与 LINQ 查询的关系。下面深入探讨 LINQ 的特性。
推迟查询的执行
在运行期间定义查询表达式时,查询就不会运行。查询会在迭代数据项时运行。
意思是说Linq查询语句被定义了,并没有执行,而是在调用和使用这个Var 结果的时候才去调用,所以在定查询语句之后的期间,修改了结果集,那么结果还是会在最新的结果集状态下发生做用
再看看扩展方法 Where()。它使用 yield return 语句返回谓词为true 的元素。因为使用了 yieldreturn 语句,
所以编译器会创建一个枚举器,在访问枚举中的项后,就返回它们。
这是一个非常有趣、也非常重要的结果。在下面的例子中,创建了一个 String 元素集合,用名称 names 填
充它。接着定义一个查询,从集合中找出以字母 J 开头的所有名称。集合也应是排好序的。在定义查询时,
不会执行迭代。相反,迭代在 foreach 语句中进行,迭代所有的项。集合中只有一个元素 Juan 满足 where 表
达式的要求,即以字母 J 开头。迭代完成后,将 Juan 写入控制台。之后在集合中添加四个新名称,再次执行
迭代。 结果是添加后结果后的新运算结果
private static void DeferQuery1() { List<string> 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); } }因为迭代在查询定义时不会执行,而是在执行每个 foreach 语句时执行,所以可以看到其中的变化,如应
用程序的结果所示
重点:每次使用Var 定义的 namesWithJ 结果集的时候都会调用查询,就好象是一个方法调用
如果这不是你要的结果可以调用扩展方法 ToArray()、ToEnumerable()、ToList()等可以改变这个操作。
在示例中,扩展方法ToList() 迭代集合,返回一个实现了 IList<string>的集合。
这样即使在修改结果集,再使用结果变量也不会再调用查询了,这回返回的就是一个切实的结果。
private static void DeferQuery2() { List<string> names = new List<string> { "Nino", "Alberto", "Juan", "Mike", "Phil" }; IList<string> namesWithJ = (from n in names //返回IList 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); } }
在结果中可以看到,两次迭代中输出保持不变,但集合中的值改变了:
参考:
C#高级编程 第六版
源代码下载:LINQSamples.rar