LINQ:.NET 语言集成查询






3.53/5 (8投票s)
.NET Framework 中新增的通用查询功能、LINQ、Lambda 表达式、扩展方法
目录
本系列第一部分将讨论以下内容:
- 引言
- 支持 LINQ 的语言特性
- 扩展方法
- Lambda 表达式
- 局部变量类型推断
- 匿名类型
- 对象初始化
引言
那么,我们直接开始写代码吧。
int[] array = new int[] { 1, 2, 3,
4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = from a in array
where a % 2 == 0
select a;
ObjectDumper.Write(evenNumbers);
上面声明了一个整数数组,并且只打印偶数。
以上代码的输出如下
2
4
6
8
10
ObjectDumper
是一个使用反射并进行 Console.WriteLine()
操作的实用类。ObjectDumper
是一个智能类,它可以接受任何对象,使用反射推断其类型,并将值打印到流中。默认情况下,流是控制台。因此,如果我有一个名为 Customer
的类,并且它有一个名为 Name
的 public
字段,然后我将该类的对象传递给 ObjectDumper
,它将以 Name=<customerName>
的方式打印。如果我传递一个集合,它会遍历集合中的所有项并打印它们各自的值。这个类也可以用于 .NET 2.0。
现在,让我们看看这段代码在 C# 编译器眼中是什么样的。架构师称这段代码为语法糖。如果你看这段代码,没有方法调用,没有对象,也没有 "." 运算符。
一旦 C# 编译器看到上面的代码,它就会将其翻译成如下所示:
var evenNumbers = array.Where(a => 0 == a % 2).Select(a => a);
现在,作为一个开发者,我很高兴看到一些对象和一个方法调用。但我仍然感到困惑,因为一个 int
数组没有 Where
和 Select
方法。微软是否为这个类型添加了这些新函数?而 "a => 0 == a % 2
" 是什么意思?
扩展方法
Where
和 Select
是扩展方法。 .NET 3.5 构建在 .NET 2.0 之上,它允许开发者在现有类/类型中添加他/她自己的函数。但是添加扩展方法有一些规则。你只能从 static
类中为类型添加方法。这个 static
类应该包含一个 static
方法,该方法将被调用为该类型的实例方法。让我们以一个例子来说明我的意思:
public static class MyExtensions
{
public static string Reverse(this string str)
{
StringBuilder sb = new StringBuilder(str.Length);
for (int i = str.Length - 1; i >= 0; i--)
sb.Append(str[i]);
return sb.ToString();
}
}
在这里,我们向 String
类添加了一个 Reverse
函数。现在让我们看看如何使用这个函数:
string s = "Hello LINQ";
Console.WriteLine(s.Reverse()); //will print "QNIL olleH"
注意事项
Reverse
函数是一个static
函数,被调用为实例函数。Reverse
函数接受一个string
作为参数,而函数调用是void
。- 注意函数有 "
this
" 作为其第一个参数。
当编译器看到对一个不是该类型成员函数的调用时,它会查找扩展方法并调用最匹配的函数。这意味着如果我有一个另一个命名空间,其中 Reverse
作为扩展方法,编译器将要么给出歧义错误,要么调用最接近函数调用的第一个函数。为了避免歧义错误,你可以显式地指定要调用的命名空间类和方法。在我们的例子中,当编译器看到这个调用时,它将其替换为:
Console.WriteLine(MyExtensions.Reverse(s));
Lambda 表达式
回到以下代码:
var even = array.Where(a => 0 == a % 2).Select(a => a);
现在我们知道 Where
和 Select
是扩展方法。但是 "" 是什么意思?
对于 .NET 2.0 开发者来说,上面的代码相当于下面的代码:
var e = array.Where(delegate(int a)
{ return 0 == a % 2; }).Select(delegate(int a) {return a;});
Where
扩展方法如下所示:
public delegate TR Func<T0, TR>(T0 a0);
public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
Func<T, bool> predicate)
{
if (source == null || predicate == null)
throw new ArgumentNullException();
foreach (T item in source)
if(predicate(item))
yield return item;
}
这个扩展方法表示它适用于所有 IEnumerable<T>
类型。它遍历所有项,只返回满足给定条件(谓词)的那些类型。
现在让我们看看 "Where(a => 0 == a % 2).Select(a => a);
"。箭头(=>
)运算符被引入,整个表达式被称为 lambda 表达式。让我们看更多关于 lambda 表达式的例子:
public delegate T Func<A0, A1, T>(A0 arg0, A1 arg1);
Func<int, int, int> f = (x, y) => x * y;
ObjectDumper.Write(f(5, 6));
上述代码的输出是 30
。
编译器所做的是推断 x
和 y
的类型,使其类型安全,然后创建一个匿名的委托函数并调用它。这对面向对象的人来说更有意义。
局部变量类型推断
再次回到主要查询:
var evenNumbers = from a in array
where a % 2 == 0
select a;
如果你还没有注意到,查询的输出被赋给了 var
。这个 var
在 Jscript 中不对应于对象,在 Jscript 中它表示一个对象。在 C# 中,它的类型在赋值时被推断出来。你不能这样做:
var unknowType = null;
这将导致编译错误。
那么 var
为什么重要呢?如果你看微软提供的绝大多数扩展方法,它们都返回 IEnumerable<T>
。所以,在你的代码中,每一次写查询,你也要写另一个封装它的类。这不是个好主意。所以你可以暂时使用 var
变量。考虑到这一点,你不能将 var
作为函数参数传递。因此,如果你想在函数外部使用查询结果,你必须定义自己的类型,除非你选择整个项。
匿名类型
这次我们来看一个不同的查询:
var contacts = from c in customers
where c.State == "WA"
select new { c.Name, c.Phone };
如果你看 select
语句,这里我们创建了一个全新的类型,甚至没有指定类型名称。在这种情况下,编译器将创建一个新类型,它将有两个 public
字段,并且字段的类型将从源类型推断出来。
注意,由于我们没有指定类型名称,我们将无法重用该类型。并且该类型的范围是它被使用的函数。一旦编译器创建了类型,它将使用源的值来初始化字段。
对象初始化
回到同一个查询:
var contacts = from c in customers
where c.State == "WA"
select new { c.Name, c.Phone };
在这种情况下,编译器创建了一个匿名类型,但它没有创建接受两个参数然后初始化其字段的构造函数。那么字段是如何初始化的呢?
让我们看另一个例子,这将使事情更清楚:
public class Point
{
private int x, y;
public int X { get { return x; } set { x = value; } }
public int Y { get { return y; } set { y = value; } }
}
Point a = new Point { X = 0, Y = 1 };
现在这段代码就像写成这样一样:
Point a = new Point();
a.X = 0;
a.Y = 1;
我想代码不言自明。
未来文章
- 本系列第二部分将讨论 LINQ to SQL。
- 本系列第三部分将讨论 LINQ to XML。
如需更多详情,您可以发送邮件至 SumitkJain@hotmail.com 与我联系。
历史
- 2007 年 8 月 27 日:初始发布