【翻译】Pro LINQ Language Integrated Query in C# 2008 -- 第一章,第三节
提示可以帮助你开始
当我写这本关于LINQ的书时,我经常发现自己被搞糊涂,搞晕。
虽然有很多非常有用的资源对开发者有效的学习和使用LINQ,但是我需要提供一些提示可以帮助您开始。在某些方面,这些提示感觉应该在书的结尾处。毕竟,我还没解释这些要点的概念。但是仅仅在最后给你提示对于你开始读这本书时,可能看起来有点残酷。所以说,我认为你可能会在这章中发现一些有用的提示,即使你不能完全理解它。
使用Var关键字的混乱
字来获取一个匿名类序列为一个变量时,如果有时候你不知道这个序列是什么时,这是一个方便的方法获取代码来编译。
虽然我们非常喜欢 开发者完全知道包含在这个序列中的数据类型 对于 IEnumerable <T> 你应该任何时候知道数据类型 T ,尤其在你刚开始学习LINQ,他可以让你费解。 思考改变明确的状态类型 所有你使用var关键字代替。
如果发生因为类型不匹配导致的项目编译不通过的问题,考虑下改变明确的类型声明,使用var关键字代替
示例代码如下:
Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");
IEnumerable<?> orders = db.Customers
.Where(c => c.Country == "USA" && c.Region == "WA")
.SelectMany(c => c.Orders);
你可能不清楚 IEnumerable 序列的类型。但你知道它是许多 T 类型的IEnumerable序列,那 T 是什么呢 ? 一个很方便的小技巧,将分配查询结果赋值给一个指定的 Var 类型变量,然后获取变量类型,所以你知道了类型 T。请看示例 1-7 。
示例 1-7. 代码简单的使用了Var关键字
var orders = db.Customers
.Where(c => c.Country == "USA" && c.Region == "WA")
.SelectMany(c => c.Orders);
Console.WriteLine(orders.GetType());
在这个例子中,请注意现在指定Var关键字为orders变量类型。
运行结果:
System.Data.Linq.DataQuery`1[nwind.Order]
这里有一些编译器乱码,但是重要的是nwind.Order部分。
你现在知道如何获取一个nwind.Order序列的数据类型了吧。
如果在debugger下运行该例子时你将乱码throw出来,并检查orders变量,将会在本地窗体显示orders数据类型如下:
它使你更清楚你有一个nwind.Order序列。从技术上讲,你获取的是一个IQueryable<nwind.Order>,但是由于IQueryable<T> 继承了 IEnumerable<T> ,可以将IQueryable<nwind.Order> 复制给一个IEnumerable<nwind.Order> ,如果你喜欢。所以你要重写以前代码,并列举结果,就像示例 1-8.
示例1-8. 去除了示例 1-7 中显示类型部分的简单代码
IEnumerable<Order> orders = db.Customers
.Where(c => c.Country == "USA" && c.Region == "WA")
.SelectMany(c => c.Orders);
foreach(Order item in orders)
Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);
注意:代码运行之前你必须使用using命令引用System.Collections.Generic命名空间。
运行结果:
3/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store
5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store
…
4/17/1998 12:00:00 AM - 11032 - White Clover Markets
5/1/1998 12:00:00 AM - 11066 - White Clover Markets
使用 Cast 或者 OfType 操作旧集合
你会发现大多数 LINQ的标准查询操作能 只能调用 一个实现了IEnumerable<T> 接口的集合 。
没有一个旧的 C# 集合 — 那些 System.Collection 命名空间中的 — 实现 IEnumerable <T>。
所以问题就变为:在你现有的代码基础上,如何让旧的集合使用LINQ?
Cast和OfType,这里有两个标准操作专门用于此目的。两种操作都使用转换旧的集合为IEnumerable<T>序列。
看示例 1-9。
示例 1-9. 使用Cast操作转换一个旧集合为一个IEnumerable<T>
ArrayList arrayList = new ArrayList();
// Sure wish I could use collection initialization here, but that
// doesn't work with legacy collections.
arrayList.Add("Adams");
arrayList.Add("Arthur");
arrayList.Add("Buchanan");
IEnumerable<string> names = arrayList.Cast<string>().Where(n => n.Length < 7);
foreach(string name in names)
Console.WriteLine(name);
示例 1-10. 展示了使用OfType操作的示例
示例 1-10. 使用OfType操作
ArrayList arrayList = new ArrayList();
// Sure wish I could use collection initialization here, but that
// doesn't work with legacy collections.
arrayList.Add("Adams");
arrayList.Add("Arthur");
arrayList.Add("Buchanan");
IEnumerable<string> names = arrayList.OfType<string>().Where(n => n.Length < 7);
foreach(string name in names)
Console.WriteLine(name);
两个示例的运行结果都一样,如下:
Adams
Arthur
两个操作方法的不同之处在于,Cast操作将尝试强制转换集合中的每个项为指定的类型,装入输出的序列中。如果集合中有一个类型无法转换成指定类型,一个错误将抛出(报错)。OfType操作尝试将能转换成指定类型的输出项,装入输出的序列中。
比起OfType操作更喜欢Cast操作
为什么泛型被添加到 C# 最重要的原因之一是使语言能够有数据收集与静态类型检查。 泛型更重要的是(你希望一个集合,只能创建你需要的数据类型)在旧的集合中没有一种方法取保每项都是正确的,例如一个ArrayList,Hashtable,等等,都是相同和正确类型。该语言没有办法预防“添加一个Textbox对象到一个只存放Label对象的ArrayList中”的代码。
根据C# 2.0中泛型的介绍,C# 开发人员现在有一种方法明确说明,集合可以只包含一个指定的类型的元素。
当OfType或者Cast任意一个操作在一个旧的集合时,Cast需要集合中每一个数据对象是正确的类型,这是Cast将旧集合创建泛型集合时的一个缺陷。当使用Cast操作,如果许多对象是不能转换成指定数据类型,一个错误将抛出(报错)。然而使用OfType操作。会只将指定数据类型对象存储到输出IEnumerable<T>序列,并且不会抛出错误。最佳状况,是所有的对象都是正确的类型并存储输出序列中。最坏状况,是有些数据项将被跳过,但是使用了Cast操作时它将抛出一个错误。
不要设想一个查询是完美无缺的
在第三章中,我讨论LINQ查询通常是后执行,并当你要显示调用它,但它没有实际被执行。例如,下面这段来自于示例 1-1的代码:
from s in greetings
where s.EndsWith("LINQ")
select s;
foreach (var item in items)
Console.WriteLine(item);
虽然它显示查询发生,当该项目变量被初始化,事实不是如此。
因为Where和Select操作是后执行,所以在这里这个查询不是真实执行的。
查询仅仅是一个调用,声明,或者解释,但是不执行。该查询实际上会从它第一次获取一个需要的结果时发生。
这个通常代表当查询结果变量是枚举。在这个例子中,直到foreach声明执行为止都不需要一个来自查询的结果。
那就是这个查询将被执行的时间点。在这种方法中我们可以说查询是后执行的。
通常容易忘记许多查询操作是后执行的并且不执行,直到一个枚举序列为止。这意味着你也可以写一个不正确的查询,当结果序列是最终枚举时将抛出一个异常。那个枚举可能会在错误的查询之后很远的代码处报错。
示例 1-11 让我们来检测下代码。
示例 1-11. 查询,直到执行枚举时报错
Console.WriteLine("Before Where() is called.");
IEnumerable<string> ieStrings = strings.Where(s => s.Length == 3);
Console.WriteLine("After Where() is called.");
foreach(string s in ieStrings)
{
Console.WriteLine("Processing " + s);
}
我定义字符串数组的第三个项是一个null,并且当我不调用null.Length将不会抛出一个错误。
在执行查询的前后插入测试代码行。直到列举ieStrings序列明确调用第三项时出现异常。
运行结果如下:
Before Where() is called.
After Where() is called.
Processing one
Processing two
Unhandled Exception: System.NullReferenceException: Object reference not set to an
instance of an object.
…
真如你看到的,我调用Where操作没有报错。直到尝试列举序列的第三项时错误被抛出了。现在假设,如果序列ieStrings是要传递给后期的一个函数时,可能要用这个序列来填充一个下拉列表或其他控件。它可以很容易地认为由该函数造成的异常,而不是 LINQ 查询本身中的一个错误引起。
充分利用后期执行查询
我想说明一点,如果一个查询是一个后期执行查询,而该查询最终返回一个经常从数据源获取最新数据的 IEnumerable<T> (IEnumerable<T>对象是可以枚举通过的)序列。你不需要实际调用,或者就像我前面指出的,不用重新声明该查询。
在本书中大多数的代码示例,你将看到一个查询调用,并且返回一个对某些类型 T 存储IEnumerable <T>变量。 然后我通常调用 foreach 枚举 IEnumerable <T> 序列。 这是出于演示目的。 如果该代码执行多次,每次调用该实际的查询是不必要的操作。它可能会更有意义,因为在一次生命周期内只调用一个查询初始化方法并构造出所有的查询。然后你就可以枚举一个特定序列,而获取最新版本的查询结果。
使用DataContext日志
使用LINQ到SQL时,不要忘记使用SQLMetal生成的数据库类继承与System.Data.Linq.DataContext。这意味着,您的生成的 DataContext 类具有一些有用的内置功能,如 TextWriter 对象名字为日志。
日志对象的好处之一是,它将在 IQueryable<T> 变量被赋值之前输出一个等效的SQL语句。DataContext的日志对象将输出这个SQL语句。
示例 1-12. 使用DataContext.Log对象的一个例子
db.Log = Console.Out;
IQueryable<Order> orders = from c in db.Customers
from o in c.Orders
where c.Country == "USA" && c.Region == "WA"
select o;
foreach(Order item in orders)
Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);
输出的结果:
SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate],
[t1].[RequiredDate], [t1].[ShippedDate], [t1].[ShipVia], [t1].[Freight],
[t1].[ShipName], [t1].[ShipAddress], [t1].[ShipCity], [t1].[ShipRegion],
[t1].[ShipPostalCode], [t1].[ShipCountry]
FROM [dbo].[Customers] AS [t0], [dbo].[Orders] AS [t1]
WHERE ([t0].[Country] = @p0) AND ([t0].[Region] = @p1) AND ([t1].[CustomerID] =
[t0].[CustomerID])
-- @p0: Input String (Size = 3; Prec = 0; Scale = 0) [USA]
-- @p1: Input String (Size = 2; Prec = 0; Scale = 0) [WA]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1
3/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store
5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store
6/19/1997 12:00:00 AM - 10574 - Trail's Head Gourmet Provisioners
6/23/1997 12:00:00 AM - 10577 - Trail's Head Gourmet Provisioners
1/8/1998 12:00:00 AM - 10822 - Trail's Head Gourmet Provisioners
7/31/1996 12:00:00 AM - 10269 - White Clover Markets
11/1/1996 12:00:00 AM - 10344 - White Clover Markets
3/10/1997 12:00:00 AM - 10469 - White Clover Markets
3/24/1997 12:00:00 AM - 10483 - White Clover Markets
4/11/1997 12:00:00 AM - 10504 - White Clover Markets
7/11/1997 12:00:00 AM - 10596 - White Clover Markets
10/6/1997 12:00:00 AM - 10693 - White Clover Markets
10/8/1997 12:00:00 AM - 10696 - White Clover Markets
10/30/1997 12:00:00 AM - 10723 - White Clover Markets
11/13/1997 12:00:00 AM - 10740 - White Clover Markets
1/30/1998 12:00:00 AM - 10861 - White Clover Markets
2/24/1998 12:00:00 AM - 10904 - White Clover Markets
4/17/1998 12:00:00 AM - 11032 - White Clover Markets
5/1/1998 12:00:00 AM - 11066 - White Clover Markets