查询表达式(LINQ)简介

  • LINQ是Language Integrated Query的简称,它是集成在.NET编程语言中的一种特性。已成为编程语言的一个组成部分,在编写程序时可以得到很好的编译时语法检查,丰富的元数据,智能感知、静态类型等强类型语言的好处。并且它同时还使得查询可以方便地对内存中的信息进行查询而不仅仅只是外部数据源。

    LINQ定义了一组标准查询操作符用于在所有基于.NET平台的编程语言中更加直接地声明跨越、过滤和投射操作的统一方式,标准查询操作符允许查询作用于所有基于IEnumerable<T>接口的源,并且它还允许适合于目标域或技术的第三方特定域操作符来扩大标准查询操作符集,更重要的是,第三方操作符可以用它们自己的提供附加服务的实现来自由地替换标准查询操作符,根据LINQ模式的习俗,这些查询喜欢采用与标准查询操作符相同的语言集成和工具支持。

    LINQ包括五个部分:LINQ to Objects、LINQ to DataSets、LINQ to SQL、LINQ to Entities、LINQ to XML。

    首先不能不说的C#特性:

    1) 对象集合初始化器

    C# 3.0为你提供了对象集合初始化器:

    clip_image001

    附加:刚开始想想这对象集合初始化器也许就一鸡肋,没啥用,不就减少一点点代码么,像这种简单的初始化工作,大部分代码生成器都可以来干。后来在研究匿名类型的时候突然发现,如果没有这个对象初始化器,匿名类型是不是要复杂一些?或者就是难以实现?var test = new{Key="test",Value="test"};如果没有对象初始化器,匿名类型该怎么办?

    2) 匿名类型与隐式类型局部变量

    在C#里有这样一些类型,它是作为临时储存数据的,生命周期只在这个方法内,方法结束了,这个类型的生命周期也没有了。那么这里我们就可以使用一个匿名类型。

    var KeyPair = new {Key=”yuyi”,Value=”20”};

    这个KeyPair就是一个匿名类型,注意KeyPair这里是一个变量名,并不是类的名字。嗯,前面还有一个var,这又是什么呢?这是C# 3.0里面的隐式局部变量。

    3) 扩展方法

    1 方法所在的类必须是静态的

    2 方法也必须是静态的

    3 方法的第一个参数必须是你要扩展的那个类型,比如你要给int扩展一个方法,那么第一个参数就必须是int。

    4 在第一个参数前面还需要有一个this关键字。

    按照上面的步骤实例:

    clip_image001[12]

    4) 匿名方法和Lambda表达式

    C# 2.0为我们提供了匿名方法:

    this.btnRefresh.Click += delegate(object sender, EventArgs e) { BindData(); };

    Lambda表达式:

    clip_image003

    Lambda操作符读作”Goes to”,它后面紧跟着表达式或者是语句块(这点和匿名方法也不同,匿名方法只能使用语句块而不能使用表达式),下面我就用实例来说明一下有那些类型的Lambda表达式:

    //x的类型省略了,编译器可以根据上下文推断出来,后面跟着的是表达式

    clip_image005

    5) 迭代器

    C# 2.0里已经内置了对迭代器的支持,看看System.Collections, System.Collections.Generic命名空间,所有的集合都实现了这个接口:IEnumerable,这个接口还有泛型的版本。注意到这个接口只有一个方法:IEnumerator GetEnumerator();,IEnumerator就是迭代器的接口,它也有泛型的版本。

    一、 LINQ to Objects

    在前面介绍了,Linq里那些查询操作都是给IEnumerable接口添加的扩展方法(这些方法在Linq里被称为查询操作符),那么就可以以方法调用的方式使用Linq了:

    clip_image006

    注意到没有,上面所有的方法都是操作IEnumerable的,然后也是返回IEnumerable类型的对象。可以把这些方法按照用途分个类

    用途

    方法

    映射的(就是将查询的结果映射成需要的结果)

    Select(2个重载),SelectMany(4个重载)

    条件过滤

    Where(2个重载),OfType(这个方法是对IEnumerable扩展的,使用的时候要带上泛型参数,books.OfType(),意思就是从books集合里遍历元素,如果这个元素是Book类型或其子类型就将其添加到返回集合中)

    排序(注意排序的方法放回的是继承自IEnumerable的IOrderedEnumerable)

    OrderBy(2个重载), OrderByDescending(2个重载), Reverse(不干别的,就是把IEnumerable的顺序倒一下), ThenBy, ThenByDescending(这两个是对IOrderedEnumerable的扩展方法,所以它只能用在OrderBy后面,它的作用就是在一个已排序的系列上再按照某个key排序一次)

    分组

    GroupBy(8个重载), ToLookup(4个重载,它们的作用是根据一个key将IEnumerable转化为一个ILookUp对象,这个对象将按照key分组元素)

    联结

    GroupJoin,Join

    转型

    Cast(Linq只能操作泛型的集合!谁告诉你Linq只能操作泛型的集合?这个方法就是干这事情的,它是对IEnumerable扩展的一个方法,将一个IEnumerable转型为IEnumerable,然后你就可以享受Linq了,比如我用ArrayList保存一个User集合,ArrayList users = new ArrayList();但是Linq的那些什么Where啊,并没有对ArrayList所实现的接口IEnumerable进行扩展,怎么办?用Cast:IEnumerable myUsers = users.Cast();就这么简单)

    查询表达式的语法:

    clip_image008

    非泛型集合的查询:

    Linq里的那些Where啊,Select啊,这些方法都是针对IEnumerable扩展的,而ArrayList实现的是IEmerable这个接口。别急,Linq已经为我们考虑到这点了:

    clip_image010

    Cast()方法是对IEnumerable扩展的一个方法,它专门就是干这种转型的事情的,它将遍历非泛型集合中的每个元素,然后把它转型为TResult类型,然后返回一个IEnumerable对象,后面我们就可以使用Linq的其他扩展方法了。但是注意:如果你的非泛型集合里有一个无法转型到TResult类型的元素,那么就要抛出异常了,如果有一个null元素是不会抛出异常的,最终的元素也会是一个null。要不你来保险点,用OfType方法:

    clip_image012

    这个方法只会将非泛型集合中那些“是”TResult类型的元素返回来,其它的忽略(这个就不会抛出异常了)。

    二、 LINQ to DataSets

    1)LINQ TO DataSet Overview

    相对而言,LINQ TO DataSet是LINQ技术中最小的一块,虽然是DB中抽取出来的一个离线的操作模型,但毕竟对象也是个内存里面的object而已。所以和LINQ TO Object相比,大多数的操作都是一样的,不同只是要根据DataSet,DataTable的结构标明字段而已。下面简单的列出LINQ TO DataSet相比LINQ TO Object一些要注意的特色。

    2)Query UnTyped DataSet

    和一般的LINQ相比,query对象是untyped DataSet的时候,使用Field和SetField来读写不同的column字段,下面是一个简单的例子:

    clip_image014

    在这里大致要注意三点

    1.因为untyped DataSet没有实现IEnumerable 和 IQueryable的interface,所以如果想把它作为一个可以查询的对象的话,要先用AsEnumerable() 或者AsQueryable()转换一下,将它转换成IEnumerable或者IQueryable对象才能用LINQ去查询。如:from o in orders.AsEnumerable()

    2.一般是使用使用Field(“Column A”)和SetField(“Column A”)来读写不同的column字段对应的element,用它来访问相对于以前我们用ds.Tables["Orders"].Row[“RowA”][ “Column A”]的访问模式比起来,一个很大的好处就是可以避免null类型产生的exception。我们以前从DataSet里面取数据的时候,如果取的出来的是null,就会抛出exception,所以我们经常作类似if(ds.Tables["Orders"].Row[“RowA”][ “Column A”]!=null)的判断来包装我们进一步的逻辑处理,但是用Field(“Column A”)就可以避免这种麻烦。因为Field(“Column A”)是nullable的。这个特性的由来是这个泛型的使用,比如你取int类型数据的时候,如果你觉得它可能是null,那你就可以用Field(“Column A”)去取,这样就可以避免了exception的抛出。

    3 .Field和SetField是使用并不局限在LINQ 的query当中,在程序的其他地方也能使用,可以用它去替代以前的我们访问DataSet的方式,例如:

    clip_image016

    3) Query Typed DataSet

    这就更加简单了。对于定义了类型的DataSet,我们可以象查询内存中一般的object那样去查询它。例如:

    clip_image018

    还有一个与untyped DataSet不同的地方是在查询它的时候不需要使用AsEnumerable() 或者AsQueryable()那样的转换方法了。因为所有定义好的DataSet都是继承了TypedTableBase这个基类,而这个基类已经实现了IEnumerable的interface

    4) Query DataSet中的relation

    DataSet当中有时候也是有relation的,和DB一样,例如在下面的DataSet中加入relation:

    clip_image020

    如果我们想像在LINQ TO SQL里面一样通过relation来访问与其有相关关系的table,可以使用GetChildRows方法来取得与当前table相关联的那个table里面的DataRows,并将其返回为可以查询的IQueryable对象。例如:

    clip_image022

    这样我们就能通过relation来访问对象table了。

    三、 LINQ to SQL

    1) LINQ to SQL实例

    第一步:建立dbml(Database Mark Language。数据库描述语言,是一种xml格式的文档,用来描述数据库)文件,以Northwind数据库为例,上述Customers类被映射成一个表,对应数据库中的 Customers表

    clip_image024

    第二步:创建一个ASP.NET页面,在页面上加入一个GridView控件

    clip_image025

    第三步:编写代码进行数据绑定

    clip_image026

    第四步:运行显示结果。

    clip_image027

    2) 入门级实例

    a) 建立映射:

    clip_image028

    b) 执行查询:

    clip_image030

    在执行上面的插入的时候肯定会碰到异常,这是因为,Id对应的数据表字段postid是该表的主键,你不应该在插入的时候赋值,有人说我实例化对象的时候确实没有给post的Id属性赋值啊,但是.net会在后台为我们将Id赋值为0,所以你要对映射对象做一下修改:clip_image032

    不仅仅数据表的主键要加这个属性,如果对于那些在数据库里设置了默认值的,你并不像用程序插入的时候,你也得使用,比如这里的CreateDate。

    c) 插入对象:

    clip_image034

    d) 更新数据:

    做更新的时候,你首先得从数据库查询出该对象,然后对该对象的属性进行修改,最后更新到数据库:

    clip_image036

    e) 删除数据:

    有了插入,更新,查询就差一个Delete CURD就全了,对于delete更简单了,你只要调用Table的DeleteOnSubmit方法就行了,这里就不再详述。

    3) 进阶级实例

    Linq to SQL和SQL语句一样,支持两种方式的联合:

    1.利用where子句,对两个表进行查找

    2.使用join子句

    首先获取Post和Blog的Table的对象,然后施加操作:

    clip_image038

    下面是Linq to SQL为我们生成的SQL语句,从上面可以看出来是用where做联结的条件来进行的,这种联结的方式是不被推荐的,存在于ANSI-82 SQL的标准中, 推荐的方式是使用join子句:

    clip_image040

    一对一的关系:

    clip_image041

    4) Linq to SQL支持的配置方式实例

    纯Attribute方式配置:

    1. AutoSync: 自动同步。这个属性是一个枚举类型

    AutoSync. Default,自动选择,默认就是这个,一般是如果该列在数据库里有默认值,Column的IsDbGenerated属性标记为true的时候则同步。

    AutoSync. Always,总是进行同步

    AutoSync.Never,从来不同步

    AutoSync.OnInsert,在执行插入操作后同步,像我们博客园的那个例子,Blog的Id是主键,自增的,插入数据库的时候我们并不提供,而是数据库自动生成的,那这个时候我们的类插入以后这个Id如何同步呢?

    AutoSync. OnUpdate,在更新的时候同步

    实例:实际上我们可以看看,这个AutoSync是如何影响Linq to SQL的行为的,我们执行一下对Post的插入:

    clip_image043

    看看生成的SQL代码:

    clip_image044

    在insert的代码下面,我们还会看到一个select的代码,将postid和createdate给查询回来,但我们并没有执行查询操作啊,原来这就是AutoSync在使坏,在默认的时候IsDbGenerated为true的列是被会查出来返回的。为了检查一下说的是不是对的,我将Post的Title属性修改一下:

    [Column(AutoSync=AutoSync.Always)]public string Title { get; set; }

    然后再执行上面 插入代码,看看生成的SQL又将如何呢:

    clip_image045

    呵呵,生成的select语句也将Title给查回来了。

    现在应该明白这个AutoSync的意义了吧,你可以自己一一试验。

    2. CanBeNull

    这个属性可以指定对应的数据库表的列是否允许是null的,如果这个列不能为null,那你给这个属性赋个null的时候,是要触发异常的。不过要记住,null并不代表是一个空字符串或者零,关于null的更多内容,你可以参见园子Anytao的佳作。

    3. DbType

    如果你想使用DataContext的CreateDataBase方法从映射类创建数据库表,那么最好指定这个,这样你就可以明确的指定出你的这个列在数据库表中的DbType是啥,不然Linq to SQL会从属性的类型推断DbType,这有可能不怎么适合,比如这个string类型:

    [Column(DbType="NVarchar(50) not null")] public string Title { get; set; }

    在这里我们是不是发现了,我们还可以从映射类创建数据库,都说ORM是以O为主,可我们实践的时候总是先建立数据库,然后根据数据库创建Object,那这不是ROM了么,从这里的信息表示,你可以先设计好Domain Object,并建立好Object之间的关系,最后使用CreateDataBase方法来创建数据库表。关于CreateDataBase更多的信息在后面我会更进一步的说明的。

    4. Expression

    我只能说Linq to SQL做的太周到了,连这个都考虑了。Expression用来表示一个计算列,什么意思?意思是这个属性是数据库表的列通过计算获取的,比如,假如我们数据库里有一列price存储的是美元,可是取出来的时候我们要求用人民币表示:

    [Column(Expression="price * 6")] public float Price { get; set; }

    上面的例子只是用来说明Expression这个属性的用途,而这个例子的做法实在是不可取,对于美元转人民币这种汇率问题,汇率是时刻波动的,所以这样硬编码,实在是糟糕的很,各位读了以后BS一下,一笑而过吧。

    5. IsDbGenerated

    从名字上就可以看出来了,这个表明这个属性的值是数据库产生的,不需要我们的程序赋值,比如这个自增的列,比如这个有默认值的列(时间,我们可以用SQL getdate()函数设置值,而无需我们在程序里指定)

    6. IsPrimaryKey

    是主键吗?如果是就要指定这个属性了,如果你的主键是多个列组成的,那么就在多个属性上加吧。关于主键,在后面还会进一步介绍。

    7. IsVersion和UpdateCheck

    这两个在这里就不说了,后面会有大篇幅的介绍的。这个Column特性是从抽象特性Data继承来的,从这个特性继承了两个属性:

    8. Name

    这个是当你的属性名字和这个列名不一致的时候指定列名用的。

    9. Storage

    有的时候啊,你的属性里的set里面应用了复杂的业务逻辑,而Linq to SQL在将值从数据库取出,然后赋值给这个属性的时候,默认是要使用这个set的,这个时候在这种情况下(从数据库取值赋给类的属性),你并不想执行这个set里面的逻辑,想把这个值直接给属性背后的那个私有字段:

    clip_image047

    10. Association

    这个特性是用来建立实体之间关系的。在前面的例子里我们看到了:

    clip_image049

    Association有两个最重要的属性就是ThisKey和OtherKey

    ThisKey用来标识和别的对象关联的键,如果没有指明就用本类属性上标识有IsPrimaryKey的了。OtherKey用来定义关联的类的键,如果没有指定就用关联的那个类的标识列了。

    Association特性也是从Data特性继承来的,也有Name和Storage属性。Storage属性和Column是一样的,这里的Name属性是CreateDataBase利用映射类动态创建数据库的时候建立关系用的,这个Name就是关系名。

    XML文件的配置方式:

    有心的你也许发现了,DataContext类有好几个重载的构造函数,我们常用的是:

    DataContext(string fileOrConnectionString);

    还有一个:

    DataContext(string fileOrConnectionString,MappingSource mapping);

    这个MappingSource是干吗的呢?在System.Data.Linq.Mapping命名空间下,你会发现这样的个关系:

    clip_image050

    看到它的两个子类的名称你也许就会猜出十之八九了吧,我们前面所使用的就是AttributeMappingSouce映射,除此之外还可以使用XML作为映射的配置文件哦,从上面的图看,XmlMappingSource还有几个静态的方法,他们可以以各种形式的Xml数据源来构建XmlMappingSource实例:

    XmlMappingSource mapping = XmlMappingSource.FromXml(File.ReadAllText(@“e:"cnblogs"map.xml”)); DataContext dbContext = new DataContext(connectionString,mapping);

    用这种方式构建DataContext对象我们就可以使用Xml作映射了

    使用Visual Studio的设计器生成映射配置代码

    毕竟整天伴随着我们的是VS,所以还是来个可视化的工具比较过瘾。

    你只要在你的项目当中新建一个LINQ to SQL Classes类型的新项,然后从服务器浏览器里把表拖到设计界面上,然后用工具栏里面的工具,连接好各个表之间的关系就OK了。

    VS会为我们生成三个文件:一个以dbml为后缀的文件(其实这个文件就是个xml格式的文件,它和刚才只用xml作映射配置的文件格式是类似的)一个C#代码文件,注意到这个代码文件是个局部类,该类和dbml文件共同编译成一个类,这个和asp.net里面的aspx和它的后台代码使用的方式类似。还有一个是以layout为后缀名的,这个文件是设计器使用的,用来存储设计器中各表在界面上的位置,和我们的程序无关。ssssss

    关于可视化的设计器的使用这里也不做介绍了,自己动动手,然后看看生成的几个文件里到底是什么东西,加上前面对手动操作的详细讲解,我想理解这个将是很容易的事情。

    四、 LINQ to XML

    1. LINQ to XML 类概述

    a) XAttribute 类

    XAttribute 表示一个 XML 属性。

    b) XCData 类

    XCData 表示一个 CDATA 文本节点。

    c) XComment 类

    XComment 表示一个 XML 注释。

    d) XContainer 类

    XContainer 是适用于可能具有子节点的所有节点的抽象基类。 下面的类派生自 XContainer 类: Xelement,XDocument

    e) XDeclaration 类

    XDeclaration 表示一个 XML 声明。 XML 声明用于声明 XML 版本和文档的编码。 此外,XML 声明还指定 XML 文档是否为独立文档。

    f) XDocument 类

    XDocument 表示一个 XML 文档。

    g) XDocumentType 类

    XDocumentType 表示一个 XML 文档类型定义 (DTD)。

    h) XElement 类

    XElement 表示一个 XML 元素。 有关详细信息和示例,

    i) XName 类

    XName 表示元素 (XElement) 和属性 (XAttribute) 的名称。 有关详细信息和示例, LINQ to XML 旨在使 XML 名称尽可能简单。 XML 名称由于复杂而通常被视为 XML 中的高级主题。 有证据证明,这种复杂性不是由开发人员编程时通常使用的命名空间造成的,而是由命名空间前缀造成的。 使用命名空间前缀可以减少输入 XML 时需要的击键数或使 XML 更具可读性。 但前缀通常只是使用完整 XML 命名空间的快捷方式,在多数情况下并不需要。LINQ to XML 通过将所有前缀解析为其对应的 XML 命名空间来简化 XML 名称。 如果需要,可以通过 GetPrefixOfNamespace 方法可以使用前缀。 如果有必要,可以控制命名空间前缀。 在某些情况下,如果使用的是其他 XML 系统(如 XSLT 或 XAML),则需要控制命名空间前缀。 例如,如果 XPath 表达式使用 XSLT 样式表中嵌入的命名空间前缀,则将需要确保使用与 XPath 表达式中使用的前缀相匹配的命名空间前缀来序列化 XML 文档。

    j) XNamespace 类

    XNamespace 表示 XElement 或 XAttribute 的命名空间。 命名空间是 XName 的一个组件。

    k) XNode 类

    XNode 是一个抽象类,它表示 XML 树的节点。 下面的类派生自 XNode 类:

    Xtext,Xcontainer,Xcomment,XprocessingInstruction,XDocumentType

    XNodeDocumentOrderComparer 类

    XNodeDocumentOrderComparer 提供用于比较节点的文档顺序的功能。

    XNodeEqualityComparer 类

    XNodeEqualityComparer 提供用于比较节点的值是否相等的功能。

    l) XObject 类

    XObject 是 XNode 和 XAttribute 的抽象基类。 它提供批注和事件功能。

    m) XObjectChange 类

    XObjectChange 指定对 XObject 引发事件时的事件类型。

    n) XObjectChangeEventArgs 类

    XObjectChangeEventArgs 为 Changing 和 Changed 事件提供数据。

    o) XProcessingInstruction 类

    XProcessingInstruction 表示一个 XML 处理指令。 处理指令将信息传递给处理 XML 的应用程序。

    p) XText 类

    XText 表示一个文本节点。 多数情况下都不必使用此类。 此类主要用于混合内容。

    q) XDocument 类

    XDocument 类包含有效的 XML 文档所需的信息。 其中包括 XML 声明、处理指令和注释。 请注意,如果需要 XDocument 类提供的特定功能,您只需创建 XDocument 对象。 在很多情况下,可以直接使用 XElement。 直接使用 XElement 是一种比较简单的编程模型。 XDocument 是从 XContainer 派生的。 因此,它可以包含子节点。 但是,XDocument 对象只能有一个子 XElement 节点。 这反映了 XML 标准,即在 XML 文档中只能有一个根元素。

    r) XElement 类

    XElement 类是 LINQ to XML 中的基础类之一。 它表示一个 XML 元素。 可以使用该类创建元素;更改元素内容;添加、更改或删除子元素;向元素中添加属性;或以文本格式序列化元素内容。 还可以与 System.Xml 中的其他类(例如 XmlReader、XmlWriter 和 XslCompiledTransform)进行互操作.

    2. 查询、更新、删除

    查找具有特定属性的元素

    clip_image051

    内存中 XML 树修改与函数构造

    就地修改 XML 树是更改 XML 文档形状的传统方法。 典型的应用程序将文档加载到数据存储区(如 DOM 或 LINQ to XML);使用编程接口插入节点、删除节点或更改节点的内容;然后将 XML 保存到文件或通过网络传输。

    LINQ to XML 允许使用另一种可在许多方案中使用的方法:函数构造。 函数构造将修改数据视为转换问题,而不是数据存储区的具体操作。 如果您采用某种数据表示形式并有效地将其从一种形式转换为另一种形式,其结果等效于您采用一个数据存储区并对其以某种方式进行操作以采用另一种形状。 函数构造方法的关键是将查询的结果传递给 XDocument 和 XElement 构造函数。

    此示例假设您想修改下面的简单 XML 文档,使属性变为元素。 本节首先介绍传统的就地修改方法。 然后显示函数构造方法。XML文件:

    clip_image052

    您可以编写一些过程代码以便从属性创建元素,然后删除属性,如下所示:

    clip_image053

    函数构造方法

    相比之下,函数方法包含用于形成新树的代码、从源树中选择元素和属性并在将其添加到新树中时进行相应的转换。 函数方法如下所示:

    clip_image054

    在本例中,函数示例一点也不比第一个示例简短,而且一点也不比第一个示例简单。 但如果要对一个 XML 树进行很多更改,则非函数方法将变得非常复杂,而且会显得很笨拙。 相比之下,使用函数方法时,您只需形成所需的 XML,嵌入适当的查询和表达式以提取需要的内容。 函数方法生成的代码更易于维护。 请注意,在本例中,函数方法的执行效果可能没有树操作方法好。 主要问题是函数方法创建了更多短生存期的对象。 但是,如果使用函数方法能够提高程序员的效率,则折中也是一种有效的方式。 这是一个很简单的示例,但它显示了这两种方法之间基本原理上的差异。 对于转换较大的 XML 文档,函数方法可以产生更高的效率增益。

    向 XML 树中添加元素、属性和节点

    下面的方法将子内容添加到 XElement 或 XDocument 中:

    clip_image055

    下面的方法将内容添加为 XNode 的同级节点。 向其中添加同级内容的最常见的节点是 XElement,不过你也可以将有效的同级内容添加到其他类型的节点,例如 XText 或 XComment。

    clip_image056

    修改 XML 树中的元素、属性和节点

    下表汇总了修改元素、元素的子元素或元素属性 (Attribute) 时可以使用的方法和属性 (Property)。下面的方法修改 XElement。

    从 XML 树中移除元素、属性和节点

    可以修改 XML 树,移除元素、属性和其他类型的节点。 从 XML 文档中移除单个元素或单个属性的操作非常简单。 但是,若要移除多个元素或属性的集合,则应首先将一个集合具体化为一个列表,然后从该列表中删除相应元素或属性。 最好的方法是使用 Remove 扩展方法,该方法可以实现此操作。 这么做的主要原因在于,从 XML 树检索的大多数集合都是用延迟执行生成的。 如果不首先将集合具体化为列表,或者不使用扩展方法,则可能会遇到某类 Bug。

    clip_image058

    示例:此示例演示三种移除元素的方法。 第一种,移除单个元素。 第二种,检索元素的集合,使用 Enumerable.ToList<(Of <(TSource>)>) 运算符将它们具体化,然后移除集合。 最后一种,检索元素的集合,使用 Remove 扩展方法移除元素。

    clip_image059