技术开发 频道

LINQ项目-支持 LINQ 项目的语言功能


【IT168技术文档】

  LINQ 完全构建于通用的语言功能之上,其中某些是在 C# 3.0 和 Visual Basic 9.0 中新增的功能。每个功能都有自己的实用工具,但这些功能共同提供了一种定义查询和可查询 API 的可扩展方法。在本部分中,我们将探究这些语言功能,以及它们如何提供更为直接和声明性的查询模式。

  λ 表达式和表达式树

  许多查询操作符都允许用户提供执行筛选、投影或键值提取的函数。基于 λ 表达式的概念而生成的查询工具为开发人员提供了一种编写函数的简便方法,这些函数可以作为后续计算的参数进行传递。λ 表达式类似于 CLR 委托,它必须符合委托类型定义的方法签名。为了进行说明,我们可以使用 Func 委托类型将上述语句扩展为更为显式的等效形式:
Func filter = s => s.Length == 5; Func extract = s => s; Func project = s => s.ToUpper(); IEnumerable expr = names.Where(filter) .OrderBy(extract) .Select(project);
  λ 表达式是 C# 2.0 匿名方法的自然演化结果。例如,我们可以使用匿名方法编写上述示例,如下所示:
Func filter = delegate (string s) { return s.Length == 5; }; Func extract = delegate (string s) { return s; }; Func project = delegate (string s) { return s.ToUpper(); }; IEnumerable expr = names.Where(filter) .OrderBy(extract) .Select(project);
  总之,开发人员可以自由地将命名方法、匿名方法或 λ 表达式与查询操作符一起使用。λ 表达式的优点是,能够提供最直接而简洁的创作语法。更重要的是,λ 表达式可以编译为代码,也可以编译为数据,从而允许优化器、转换器和计算器在运行时处理 λ 表达式。

  LINQ 定义了一个特殊类型 Expression(在 System.Expressions 命名空间中),该类型用于指示给定 λ 表达式需要表达式树,而不是基于 IL 的传统方法体。表达式树是 λ 表达式的有效内存中数据表示形式,它使表达式的结构透明且显式。

  编译器是发出可执行 IL 还是表达式树取决于 λ 表达式的用法。如果将 λ 表达式指定给委托类型的变量、字段或参数,则编译器将发出与匿名方法等效的 IL。如果将 λ 表达式指定给 Expression 类型的变量、字段或参数,则编译器将发出表达式树。

  例如,请考虑以下两个变量声明:
Func f = n => n < 5; Expression<FUNC> e = n => n < 5;
  变量 f 是对委托的引用,可以直接执行:
bool isSmall = f(2); // isSmall is now true
  变量 e 是对表达式树的引用,不可直接执行:
bool isSmall = e(2); // compile error, expressions == data
  与委托(有效的不透明代码)不同,我们可以像与程序中的任何其他数据结构交互那样与表达式树进行交互。例如,以下程序:
Expression<FUNC> filter = n => n < 5; BinaryExpression body = (BinaryExpression)filter.Body; ParameterExpression left = (ParameterExpression)body.Left; ConstantExpression right = (ConstantExpression)body.Right; Console.WriteLine("{0} {1} {2}", left.Name, body.NodeType, right.Value);
  在运行时分解表达式树,并显示以下字符串:

  n LT 5

  对于启用第三方库(利用属于平台一部分的基本查询抽象)的环境,这种在运行时将表达式视为数据的功能很重要。DLinq 数据访问实现利用该功能将表达式树转换为适用于在存储中计算的 T-SQL 语句。
  扩展方法

  λ 表达式是查询体系结构的一个重要部分。扩展方法 是另一个重要部分。扩展方法将动态语言中常见的“快速输入”的灵活性与静态输入语言的性能和编译时验证结合在一起。通过扩展方法,第三方可以使用新方法增加一个类型的公共协定,同时仍然允许单个类型创作者为这些方法提供他们自己的特定实现。

  扩展方法在静态类中定义为静态方法,但在 CLR 元数据中以 [System.Runtime.CompilerServices.Extension] 属性标记。我们鼓励语言为扩展方法提供直接语法。在 C# 中,扩展方法由 this 修饰符指示,该修饰符必须应用于扩展方法的第一个参数。我们来看一下最简单的查询操作符 Where 的定义:
namespace System.Query { using System; using System.Collections.Generic; public static class Sequence { public static IEnumerable Where( this IEnumerable source, Func predicate) { foreach (T item in source) if (predicate(item)) yield return item; } } }
  扩展方法第一个参数的类型指示该扩展应用于哪种类型。在上述示例中,Where 扩展方法将扩展 IEnumerable 类型。由于 Where 是静态方法,因此我们可以像调用任何其他静态方法那样直接调用它:
IEnumerable expr = Sequence.Where(names, s => s.Length < 6);
  但是,扩展方法的特殊之处在于,它们还可以通过实例语法来调用:
IEnumerable expr = names.Where(s => s.Length < 6);
  扩展方法在编译时根据哪些扩展方法在范围内进行解析。当一个命名空间与 C# 的 using 语句或 VB 的 Import 语句一起导入时,由该命名空间的静态类定义的所有扩展方法将导入范围中。

  标准查询操作符将定义为 System.Query.Sequence 类型的扩展方法。在检查标准查询操作符时,您将注意到,除了一个以外,所有操作符都可以定义为 IEnumerable 接口(这个例外是 OfType,我们将在后文加以说明)。这意味着,每个与 IEnumerable 兼容的信息源都可以通过在 C# 中添加以下 using 语句来轻松地获得标准查询操作符:
using System.Query; // makes query operators visible
  希望将标准查询操作符替换为特定类型的用户可以:(a) 使用兼容的签名在特定类型上定义他们自己的同名方法,或者 (b) 定义可扩展特定类型的新的同名扩展方法。希望完全避免标准查询操作符的用户只能将 System.Query 置于范围以外,并为 IEnumerable 编写他们自己的扩展方法。

  对于解析而言,扩展方法具有最低的优先权,并且只有在没有合适的目标类型及其基类型的匹配时才使用。这允许用户定义的类型提供他们自己的、优于标准操作符的查询操作符。例如,请考虑以下自定义集合:
public class MySequence : IEnumerable { public IEnumerator GetEnumerator() { for (int i = 1; i <= 10; i++) yield return i; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public IEnumerable Where(Func filter) { for (int i = 1; i <= 10; i++) if (filter(i)) yield return i; } }
  假定使用该类定义,以下程序:
MySequence s = new MySequence(); foreach (int item in s.Where(n => n > 3)) Console.WriteLine(item);
  将使用 MySequence.Where 实现,而不是扩展方法,因为实例方法优于扩展方法。

  前面提到的 OfType 操作符是一个无法扩展基于 IEnumerable 的信息源的标准操作符。下面,我们来看一下 OfType 查询操作符:
public static IEnumerable OfType(this IEnumerable source) { foreach (object item in source) if (item is T) yield return (T)item; }
  OfType 不仅接受基于 IEnumerable 的源,还接受针对非参数化 IEnumerable 接口(在 .NET Framework 1.0 版本中提供)编写的源。OfType 操作符允许用户将标准查询操作符应用于以下传统的 .NET 集合:
// "classic" cannot be used directly with query operators IEnumerable classic = new OlderCollectionType(); // "modern" can be used directly with query operators IEnumerablemodern = classic.OfType();
  在本例中,变量 modern 生成了与 classic 相同的值序列,但其类型与现在的 IEnumerable 代码兼容,包括标准查询操作符。

  OfType 操作符对于较新的信息源也很有用,因为它允许根据类型从源筛选值。在生成新序列时,OfType 只省略原始序列中与类型参数不兼容的成员。请考虑下面这个简单的程序,它将从异类数组中提取字符串:
object[] vals = { 1, "Hello", true, "World", 9.1 }; IEnumerable justStrings = vals.OfType();

  当我们在 foreach 语句中枚举 justStrings 变量时,将获得一个由两个字符串(“Hello”和“World”)组成的序列。

  延迟的查询计算

  观察力敏锐的读者可能会注意到,标准的 Where 操作符是使用 C# 2.0 中引入的 yield 结构实现的。该实现技术常用于返回值序列的所有标准操作符。使用 yield 的一个有趣的优点是,查询实际上是在迭代完毕后计算的(通过使用 foreach 语句,或者手动使用基础的 GetEnumerator 和 MoveNext 方法)。该延迟计算允许将查询保留为基于 IEnumerable 的值,这些值可以计算多次,每次都可能生成不同的值。

  对于许多应用程序而言,这正是所需的行为。对于希望缓存查询计算结果的应用程序而言,提供的两个操作符(ToList 和 ToArray)会强制立即计算查询,并返回包含查询计算结果的 List 或数组。

  要了解延迟查询计算如何工作,请考虑以下程序,该程序对数组运行了一个简单的查询:
// declare a variable containing some strings string[] names = { "Allen", "Arthur", "Bennett" }; // declare a variable that represents a query IEnumerable ayes = names.Where(s => s[0] == 'A'); // evaluate the query foreach (string item in ayes) Console.WriteLine(item); // modify the original information source names[0] = "Bob"; // evaluate the query again, this time no "Allen" foreach (string item in ayes) Console.WriteLine(item);
  每次迭代变量 ayes 时,都会计算查询。要指示所需结果的缓存副本,我们只需在查询中追加一个 ToList 或 ToArray 操作符,如下所示:
// declare a variable containing some strings string[] names = { "Allen", "Arthur", "Bennett" }; // declare a variable that represents the result // of an immediate query evaluation string[] ayes = names.Where(s => s[0] == 'A').ToArray(); // iterate over the cached query results foreach (string item in ayes) Console.WriteLine(item); // modifying the original source has no effect on ayes names[0] = "Bob"; // iterate over result again, which still contains "Allen" foreach (string item in ayes) Console.WriteLine(item);
  ToArray 和 ToList 都可以强制立即执行查询计算,这与返回单个值的标准查询操作符(例如,First、ElementAt、Sum、Average、All 和 Any)一样。

  初始化复合值

  λ 表达式和扩展方法为我们提供了只从值序列筛选成员的查询所需的全部内容。大多数查询表达式还针对这些成员执行投影,将原始序列的成员有效地转换为值和类型可能不同于原先的成员。要支持编写这些转换,LINQ 依赖一个名为对象初始化表达式 的新结构,以创建结构化类型的新实例。在本文其余部分中,我们将假设定义了以下类型:
public class Person { string name; int age; bool canCode; public string Name { get { return name; } set { name = value; } } public int Age { get { return age; } set { age = value; } } public bool CanCode { get { return canCode; } set { canCode = value; } } }
  对象初始化表达式使我们能够根据类型的公共字段和属性轻松地生成值。例如,要创建 Person 类型的新值,我们可以编写以下语句:
Person value = new Person { Name = "Chris Smith", Age = 31, CanCode = false };

  从语义上说,该语句等效于以下语句序列:

Person value = new Person(); value.Name = "Chris Smith"; value.Age = 31; value.CanCode = false;

  对象初始化表达式是语言集成查询的一个重要功能,因为它们允许在仅允许表达式的上下文(如 λ 表达式和表达式树)中生成新的结构化值。例如,请考虑以下查询表达式,它为输入序列中的每个值创建了新的 Person 值:
IEnumerable expr = names.Select(s => new Person { Name = s, Age = 21, CanCode = s.Length == 5 });
  对象初始化语法也可以方便地用于初始化结构化值的数组。例如,请考虑以下数组变量,该变量是使用单个的对象初始值设定项来初始化的:
static Person[] people = { new Person { Name="Allen Frances", Age=11, CanCode=false }, new Person { Name="Burke Madison", Age=50, CanCode=true }, new Person { Name="Connor Morgan", Age=59, CanCode=false }, new Person { Name="David Charles", Age=33, CanCode=true }, new Person { Name="Everett Frank", Age=16, CanCode=true }, };
  结构化值和类型

  LINQ 项目支持以数据为中心的编程样式,其中,某些类型的存在主要是为了通过结构化值提供静态“形式”,而不是提供同时具有状态和行为的完整对象。根据它的逻辑结论推测,通常,开发人员所关心的只是值的结构,以及对命名类型的需要,因为该形式很少使用。这就引出了对匿名类型 的介绍,匿名类型允许将新的结构定义为与它们的初始化进行“内联”。

  在 C# 中,匿名类型的语法与对象初始化语法完全相同(除了省略了类型的名称)。例如,请考虑以下两个语句:
object v1 = new Person { Name = "Chris Smith", Age = 31, CanCode = false }; object v2 = new { // note the omission of type name Name = "Chris Smith", Age = 31, CanCode = false };
  变量 v1 和 v2 都指向一个内存中对象,该对象的 CLR 类型有三个公共属性 — Name、Age 和 CanCode。变量的不同之处在于,v2 引用了匿名类型 的实例。在 CLR 术语中,匿名类型与任何其他类型没有区别。匿名类型的特殊之处在于,它们在编程语言中没有有意义的名称 — 创建匿名类型实例的唯一方法就是使用上述语法。

  要使变量能够引用匿名类型的实例,同时仍然从静态类型获益,C# 引入了 var 关键字,以便用于替换局部变量声明的类型名称。例如,请考虑以下合法的 C# 3.0 程序:
var s = "Bob"; var n = 32; var b = true;
  var 关键字会告诉编译器,从用于初始化变量的表达式的静态类型推断出变量的类型。在本例中,s、n 和 b 的类型分别是 string、int 和 bool。该程序与以下程序完全相同:
string s = "Bob"; int n = 32; bool b = true;
  var 关键字方便用于其类型名称有意义的变量,但对于引用匿名类型实例的变量而言是必需的。
var value = new { Name = "Chris Smith", Age = 31, CanCode = false };
  在上述示例中,变量 value 是匿名类型,其定义与以下伪 C# 等效:
internal class ??? { string _Name; int _Age; bool _CanCode; public string Name { get { return _Name; } set { _Name = value; } } public int Age{ get { return _Age; } set { _Age = value; } } public bool CanCode { get { return _CanCode; } set { _CanCode = value; } } }

  匿名类型不能跨程序集边界共享;但是,编译器可确保在每个程序集中,属性名/类型对的给定序列最多有一个匿名类型。

  由于匿名类型通常用于投影,以选择现有结构化值的一个或多个成员,因此我们只需从匿名类型初始化的另一个值中引用字段或属性。这将导致一个新的匿名类型,其属性的名称、类型和值均从所引用的属性或字段复制而来。

  例如,请考虑以下示例,该示例通过组合其他值的属性创建了一个新的结构化值:
var bob = new Person { Name = "Bob", Age = 51, CanCode = true }; var jane = new { Age = 29, FirstName = "Jane" }; var couple = new { Husband = new { bob.Name, bob.Age }, Wife = new { Name = jane.FirstName, jane.Age } }; int ha = couple.Husband.Age; // ha == 51 string wn = couple.Wife.Name; // wn == "Jane"
  对上述字段或属性的引用只是一种方便的语法,可用于编写以下更显式的窗体:
var couple = new { Husband = new { Name = bob.Name, Age = bob.Age }, Wife = new { Name = jane.FirstName, Age = jane.Age } };
  在这两个示例中,couple 变量从 bob 和 jane 获得了自己的 Name 和 Age 属性副本。


  匿名类型通常用于查询的 select 子句。例如,请考虑以下查询:
var expr = people.Select(p => new { p.Name, BadCoder = p.Age == 11 }); foreach (var item in expr) Console.WriteLine("{0} is a {1} coder", item.Name, item.BadCoder ? "bad" : "good");


  在本例中,我们能够通过 Person 类型创建新投影,以完全匹配处理代码所需的形式,同时仍然提供静态类型的优势。
0
相关文章