内存查询之外

内存查询之外

本章包括

n  Object LINQ 通用场景

n  动态查询

n  设计模式

n  性能考虑

 

通过其上面几章的学习,你已经能够相信自己能够写出有效的LINQ查询了。但是LINQ是一个海洋, 每一个变体都是一个岛屿。如果你想安全登陆这些岛屿,需要学习更多知识。你知道如何编写一个查询,但是却不知道如何编写一个高效的查询。本章中,我们将扩展前面学习的LINQ知识继续提高你的LINQ水平。

本章对计划使用LINQ的人非常重要。本章中的知识不只是介绍对象LINQ,也介绍了LINQ的其他方面。如XML LINQ。我们的一个目标是帮你识别LINQ内存查询的通用场景,并提供可用的解决方案。另一个目标是介绍LINQ设计模式,探索最佳实践。同样会处理你可能会担心的查询性能问题。

一旦你阅读了本章,你就应该为一探SQL LINQXML LINQ的准备了。

5.1 通用场景

我们已经非常确定, 你现在已经非常急切想使用LINQ进行真正的开发了。当你编写LINQ代码的时候。你会发现一些在那些示例中不曾遇到的问题。那些示例只能帮助你了解这个技术,而不能帮你解决你开发中每天遇到的问题。

5.1.1   查询非泛型集合

如果你仔细阅读了前面几章,你应该能够使用Object LINQ编写集合查询了。有一个问题是,你只能查询特定的集合。问题的源自Object LINQ被设计用于查询实现System.Collections.Generic.IEnumerable<T>接口的泛型集合。不过,大多数.NET Framework集合都实现了该接口.这包括System.Collections.Generic.List<T>类,数组,词典和队列等许多类。问题在于IEnumerable<T>是一个泛型接口,而并不是所有的类都是泛型的。

.NET2.0中有许多可用的泛型。但是你不可能避免的要对非泛型数据进行处理。如最早在.NET中使用的集合是System.Collections.ArrayList类型的数据。ArrayList是一个非泛型集合。它没有实现IEnumerable<T>接口。这表示我们不能对ArrayList使用LINQ吗?

如果你使用列表5.1的查询,你会得到一个编译错误。因为LINQ不支持books的类型。

列表 5.1    使用Object LINQ查询ArrayList 会导致编译错误

 

ArrayList books = GetArrayList();

 

var query =

from book in books

where book.PageCount > 150

select new { book.Title, book.Publisher.Name };

 

如果我们不能使用LINQ处理非泛型集合的话,那情况真是太糟糕了。需要有一个解决方案。当你知道这个戏法之后,LINQ处理非泛型集合就不是一个问题了。

假设你获取了非泛型集合,如ArrayList对象。当你想使用LINQ的时候,这个戏法就是使用Cast操作符,简单的说,Cast操作符接受一个IEnumerable而返回一个IEnumerable<T>。每当你使用LINQ操作非泛型集合的时候,你都需要Cast操作符。

列表5.2阐述了,如何使用Cast操作符将一个ArrayList转换为一个泛型集合,从而可以使用LINQ进行集合操作。

列表 5.2    使用Cast查询操作符查询一个ArrayList

 

ArrayList books = GetArrayList();

 

var query =

from book in books.Cast<Book>()

where book.PageCount > 150

select new { book.Title, book.Publisher.Name };

 

dataGridView.DataSource = query.ToList();

 

注意到使用Cast操作符可以很简单的让ArrayList支持LINQ查询。Cast操作符转换源序列为一个指定的类型,下面是Cast操作符的签名:

public static IEnumerable<T> Cast<T>(this IEnumerable source)

该方法遍历源序列,将每个元素类型转换为T并返回。如果源序列中的元素不能转换为目标类型,将会抛出InvalidCastException异常。

 

注意 如果目标类型是值类型,那么当源序列中的元素为null值时,将会抛出NullRef- erenceException异常.如果目标是引用类型,将不会抛出异常。

 

非常有趣的是,由于查询表达式的特性,我们可以简化以上示例代码,我们不需要显示调用Cast操作符,在C#查询表达式中,我们的ArrayList对象会自动调用Cast操作符,所以列表5.3和列表5.2的代码是等价的,但是更简短。

列表 5.3    在查询表达式中使用类型声明使得查询ArrayList 变得简单

var query =

from Book book in books where book.PageCount > 150

select new { book.Title, book.Publisher.Name };

 

对于DataSet对象,我们可以使用相同的技术。例如,下面是对DataTable的行进行的LINQ查询。

 

from DataRow row in myDataTable.Rows

where (String)row[0] == "LINQ"

select row

 

除了使用Cast操作符,我们还可以使用OfType操作符。不同的是,OfType只会返回一个集合中指定类型的对象。例如,如果你有一个包含BookPublisher对象的ArrayList对象,调用theArrayList.OfType<Book>(),之返回该ArrayList中所有的Book对象。

随着时间流逝,你可能会看不到非泛型集合的影子,但是如果你想要使用LINQ对非泛型集合进行查询,你就会用到CastOfType两个朋友。

5.1.2   根据多个条件分组

当我们在第四章中介绍分组的时候,我们根据一个简单属性进行分组,如下查询:

var query = from book in SampleData.Books group book by book.Publisher;

这里,我们使用出版社对书籍分组,但是如果要如何使用多个标准进行分组呢?如果我们想要使用出版社和主题分组,你会失望的发现,LINQ查询表达式语法不支持在一个分组语句中接受多标准查询。

下面的查询是无效的:

var query1 =

from book in SampleData.Books

group book by book.Publisher, book.Subject;

var query2 =

from book in SampleData.Books

group book by book.Publisher

group book by book.Subject;

这并不表示不可以在一个查询表达式中使用多个标准进行分组。我们可以使用一个匿名类型来指定分组的成员。这看上去好像有些奇怪,所以让我们一步一步讲解。

考虑根据出版社和主题分组。这将会产生如下示例数据:

Publisher=FunBooks         Subject=Software development

Books: Title=Funny Stories       PublicationDate=10/11/2004...

Publisher=Joe Publishing Subject=Software development

Books: Title=LINQ rules   PublicationDate=02/09/2007...

Books: Title=C# on Rails  PublicationDate=01/04/2007...

Publisher=Joe Publishing Subject=Science fiction

Books: Title=All your base are belong to us PublicationDate=05/05/2006...

Publisher=FunBooks         Subject=Novel

Books: Title=Bonjour mon Amour PublicationDate=18/02/1973...

为了获得这种结果,你的分组子句需要包含一个匿名类型,该匿名类型包含PublisherSubject两个属性。列表5.4中,我们使用组合键代替了简单键。

列表 5.4    根据publisher subject分组Book

var query =

from book in SampleData.Books

group book by new { book.Publisher, book.Subject };

这种查询结果是一个分组的集合。每个分组包含一个key(匿名类型的实例)和一个匹配键值的书籍对象的序列。

为了产生更有意义的结果,我们可以使用select子句改进查询,如列表5.5所示:

 

列表 5.5    group子句中使用into关键字

 

var query =

from book in SampleData.Books

group book by new { book.Publisher, book.Subject }

into grouping

select new {

Publisher = grouping.Key.Publisher.Name,

Subject = grouping.Key.Subject.Name,

Books = grouping

};

 

Into关键字引入了一个我们可以在select或者其它子句中使用的分组变量。分组变量中包含了分组的键值,我们可以通过key属性访问。同时也包含了分组元素。分组元素可以通过对分组变量遍历获得。分组变量实现了IEnumerable<T>接口,类型Tgroup关键字后边指定变量的类型。

为了显示结果,你可以使用ObjectDumper类:

 

ObjectDumper.Write(query, 1);

 

分组结果元素的类型可以与源元素类型不同。例如,你可以只获取书籍的标题而不是整个书籍对象。在这种情况下,我们可以使用列表5.6的查询。

 

列表 5.6    通过分组获取Book标题而不是Book对象

 

var query =

from book in SampleData.Books

group book.Title by new { book.Publisher, book.Subject }

into grouping

select new {

Publisher = grouping.Key.Publisher.Name,

Subject = grouping.Key.Subject.Name,

Titles = grouping

};

 

进一步说, 你可以使用匿名类型来指定结果元素的内容。在下面的查询中,我们指定了要在结果中获取出版社名称和以主题分组的书籍列表。

var query =

from book in SampleData.Books

group new { book.Title, book.Publisher.Name } by book.Subject into grouping

select new {Subject=grouping.Key.Name, Books=grouping };

在这个查询中,为了保持简单,我们只使用主题作为分组的键。但是你可以使用匿名类型作为键值,就像前面讲的一样。

备注 匿名类型在其他查询字句中可以用作组合键,如joinorderby子句。

5.1.3    动态查询

当你使用LINQ的时候,可能有一个你非常担心的问题,你的第一个查询,看上去非常固定。

让我们看一个非常典型的查询:

from book in books

where book.Title = "LINQ in Action"

select book.Publisher

这种构造可能给你一种这样的印象,LINQ查询只能用于特定的查询。

自定义排序

对查询结果进行排序是动态查询应用的一个情况。排序顺序可以通过orderby子句或者OrderBy操作符指定。下面是一个按照标题对书籍排序的查询表达式示例:

from book in SampleData.Books

orderby book.Title

select book.Title;

下面是使用操作符语法的等价查询:

SampleData.Books

.Orderby(book => book.Title)

.Select(book => book.Title);

问题在于, 这些查询中排序顺序都是硬编码的:这样查询的结果总是按照标题排序。如果你希望动态的指定排序顺序呢?

假定你创建了一个应用程序,希望让用户决定如何对书籍排序,用户界面如图5.1所示。

5.1    允许用户选择排序顺序的用户界面

 

 

你可以实现一个方法,该方法接受排序方法代理作为参数。这个参数可以被OrderBy操作符调用。下面是OrderBy操作符的签名:

OrderedSequence<TElement> OrderBy<TElement, TKey>(

this IEnumerable<TElement> source, Func<TElement, TKey> keySelector)

这表明,提供给OrderBy操作符的代理是Func<TElement, TKey>。在我们这种情况下,源序列是Book对象。所以TElementBook类。键值是动态选择的,它可以是一个字符串或者一个整数。为了支持这两种类型的键值,你可以使用泛型方法,TKey是一个类型参数。

列表5.9显示了如何使用一个方法采用一个排序键选择器作为一个参数。

列表 5.9    使用参数启用自定义排序的方法

 

void CustomSort<TKey>(Func<Book, TKey> selector)

{

var books = SampleData.Books.OrderBy(selector);

ObjectDumper.Write(books);

}

 

该方法同样可以使用查询表达式实现,如5.10

列表 5.10    在查询表达式中使用参数启用自定义排序

void CustomSort<TKey>(Func<Book, TKey> selector)

{

var books =

from book in SampleData.Books

orderby selector(book)

select book;

ObjectDumper.Write(books);

}

该方法可以使用如下:

CustomSort(book => book.Title);

or

CustomSort(book => book.Publisher.Name);

一个问题是, 该方法不能对数据降序排序。为了支持降序排序,CustomSort方法需要构建为列表5.11所示。

列表 5.11    使用参数进行自定义升序或者降序排序

void CustomSort<TKey>(Func<Book, TKey> selector, Boolean ascending)

{

IEnumerable<Book> books = SampleData.Books;

books = ascending ? books.OrderBy(selector)

: books.OrderByDescending(selector);

 ObjectDumper.Write(books);

}

这次,方法只能显式调用操作符来实现。查询表达式不能包含升序或者降序参数,因为它需要一个静态的orderby子句。

额外的ascending参数允许我们在OrderByOrderByDescending操作符之间选择。可以使用如下调用来进行排序选择:

CustomSort(book => book.Title, false);

最后,我们有一个完整的CustomSort方法,使用动态查询来处理通用场景。所有要做的就是使用switch语句来决定用户的排序选择,如列表5.12所示。

列表 5.12    选择自定义排序方法的Switch 语句

 

switch (cbxSortOrder.SelectedIndex)

{

case 0:

CustomSort(book => book.Title);

break;

case 1:

CustomSort(book => book.Title, false);

break;

case 2:

CustomSort(book => book.Publisher.Name);

break;

case 3:

CustomSort(book => book.PageCount);

break;

}

 

条件化构建查询

前一个示例显示了如何根据变化的值自定义查询,新的示例将会为你展示如何动态添加操作符到一个查询中。这种技术允许你基于用户的查询形成查询。

让我们考虑一个通用场景。在大多数应用程序中,数据并不是直接向用户展示。从数据库或者XML或者其他数据源取得数据以后,数据是经过过滤,排序和格式化处理。这就是LINQ的意义所在,LINQ查询允许我们通过声明性的语法实现所有这些数据操作。大多数情况下,数据是通过用户的输入用动态的构建的。

作为一个示例,一个典型的查询界面包含了用户可以输入的标准的集合。如图5.4所示。

5.4    根据多个标准查询Book对象

 

为了取得用户输入的标准,我们可以编写如下查询,如列表5.13所示。

列表 5.13   基于用户输入构建查询

var query = SampleData.Books

.Where(book => book.PageCount >= (int)cbxPageCount.SelectedValue)

.Where(book => book.Title.Contains(txtTitleFilter.Text))

 

if (cbxSortOrder.SelectedIndex == 1)

query = query.OrderBy(book => book.Title);

else if (cbxSortOrder.SelectedIndex == 2)

query = query.OrderBy(book => book.Publisher.Name);

else if (cbxSortOrder.SelectedIndex == 3)

query = query.OrderBy(book => book.PageCount);

 

query = query.Select(

book => new { book.Title, book.PageCount,Publisher=book.Publisher.Name });

dataGridView1.DataSource = query.ToList();

 

为了代码重用和清晰,最好将该查询作为一个独立的方法,如列表5.14所示:

列表 5.14    将动态查询重构到一个方法中去

 

void ConditionalQuery<TSortKey>(

int minPageCount, String titleFilter, Func<Book, TSortKey> sortSelector)

{

var query = SampleData.Books

.Where(book => book.PageCount >= minPageCount.Value)

.Where(book => book.Title.Contains(titleFilter))

.OrderBy(sortSelector)

.Select(

book => new { book.Title, book.PageCount,Publisher=book.Publisher.Name });

 

dataGridView1.DataSource = query.ToList();

}

该方法可使用如列表5.15中的方法调用。

列表 5.15   调用条件化查询

 

int? minPageCount;

string titleFilter;

 

minPageCount = (int?)cbxPageCount.SelectedValue;

titleFilter = txtTitleFilter.Text;

if (cbxSortOrder2.SelectedIndex == 1)

{

ConditionalQuery(minPageCount, titleFilter, book => book.Title);

}

else if (cbxSortOrder2.SelectedIndex == 2)

{

ConditionalQuery(minPageCount, titleFilter, book => book.Publisher.Name);

}

else if (cbxSortOrder2.SelectedIndex == 3)

{

ConditionalQuery(minPageCount, titleFilter, book => book.PageCount);

}

else

{

ConditionalQuery<Object>(minPageCount, titleFilter, null);

}

一切都很好,但是我们的示例并不完整,我们没有实现我们灵活的查询。事实上,我们有一个小问题。如果用户并不提供所有的标准的值时,将会发生甚么呢?我们得不到正确的结果,因为方法并没有能力处理空值。

我们需要负责测试在此标准下是否有值。当这种标准没有值时,我们简单的排除对应的查询子句,实际上,如果你看以下在列表5.16中新版本的方法。你将会看到我们只是匆匆的添加了一个一个的子句。

列表 5.16    条件查询方法的完整版本

void ConditionalQuery<TSortKey>(

int? minPageCount, String titleFilter, Func<Book, TSortKey> sortSelector)

{

IEnumerable<Book> query;

 

query = SampleData.Books;

if (minPageCount.HasValue)

query =query.Where(book => book.PageCount >= minPageCount.Value);

 

if (!String.IsNullOrEmpty(titleFilter))

query = query.Where(book => book.Title.Contains(titleFilter));

 

if (sortSelector != null)

query = query.OrderBy(sortSelector);

 

var completeQuery = query.Select(

book => new { book.Title, book.PageCount,Publisher=book.Publisher.Name });

 

dataGridView1.DataSource = completeQuery.ToList();

}

 

在运行时创建查询

在前面的示例中,我们为你演示了如何创建动态查询。因为该查询中的一些值并不是在编译时就能确定。在更高级的场景中,你可能完全的动态创建查询。假如你的应用程序需要根据一个来自XML或者远程应用程序的描述来创建查询。在这种情况下,我们需要表达式树的帮忙。

假定如下的XML片段描述了要查询的条件:

 

<and>

<notEqual property="Title" value="Funny Stories" />

<greaterThan property="PageCount" value="100" />

</and>

如果我们写一段查询来匹配这些条件,看上去是这样:

var query =

from book in SampleData.Books

where (book.Title != "Funny Stories") && (book.PageCount > 100)

select book;

这是一个典型的在编译时就确定的查询。然而,如果XML是在运行时提供给我们的应用程序,这种查询就无能为力。解决的办法时使用表达式树。

如第三章中所讲,创建表达式树的最简单的方法就是让编译器将使用Expression<TDelegate>类声明的lambda表达式转换为一系列的工厂方法调用,这些方法将会产生表达式树。在运行时,为了创建动态查询,你可以利用表达式树的优点。你可以使用工厂方法(它们是Expression<TDelegate>类的静态方法)“滚动”你的表达式树并且在运行时将其编译为lambda表达式。

列表 5.17 在运行时动态创建了与前一个查询等价的查询

列表 5.17    使用表达式树在运行时动态创建查询

var book = Expression.Parameter(typeof(Book), "book");

var titleExpression = Expression.NotEqual(

Expression.Property(book, "Title"), Expression.Constant("Funny Stories"));

 

var pageCountExpression = Expression.GreaterThan(

Expression.Property(book, "PageCount"), Expression.Constant(100));

 

var andExpression = Expression.And(titleExpression,pageCountExpression);

var predicate = Expression.Lambda(andExpression, book);

var query = Enumerable.Where(SampleData.Books,

(Func<Book, Boolean>)predicate.Compile());

列表代码创建一个表达式树并描述了过滤条件。通过不断添加新的表达式,最后得到一个完整的表达式。后两句代码将表达式树转换为代码,形成了一个可执行的查询。代码中的查询变量(query)可以像其他LINQ查询一样使用。

当然,列表5.17使用了应编码的值如:“Title,Funny  Stories, PageCount, 和“100”。在真正的应用程序中,这些值可以取自XML文档或者其他信息源。表达式树时一个高级的主题。我们并不想深入探讨如何在动态查询的环境中使用它们。但是一旦你掌握了它们,就能发现它的强大能力。你可以参考LINQ to Amazon13章)示例来学习如何使用表达式树。

我们要阐述的最后一个通用场景是如何用LINQ查询处理文本文件。你已经知道如何查询内存集合,但是怎么样才能查询文本文件呢?我们需要另一个LINQ的变体吗?

5.1.4    LINQ 处理文本文件

LINQ的各个变体用于处理不同类型的数据和数据结构, 你已经知道了最主要的变体: Object LINQ DataSet LINQ XML LINQ SQL LINQ。如果要用LINQ查询文本文件,你将如何实现?答案是:Object LINQ足以应付我们面对的问题。

下面的示例显示了如何从一个CSV文件中提取信息(示例来自Eric White,一个来自微软的软件工程师)。

注意 CSV表示用逗号分割的值。在一个CSV文件中,不同的字段用分号分割。

列表5.18显示了示例中用到的CSV文件

列表 5.18    包含书籍信息的示例CSV文档

#Books (format: ISBN, Title, Authors, Publisher, Date, Price)

0735621632,CLR via C#,Jeffrey Richter,Microsoft Press,02-22-2006,59.99

0321127420,Patterns Of Enterprise Application Architecture,Martin Fowler,Addison-Wesley, 11-05-2002,54.99

0321200683,Enterprise Integration Patterns,Gregor Hohpe,Addison-Wesley,10-10-2003,54.99

0321125215,Domain-Driven Design,Eric Evans,Addison-Wesley Professional,08-22-2003,54.99

1932394613,Ajax In Action,Dave Crane;Eric Pascarello;Darren James,Manning Publications,10-01-2005,44.95

这个CSV文件包含了Book信息。为了读取CSV的数据,第一步就需要打开文件并获取它包含的行。一个简单的解决方案是使用File.ReadAllLines方法,这是一个静态方法。该方法从文本文件中读取所有行并返回一个字符串数组。第二步就是进行过滤, 可以使用where子句很容易实现。下面是该查询的开始部分:

from line in File.ReadAllLines("books.csv")

where !line.StartsWith("#")

这里,我们使用了File.ReadAllLines方法返回的字符串数组作为源序列。而且忽略了以#开始的行。下一步是将每行进行分割。为了做到这一点,我们可是用string对象上的Split方法。Split方法返回由分隔符分割的字符串数组。这里,分隔符是逗号。

我们需要引用分割后的每个字符串,在这里,最重要的是需要保证只执行一次分割操作。所以这是let子句的一种典型应用。Let子句使用一个标识符保存计算后的值。一旦我们分割了一行,我们就可以使用一个匿名对象保存这个值。

列表 5.19 显示了完整查询

列表 5.19    查询CSV中的书籍信息

from line in File.ReadAllLines("books.csv")

where !line.StartsWith("#")

let parts = line.Split(',')

select new { Isbn=parts[0], Title=parts[1], Publisher=parts[3] };

下面是使用ObjectDumper显示的查询结果:

 

Isbn=0735621632  Title=CLR via C#  Publisher=Microsoft Press

Isbn=0321127420  Title=Patterns Of Enterprise Application Architecture Publisher=Addison-Wesley

Isbn=0321200683  Title=Enterprise Integration Patterns  Publisher= Addison-Wesley

Isbn=0321125215  Title=Domain-Driven Design  Publisher=Addison-Wesley

Isbn=1932394613  Title=Ajax In Action  Publisher=Manning Publications

如此简单,就可以完成LINQ操作文本文件的操作, Object LINQ对我们来说已经足够用了。

警告:这是示例使用了一种非常天真的方法来处理CSV文件,它没有处理其它CSV的高级特性。

此外,该查询有一些不好的性能问题。我们将会在5.3.1中为你展示如何解决此问题。为了优化这种通用的操作,经常会创建一些设计模式。下一节将会给你一些应用到LINQ上的设计模式的概览。我们将会介绍一些广泛应用在LINQ查询中设计模式:功能构建模式。

5.2 设计模式

像其它技术一样,LINQ的设计也可以被重用。这些设计最后变为文档化的设计模式,可以被方便和简单的重用。有关设计设计模式的概念,请参考“四人帮”的《设计模式》一书。

我们要讲述的应用到LINQ的设计模式包括Functional Construction ForEach模式。

5.2.1   Functional Construction 模式

我们要展示的第一种设计模式使用了集合初始化器和组合查询。这种查询在LINQ查询中广泛使用,特别是在XML LINQ中。模式的名称是Functional Construction,因为它用于构建一个对象图表或者对象树,使用了类似与LISP语言(功能性语言)的代码结构来完成这个功能。

为了引入Functional Construction模式,我们重用了LINQ查询文本文件的示例。下面是我们使用的查询:

from line in File.ReadAllLines("books.csv")

where !line.StartsWith("#")

let parts = line.Split(',')

select new { Isbn=parts[0], Title=parts[1], Publisher=parts[3] };

我们没有处理作者信息,因为它需要一点额外的工作。我们需要得到如下结果:

 

Isbn=0735621632  Title=CLR via C#  Publisher=Microsoft Press Authors: FirstName=Jeffrey  LastName=Richter

Isbn=0321127420  Title=Patterns Of Enterprise Application Architecture Publisher=Addison-Wesley Authors: FirstName=Martin  LastName=Fowler

Isbn=0321200683  Title=Enterprise Integration Patterns  Publisher= Addison-Wesley Authors: FirstName=Gregor  LastName=Hohpe

Isbn=0321125215  Title=Domain-Driven Design  Publisher=Addison-Wesley Professional Authors: FirstName=Eric  LastName=Evans

Isbn=1932394613  Title=Ajax In Action  Publisher=Manning Publications Authors: FirstName=Dave  LastName=Crane Authors: FirstName=Eric  LastName=Pascarello Authors: FirstName=Darren  LastName=James

不想文本文件中其它字段,一本书可能有多个作者。如果再次阅读列表5.18的内容,可以看到作者之间用分号分隔:

Dave Crane;Eric Pascarello;Darren James

如同我们在整行中作的一样,我们可以将author字符串分割为一个字符串数组,数组的每个元素都只包含一个作者。此后又将每个作者的名字分割为first namelast name,最后将解析出的作者信息包装到Author属性中。

列表5.20显示了整个查询。

列表 5.20    使用声明性的方法解析CSV文件,其中使用了匿名类型

var books =

from line in File.ReadAllLines("books.csv")

where !line.StartsWith("#")

let parts = line.Split(',')

select new {

Isbn = parts[0],

Title = parts[1],

Publisher = parts[3],

Authors =

from authorFullName in parts[2].Split(';')

let authorNameParts = authorFullName.Split(' ')

select new {

FirstName = authorNameParts[0],

LastName = authorNameParts[1]}};

 

ObjectDumper.Write(books, 1);

 

在这个查询中,我们使用了匿名类型来保存结果,但是我们也可以使用已定义类型,列表5.21重用了类型Book, Publisher, Author

 

列表 5.21    使用现有类型解析CSV文件

 

var books =

from line in File.ReadAllLines("books.csv")

where !line.StartsWith("#")

let parts = line.Split(',')

select new Book {

Isbn = parts[0],

Title = parts[1],

Publisher = new Publisher {

Name = parts[3] },

Authors =

from authorFullName in parts[2].Split(';')

let authorNameParts = authorFullName.Split(' ')

select new Author {

FirstName=authorNameParts[0],

LastName=authorNameParts[1]}};

有趣的事情是Authors属性是使用一个嵌套查询初始化的。由于LINQ是可组合的,所以LINQ查询可以任意嵌套。子查询的结果转换为一个IEnumerable<Author>类型。该模式通常被称作“转换模式”。因为该模式通常用于从源对象创建新的对象结构。它允许我们编写声明式代码而不是命令式代码。如果你不采用这种方法,你需要写很多命令式代码。

列表 5.22 等价于列表 5.21所示代码的命令式代码

列表 5.22    使用命令式代码解析CSV 文件

List<Book> books = new List<Book>();

foreach (String line in File.ReadAllLines("books.csv"))

{

if (line.StartsWith("#"))

continue;

 

String[] parts = line.Split(','); Book book = new Book();

book.Isbn = parts[0];

book.Title = parts[1];

Publisher publisher = new Publisher();

publisher.Name = parts[3];

book.Publisher = publisher;

List<Author> authors = new List<Author>();

 

foreach (String authorFullName in parts[2].Split(';'))

{

String[] authorNameParts = authorFullName.Split(' ');

Author author = new Author();

author.FirstName = authorNameParts[0];

author.LastName = authorNameParts[1];

authors.Add(author);

}

 

book.Authors = authors;

books.Add(book);

}

如上所见,Functional Construction模式提供了一种更简洁的方式。当然,如果已经定义了Book PublisherAuthor类,那么区别就会小一些。实际上,真正的区别是其它方面的。比较这两段代码可以看出,声明式方法注重你要获得什么而不是如何获得。Functional Construction模式使得代码外形更像结果。我们可以从列表5.21的源代码中看到结果数据的结构。在第四部分中,你会看到这种模式被大量使用来构建XML

5.2.2    ForEach 模式

当迭代紧跟在查询之后的时候,ForEach模式允许你编写更简短的代码。在ForEach模式出现之前,典型的LINQ代码如列表 5.23所示.

列表 5.23    执行和便利LINQ查询的标准代码

var query =

from sourceItem in sequence

where some condition

select some projection

 

foreach (var item in query)

{

// work with item

}

看过这段代码就会有一个疑问:是否有一个方法能让我们在查询内迭代来取代foreach循环呢?答案是LINQ中没有一个操作符能帮助我们做到这点。但是你可以自己完成这个功能。你可以创建ForEach操作符来处理你的问题。如列表5.24所示。

列表 5.24    对源序列执行一个函数的ForEach 查询操作符

public static void ForEach<T>( this IEnumerable<T> source, Action<T> func)

{

foreach (var item in source)

func(item);

}

 

ForEachIEnumerable<T>的一个简单的扩展方法,ForEach操作符可以按照如下语法使用在一个查询中,如列表 5.25所示。

列表 5.25    在一个方法中使用ForEach 查询操作符

SampleData.Books

.Where(book => book.PageCount > 150)

.ForEach(book => Console.WriteLine(book.Title));

 

ForEach 还可以按照如下语法使用,如列表 5.26.

列表 5.26    ForEach 查询中使用查询表达式

 

(from book in SampleData.Books where book.PageCount > 150 select book)

.ForEach(book => Console.WriteLine(book.Title));

在这些示例中,ForEach中只有一个语句。由于lambda表达式支持语句体。在ForEach中调用多个语句是可能的。如列表5.27所示。

列表 5.27    在调用ForEach时使用多语句

SampleData.Books

.Where(book => book.PageCount > 150)

.ForEach(book => {

book.Title += " (long)";

Console.WriteLine(book.Title);

});

如此使用ForEach操作符,可以很好的与查询集成。

 

警告 ForEach 不能应用在VB中, 因为VB中的lambda不支持语句体

现在我们已经知道了通用的场景和设计模式,到了讨论第二个主题的时候了。你已经学会使用Object LINQ编写一些简单或者复杂的查询,当时当你在产品环境使用LINQ的时候,你需要注意一个重要的问题:性能问题。你需要确保使用的LINQ查询时高效的。这就是我们下面所要讲述的。

5.3 性能考虑

LINQ的主要优势不是允许你做新的事情,而是允许你用更简单,简洁的方法做同样的事情。但是代价往往需要损失性能,LINQ也不例外。本章的目的时让你知道LINQ查询潜在性能问题。并且提供一些图标使你对LINQ对性能的影响有一个整体的了解。同样强调了使用LINQ时可能犯的错误。如果你知道这些可能的错误,你就可以避免它们。

大多数时候,一个任务可以有很多方法来完成。有时候,选择只是与一个人的口味相关。但是有时候作出正确的选择将会成为关键,它会影响程序的行为。

在本节中,我们将测试使用LINQ时的性能问题。同时比较使用和不使用LINQ的代码,目标是在效率和可读性方面进行比较。你需要了解影响性能的各个方面。我们将再次以LINQ处理文本文件的示例作为开始。

5.3.1   偏爱流方法

前面所述的LINQ处理文本的示例有一个潜在的问题:ReadAllLine的使用。这个方法返回CSV文件中的所有行数据。对于小文件,这是没有问题的, 但是假如有大量的行。该程序会在内存中分配一个巨大的数组!

此外,这个查询并没有使用标准的LINQ延迟查询。通常,查询将被延迟。这表示在我们迭代开始以前,查询并没有被执行。而ReadAllLines则立即执行并将所有内容载入内存。所以会占用大量内存,而且在载入内存后,我们并没有立即进行处理。

Object LINQ设计用来支持延迟执行查询。流方法的使用节约了资源。只要有可能,我们就应该使用此方法。使用.NET Framework我们可以有多种方法从文件中读取文本。File.ReadAllLines只是其中简单的一个。更好的解决办法是使用流方法进行文件载入。可以使用StreamReader对象做到这点。它允许我们节约资源,以一种更平稳的方式执行读取。为了在查询中使用StreamReader,需要创建一个更优雅的解决方案。如列表5.28

列表 5.28    使用StreamReader读取文本文件

 

public static class StreamReaderEnumerable

{

public static IEnumerable<String> Lines(this StreamReader source)

{

String line;

 

if (source == null)

throw new ArgumentNullException("source");

 

while ((line = source.ReadLine()) != null)

yield return line;

}

}

该查询操作符作为StreamReader类的扩展方法。它使用StreamReader提供的方法一行一行的读取信息,但是并不一次全部载入所有信息。在我们的查询中使用这种技术很简单,如列表5.29所示。

列表 5.29   使用流方法的查询操作符

 

using (StreamReader reader = new StreamReader("books.csv"))

{

var books =

from line in reader.Lines()

where !line.StartsWith("#")

let parts = line.Split(',')

select new {Title=parts[1], Publisher=parts[3], Isbn=parts[0]}

 

ObjectDumper.Write(books, 1);

}

这个方法让你在处理大文件的同时保持一个较小的内存用量。这是为了提高你的查询性能需要注意的一种问题。写一个差劲的查询很简单。

5.3.2   当心立即执行

大多数标准的查询操作符支持延迟查询,这种特性减轻了资源的压力。但是有些查询操作符却不支持延迟查询。这些查询操作符的一个行为就是需要遍历所有序列元素。

一般情况下,这些操作符不是返回一个序列而是返回一个标量值。其中包括聚合操作符(Aggregate, Average, Count, LongCount, Max, Min, and Sum)。这并不奇怪,因为聚合就是处理集合种的所有元素然后产生一个标量值。

此外,另外一些返回序列的操作符也需要迭代序列中的所有元素。如OrderBy, OrderByDescending, Reverse。这些操作符改变了源序列中元素的顺序。为了知道如何对所有元素排序,这些操作符需要完全迭代整个序列。

为了详细分析问题所在,请看列表5.30所示代码:

列表 5.30    解析CSV文档的代码

 

using (StreamReader reader = new StreamReader("books.csv"))

{

var books =

from line in reader.Lines() where !line.StartsWith("#") let parts = line.Split(',')

select new {Title=parts[1], Publisher=parts[3], Isbn=parts[0]}

 

foreach (var book in books)

{

...Work with book objects

}

}

如果你运行这段代码,下面就是所发生的处理:

1            循环开始,使用Lines操作符从文件中读取一行

a)         如果没有更多的行处理, 处理将停止

2            Where操作符在行数据上执行

a)         如果行以#开始,就是注释行,所以该行将被跳过。执行回到第一步。

b)         如果该行不是注释行,处理将继续。

3            行被分割成多个部分

4            Select操作符创建了一个对象

5            Foreach语句用来处理当前book对象。

6            处理回到第一步继续执行

 

注意 如果你使用Visual Studio的调试功能,你就能看到一步一步的执行过程,我们推荐用这种方式来让你熟悉LINQ的执行方式

如果你决定通过orderby子句使用不同的顺序来处理文件,或者在查询中调用的Reverse操作符。那么处理顺序将会改变。假如你添加了对Reverse的调用。

...

from line in reader.Lines().Reverse()

...

现在,查询按照如下顺序执行:

 

1            Reverse 操作符执行

a)         Reverse方法遍历所有行

2            一个基于Reverse方法返回的序列的循环开始了。

a)         如果没有行可以处理了,处理将会停止。

3            Where操作符在当前行执行。

a)         如果行以#开始,则跳过该注释行。执行回到第一步。

b)         如果该行不是注释行,那么处理将继续。

4            行被分割成多个部分。

5            Select操作符创建了一个对象

6            Foreach语句对当前Book对象进行处理。

7            处理将会在第二步继续。

 

现在可以看到,Reverse操作符打断了优秀的管道流,因为它在处理一开始就把所有行都载入到了内存中。需要确保确实有必要在查询中使用此类操作符。至少应该注意到这些操作符对查询的影响。否则应用程序很差的性能会让你感到很惊讶。

许多转换操作符有同样的行为,这些操作符是ToArray, ToDictionary, ToList, ToLookup。它们都返回序列,但是从源序列创建了包含所有元素的集合,所以会遍历整个序列。

5.3.3    Object LINQ对伤害我代码的性能吗?

有时候,Object LINQ提供的功能可能不会与你的应用完全对应,考虑这个示例,你要从一个集合中查找一个指定的属性值最大的对象。首先,你会想到使用Max操作符。但是Max操作符在这种情况下不可用。因为它返回的是最大值,而不是具有最大值的对象。我们有很多方法可以处理这种情况,如下所示:

Options

第一个选择就是使用简单的foreach 循环,如列表 5.31.

列表 5.31    使用foreach 来得到页数最多的book对象

 

Book maxBook = null;

foreach (var book in books)

{

if ((maxBook == null) || (book.PageCount > maxBook.PageCount))

maxBook = book;

}

这种方法相当直接。它的复杂度是O(n)。如果我们对这个序列一无所知,在数学上,这种方法就是最好的方法。

第二个选择就是对集合排序并选择第一个元素,如列表5.32.

列表 5.32    使用  sorting  First 来完成查询工作

var sortedList =

from book in books

orderby book.PageCount descending

select book;

 

var maxBook = sortedList.First();

使用这种解决方案,操作的时间复杂度为O(n log n)

第三种选择是使用一个子查询,如列表 5.33.

列表 5.33    使用子查询进行对象选取

var maxList =

from book in books

where book.PageCount == books.Max(b => b.PageCount)

select book;

var maxBook = maxList.First();

这种方法将会遍历列表,查找页数与最大值相等的book对象。并选择第一个book对象。不幸的是,这种方法将在每次比较都会重新计算最大值。致使时间复杂度为O(n2)

第四中选择是使用两个单独的查询,如列表 5.34.

列表 5.34   使用两个单独的查询完成工作

var maxPageCount = books.Max(book => book.PageCount);

var maxList =

from book in books

where book.PageCount == maxPageCount

select book;

var maxBook = maxList.First();

 

这种方案的时间复杂度为O(n), 但是我们需要对序列进行两次迭代

The last solution wed recommend for its higher integration with LINQ is to create a custom query operator. 列表 5.35 shows how to code such an operator, which well call MaxElement.

我们推荐的最后一种解决方案是创建一个自定义操作符来与LINQ进行高度集成。列表5.35显示了MaxElement操作符的代码。

 

列表 5.35    自定义操作符MaxElement

public static TElement MaxElement<TElement, TData>(

this IEnumerable<TElement> source,

Func<TElement, TData> selector) where TData : IComparable<TData>

{

if (source == null)

throw new ArgumentNullException("source");

if (selector == null)

throw new ArgumentNullException("selector");

 

Boolean firstElement = true;

TElement result = default(TElement);

TData maxValue = default(TData);

 

foreach (TElement element in source)

{

var candidate = selector(element);

if (firstElement || (candidate.CompareTo(maxValue) > 0))

{

firstElement = false;

maxValue = candidate;

result = element;

}

}

return result;

}

这个查询操作符很容易使用:

var maxBook = books.MaxElement(book => book.PageCount);

5.1显示了几种不同选择的测试结果(20次测试)。通过结果可以看出,它们之间的执行性能有很大的不同。正确的使用LINQ查询很重要。使用自定义操作符没有不用LINQ的方法快。所以由你来决定是不是使用自定义操作符,但是我们要说的是在LINQ上下文中使用自定义查询操作符是一个不错的解决方案,即使它有些性能问题。

5.1    MaxElement各种选择所用的时间

选择

平均时间 ( ms)

最大时间 (in ms)

最小时间(in ms)

foreach

37

35

42

OrderBy + First

1724

1704

1933

子查询

37482

37201

45233

两个查询

66

65

69

自定义操作符

56

54

73

 

 

 

Lessons learned

你需要考虑Object LINQ查询的复杂度。应该避免写那些迭代超过一次的查询;否则你的查询可能不会有很好的执行效率。同样,你需要负责管理执行上下文。例如,在SQL LINQ上下文中,同一个查询可能完全不同,因为SQL LINQ按照它们自己的方式理解这个查询。

结论是你需要明智的使用Object LINQObject LINQ并不是所有情况的最终解决方案。在某些情况下,传统的方法可能更好一些。例如forforeach循环。在其它情况下你可以使用LINQ,但是最好创建自己的查询操作符来提高性能。在下一节,我们将会比较LINQ解决方案和传统的解决方案。

5.3.4   Object LINQ的整体概念

Object LINQ允许你写一些更容易读和写的代码。但是有时基于性能考虑,你不得不在两者之间进行权衡。

LINQ查询能够提供的最简单的操作就是过滤,如列表5.36中所示:

列表 5.36    使用LINQ过滤集合

var results =

from book in books

where book.PageCount > 500

select book;

 

列表 5.37 显示了使用foreach语句的等价代码

列表 5.37    使用foreach 循环过滤集合

var results = new List<Book>()

foreach (var book in books)

{

if (book.PageCount > 500)

results.Add(book);

}

列表 5.38显示了使用for语句做到同样的事情

列表 5.38    使用for循环过滤集合

var results = new List<Book>()

for (int i = 0; i < books.Count; i ++)

{

Book book = books[i];

if (book.PageCount > 500)

results.Add(book);

}

同样,也可以使用List<T>.FindAll做到这点。如列表 5.39.

列表 5.39    使用List<T>.FindAll 方法过滤集合

var results = books.FindAll(book => book.PageCount > 500);

 

为了让你了解每种做法的性能,我们使用百万多个随机产生的对象进行了测试。表5.2显示了50次的测试结果。

Table 5.2    执行50次的测试结果

Option

 

Average time

(in ms)

 

Minimum time

(in ms)

Maximum time

(in ms)

 

foreach

68

47

384

for

59

42

383

List<T>.FindAll

62

51

278

LINQ

91

74

404

 

惊讶?沮丧?Object LINQ看上去好像比其它方法慢了50%。但是对待测试结果要小心,所以还需要再分析一下。首先,这些结果只是局限于一个查询。如果我们改变了查询呢?例如改变where子句。这里我们使用Titlestring)替代了PageCountint)。

var results =

from book in books

where book.Title.StartsWith("1")

select book;

同样运行50次,我们得到了表5.3。对于新结果,我们注意到了什么呢?它们花费的时间相差无几。

 

Table 5.3    以字符串为条件的50次查询测试

Option

 

Average time

(in ms)

 

Minimum time

(in ms)

Maximum time

(in ms)

 

foreach

327

323

361

for

292

288

329

List<T>.FindAll

325

321

355

LINQ

339

377

377

 

为什么会出现这种结果呢?这是因为string操作比整数操作更耗时。但是有趣的是,这次LINQ只比其它方法慢了10%。这清楚的显示了LINQ并不是总是导致性能问题。

为什么会出现这种差别呢?当我们在where子句中改变了查询条件。其实是增加了每次测试执行的时间。这会影响到所有方法。但是对LINQ的影响却没有那么大。我们可以以另一种方法来看待这个现象,查询中所作的工作越少,LINQ对性能的影响就越大。这并不奇怪,LINQ不是免费的午餐。LINQ查询需要一些额外的工作,对象创建,垃圾回收,这些都基于查询的复杂程度。它对性能的影响可能低于5%,但也可能高于500%。

不过, 不要害怕使用LINQ,而是明智的使用它。对于简单且大量执行的操作,你可以考虑使用传统方法。对于简单的过滤和查询操作,你可以使用List<T>Array提供的方法,如FindAll, ForEach, Find, ConvertAll, 或者 TrueForAll。当然你也可以使用传统的foreach语句。对于那些不经常被执行的查询,你可以完全放心的使用Object LINQ。在一个不严格限制时间的环境中60毫秒和10毫秒并没有多大区别。别忘了,你的代码获得了更好的可读性和可维护性。

下面看另外一个例子

5.3.5   性能和简明:让你左右为难?

刚才我们看到了,LINQ在性能和代码的简明和清晰上做了交换。为了驳倒这种理论,这次我们执行一个分组操作,列表 5.40显示了一个根据publisher分组book对象的操作。

列表 5.40    使用LINQ进行分组

var results =

from book in SampleData.Books

group book by book.Publisher.Name into publisherBooks

orderby publisherBooks.Key

select publisherBooks;

 

列表 5.41 显示不实用LINQ进行分组的情况

列表 5.41    不使用LINQ进行分组

var results = new SortedDictionary<String, IList<Book>>();

foreach (var book in SampleData.Books)

{

IList<Book> publisherBooks;

 

if (!results.TryGetValue(book.Publisher.Name, out publisherBooks))

{

publisherBooks = new List<Book>();

results[book.Publisher.Name] = publisherBooks;

}

publisherBooks.Add(book);

}

 

毫无疑问,没有LINQ的代码更长更复杂。不过也很容易编写,但是当查询变得更加复杂的时候,使用LINQ绝对是明智的选择。两段代码的主要的不同之处就是用了两种完全不同的方法,LINQ遵循了声明式的方法。而传统代码使用了命令式的方法。没有使用LINQ的代码阐述了工作如何执行,而使用LINQ的代码描述要获取的结果。这是LINQ的基本原则。如果我们对以上两个示例进行性能测试。你会发现LINQ代码用了更少的时间。当然你会猜测,为什么传统的方法会更慢呢?我们把这个问题留给你自己解决。我们的观点是:如果你像获取性能优势而不使用LINQ代码,你就要写更多复杂的代码。

提示 SortedDictionary 是一种昂贵的数据结构。此外,我们在每一次循环中使用TryGetValue. LINQ 操作符以一种更加有效的方式处理这种问题。不使用LINQ的代码可以改进以获取性能优势,但是显示将会使用更加复杂的代码。

5.4 摘要

本章显示了如何处理通用场景,例如查询非泛型集合,根据多标准分组,创建动态查询和查询文本文件。同样介绍了两种LINQ设计模式:功能构建模式和ForEach模式。

第二个主要的主题就是性能问题。我们研究了LINQ查询可能出现的性能问题。这能让你避免编写糟糕的LINQ代码。同时和传统的查询方式做了比较,这能让我们看到LINQ的优点和缺点。

结论是:生活不是一个精彩的故事。并不是所有的事情除了黑就是白。但是LINQ提供了一种更有效的方法,它让你的代码有更好的可读性和可维护性。在将来,LINQ查询也许会帮你解决这些性能问题。微软正在开发PLINQ。它允许你在类似的Object LINQ查询支持并发。PLINQ现在还没有发布,但是应该在2008年的某个时候发布。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值