现在的位置: 首页 > 综合 > 正文

查询综合:下一代编程语言必备功能

2014年09月13日 ⁄ 综合 ⁄ 共 8203字 ⁄ 字号 评论关闭

查询综合:下一代编程语言必备功能
周融,海鼎公司
2006年7月

现代应用程序需要进行大量的数据访问和操作,包括与数据库的直接交互和对自身数据的访问。本文描述了一种新型的数据访问技术,利用这种技术,开发人员可以方便的在各种数据之间进行访问和查询。正是由于这种技术的高可用性,因而这种技术将被下一代编程语言所采用和发展。

简单的例子

在应用程序中使用数据的例子多不胜举。这里假设开发人员需要做一个世界国家信息的查询应用程序,这些信息可以通过第三方提供的 Web Services 获取,在应用程序中使用一个 Country 类进行存储对应国家的信息。

 public class Country {
 public string Name { get; set; }
 public int Population { get; set; }
 public double Area { get; set; }
 public string Region { get; set; }
 }

开发人员往往需要利用这个类结构查询一些被筛选的信息,例如,需要得到国土面积大于 1,000,000 平方米的所有国家名称,可以使用一种新的语法来实现:

Country[] countries = new Country[] {
new Country(“China”, 9600000),
new Country(“U.S.”, 5700000)
};
var x = from c in countries where c.Area >= 1000000 select c.Name;

利用类似于 SQL 语法的查询,可以非常方便的获取这些数据。

另外,开发人员并不满足这种简单的查询方式,他们可能提出更多的查询需求,例如对现有查询结果进行排序,连接多个查询结果,在多种数据集之间查询等。下面的例子演示了如何利用类似于以上例子的语法进行复杂查询。

var q = from c in countries
where c.Population >= 500000 && c.Region == “Africa”
orderby c.Name descending
select c;

不仅如此,开发人员可以直接查询整个数据集,或者是数据库:

DataContent dc = new DataContent(NorthWind.ConnectionString);
var northWind = dc.ToQueryable();
var p = from p in dc.Products.Production,
join e in dc.Employees.Employee on e.EmployeeID == p.ManagerID, 
join pi in dc.Products.ProductImage on p.ProductID == pi.ProductID
where p.Price >= 50 && p.ProductName.Contains(“Office”)
group p by p.Name, pi by pi.ID
select new {Name = p.Name, Image = p.Image};

这些新的语法使得开发人员在数据处理方面有了更加坚定的信心,也使得他们将更多的精力投入到逻辑和架构设计上。这种利用宿主语言进行数据查询的语法特例称为查询综合(Query Comprehension)

查询综合不仅能完成基于类、数组、集合等具备 IEnumerable<T> 接口的类型对象上,而且它还能实现基于 XML 文档的查询,甚至基于数据库的查询。由此,查询综合技术分为三块:一是在类型上的数据查询综合(DLinq),二是基于 XML 的查询综合(XLinq),三是在数据集上的查询综合(Query over DataSet),这三项技术以一种宿主语言为基础,以一组新编程技术为框架而设计,统称为基于语言的查询框架(Linq,Language Integrated Query)。

为什么使用Linq

开发人员经常对已经成熟的技术感到可信任和依赖,而对于一些新的技术可能不会过度关心。但是,基于数据和数据仓库的应用程序越来越多,开发人员必须无时无刻的与数据打交道,因此,提高数据处理和访问效率是这些人员重点研究和着重考虑的问题。下面列举的几个要素完全可以让这部分人对 Linq 充满信心:

Linq 可以让数据访问更加轻松。使用 Linq 对数据进行查询是一种非常方便的方法。开发人员不用太多的关心实现这些功能(如排序和连接)的算法,只要能熟悉 Linq 的基本语法(甚至河 SQL 标准语言的语法一样)就能够使用功能强大的查询综合。

Linq 可以对基于多种形式的数据进行访问。不论是简单的数组、集合、泛型和自定义类,还是 XML 以及不同数据库管理系统(例如 SQL Server 和 Oracle),Linq 都能够对这些数据进行访问和处理,使数据访问统一化、标准化、简单化。

充分挖掘宿主语言的特有功能。因为 Linq 基于一种宿主语言而实现,因此,利用 Linq 进行数据访问很大程度上发挥了宿主语言的强大能力,这无疑将推动目前的各大语言编译器厂商改进和完善其语言。Linq 使用泛型为基础,使用一批新编译技术构建,意味着宿主语言必须依赖于这些特定技术,这就为各种编译器版本定义了标准接口,使应用程序的可移植性大大增强。

Linq 为开发人员带来的远远不止这么多,它甚至提供了对于 SQL-92 标准的通用 SELECT 语法的一个完整的实现。这不仅有利于提高数据访问效率,还可以让开发人员节省时间做更复杂的工作。

实现查询综合的先决条件

Linq 不是一种全新的语言体系,而是基于一种宿主语言的技术扩展,因此它必定需要一些前沿技术作为基础支撑框架。下面列举的一些编译器技术,可能对于 Linq 来说是至关重要的。

 匿名结构和匿名类
 隐式类型
 对象初始值设定项
 泛型
 扩展方法
 Lambda 表达式

本文将针对以上各个关键技术展开剖析,并向开发人员说明为什么需要它们和它们如何工作。为了更加生动的说明问题,本文选择一种完全面向对象语言 C# 进行描述,C# 与 Java 比较类似,但 C# 已经实现了 Linq,这仅仅是一种示例语言,在未来,Java 和 Delphi 或许也将步入查询综合的语言范畴。

匿名结构和匿名类(Anonymous Structures and Classes)

在 Microsoft SQL Server 中执行 SELECT 语句时,很容易从 IDE 上得到查询的结果集,这里不妨深入的想一想,IDE 怎么知道这条查询返回了什么数据,返回了多少数据,每一列数据项的类型是什么呢?这个问题对于 Linq 来说相当重要。因为在现代语言体系中,往往需要“强类型”命名。强类型不仅有利于编译器更好的工作,也有利于提高应用程序的可靠性。但是,类似于 SELECT 语句的返回数据集,不仅数据量是未知的,而且数据列上的类型在返回结果集之前也是未知的,如果将这个返回结果集作为基类型(一般为 Object 类型)描述,这就有悖于强类型约束;编译器需要一种更先进的命名解析机制支持这种延迟计算模型。

C# 3.0 提供一种匿名结构声明,允许开发人员在未知目标类型的前提下声明变量。例如:

var person = new {Name = “zhourong”, Age = 23, Company = “Heading”};

编译器将为这个声明产生新的类型:

class Anoymous_Name_String_Age_Integer_Company_String {
  public string Name;
  public int Age;
  public string Company;
}

类 Anonymous_Name_String_Age_Integer_Company_String 在编译时产生,而不是设计时。再看看开篇的一个例子:

var p = from p in dc.Products.Production,
join e in dc.Employees.Employee on e.EmployeeID == p.ManagerID,
  join pi in dc.Products.ProductImage on p.ProductID == pi.ProductID
  where p.Price >= 50 &&p.ProductName.Contains(“Office”)
  group p by p.Name, pi by pi.ID
  select new {Name = p.Name, Image = p.Image};

可以发现变量 p 的类型是 new { string Name, System.Drawing.Image Image}。var 关键字指明变量 p 被声明为一个匿名类型。

注意,匿名类型不是 Object 类型的代替,Object 类型是任何引用类型的基类型,而匿名类型是 Object 的子孙类型。它在设计时甚至编译时都不可见,直到应用程序使用到它的时候,或编译器有必要强类型化它的时候它的类型信息才产生。

另外还有更加复杂的情况:有些时候目标的类型和类型信息(如包含的属性或域)都不可用,此时的延迟计算模型需要在运行时确定。例如:

var q = from e in Employee select e;
foreach(item in q) Console.WriteLine(item.Name);

变量 item 的类型和类型信息完全未知。当使用成员运算 item.Name 对 q 进行操作时 item 的信息被编译器视为类型占位符,当变量 e 被实例化之后,q 的类型信息才可用。

隐式类型本地变量(Implicity Typed Local Variable)

前面的示例中多次使用 var 关键字声明本地变量。var 引起的声明语句表明此本地变量的类型由编译器自动推断。例如:

var i = 123;
var s = “Hello”;
var array = new[] {1, 3, 4};
var obj = new {Name = “Me”, Age = 20, Height = 176.05};

编译器会根据赋值运算右侧的表达式确定变量的类型,相当于下列等同语句:

int i = 123;
string s = “Hello”;
int[] array = new int[] {1, 2, 3, 4, 5};
struct __anonymous001{string Name, int Age, double Height} obj = new __anonymous001() {Name = “Me”, Age = 20, Height = 176.05};

注意,隐式类型不等同于 Object 类型:隐式类型本地变量的类型是由编译器在编译时根据右侧表达式推断出来的,而 Object 类型本地变量在赋值时需要执行装箱操作,使用时需要进行拆箱操作,并且其类型为基类型。

提出隐式类型的目的在于方便查询综合。查询综合产生的结果实现 IEnumerable 接口,但类型在设计时并不明确,通过隐式类型本地变量,可以很好的消除在声明变量时的类型不明确因素,并为代码提高可用性。

对象初始值设定项(Object Initializer)

由于延迟计算的原因,在某一些编译时类型产生之后,其类型信息并没有确定,而查询综合必须需要一种类型占位符,当该类型没有产生确定类型信息时,将代码对成员信息的访问作为占位符;当实际类型信息可用后,将占位符替换为成员访问的地址。这种技术称之为动态标识符(Dynamic Identifier)。

对象初始值设定项为实现动态标识符提供有力的支持。它允许在声明变量的时候直接对其成员进行访问,看下例。

public class Order {
  public int Number;
  public int State;
  public int Class;
 }
Order[] orders = new Order(Number = 1000, State = 0, Class = 1);
var orders2 = new[] {new Order(), new Order{Number = 1001, State = 100, Class = 2}};

在一对大括号中,可以直接使用“=”运算对类、结构等具备 public 作用域的成员进行访问,这非常有利于查询综合语法。请看这个例子。

var q = from c in Countries, w in WorldWeather
  where c.ID == w.ContryID && w.MeasureUnit == 0
  select new {Name = c.Name, Region = c.Region, MeasureUnit = w.Measure, Date = System.DateTime.Now()};

由于选择出来的国家和天气数据需要关联,这个返回结果集的成员信息和名称都不能确定,而采用匿名结构和对象初始值设定项则完美的解决了这一问题。

泛型(Generic)

如果说没有泛型,那么所有的 Linq 技术都将是白日梦。从理论上来说,任何一个查询的结果集,一定是一个顺序表。这个顺序表可以为空,也可以拥有 0 或者多行记录。在所有平台语言(如 Java 和 C#)中,这种表结构都实现 IEnumerable 接口,因为这些结果数据集必须可以被枚举。

泛型使得 Linq 在多种数据类型上实现成为可能,IEnumerable 提供一个类型参数 T,表示查询结果的返回对象类型,这个类型往往是一个匿名类型,由编译器通过延迟计算得到。

可以将“简单的例子”代码作如下更改:
IEnumerable<string>q1 = from c in countries
  select c.Name;

或者

IEnumerable q2 = from c in countries select c;

这里,q1 的类型是 IEnumerable<string>,q2 的类型是 IEnumerable<Country>,类型参数实例 Country 是由编译器计算出来的。

正因为查询综合返回类型为 IEnumeralbe<T>,所以通过查询综合得到的数据集非常便于访问。请看示例:

foreach(string item in q1) {
  Console.WriteLine(item);
}
foreach(var item in q2) {
  Console.WriteLine(“Name: {0}, Populaton: {1}”, item.Name, item.Population);
}

可以实现这种操作的原因是 foreach 可以枚举实现 IEnumerable<T> 接口的任何类型。

泛型同样为实现扩展方法提供支持。在 Select 函数的实现上,它发挥了重要作用。这将在扩展方法中进行介绍。

扩展方法(Extension Method)

扩展方法的提出是为了解决某些通用过程在多种类型对象上执行的问题。例如,一个 Count 函数可以在任何 IEnumerable<T> 上执行,但有时对于类型 IEnumerable 无法更改其类型信息(因为它是系统对象,不能对它进行重写,也不能增加一个 Count 的公共方法),另一方面,对于 Count 本身来说具备一定的延迟计算特性,在 Count 上执行的计算目标类型可以是 IEnumerable<T>,也可以是 DataTable,也可以是 string。所以,通过改写这些类增加公共的 Count 方法是不明智的,编译器需要引入一种针对于一(或多)种类型限制的扩展方法。

以下的代码实现了在 IEnumerable 上的 Count,这样,查询综合可以利用它,在所有返回的查询结果集上使用 Count 聚合。

[System.Runtime.CompilerService.Extension]
public static class Extension {
  [System.Runtime.CompilerService.Extension]
  public static int Count<T>(this IEnumerable<T> source) {
   int i = 0;
   foreach(T item in source) {
    i++;
   }
   return i;
  }
}

在一个例程上的调用方法如下:

int[] integers = {1, 2, 3, 4, 5};
var names = new string[] {“zhourong”, “chenquan”, “jinlei”};
int countOfIntegers = Count(c);      // 正确调用
var countOfNames = names.Count();      // 正确调用
var q = (from item in names select item).Count();  // 正确调用

扩展方法可以像函数一样调用,也可以作为 surce 类型实例的公共方法调用。两种形式是等价的,注意到,Count 本身被声明为一个泛型方法,类型参数 T 在调用时没有给定,这是由编译器动态计算得到的。

有了扩展方法之后,则可以利用委托实现在 IEnumerable<T> 上的 Select。实现 Select 的代码相当简单,下面的例子就实现了布尔型 Select 的一个版本。

public delegate bool Func<T, S>(T a0, S a1);
 
[Extension] public static IEnumerable<T>Select<T, S>
(this IEnumerable<T> source, Func<T, S> predicate) {
foreach(var item in source) {
   if (predicate)
    yield return item;
}
}

在例程中这样使用 Select 扩展方法:

public static bool Test(Country c) {
  return c.Population >= 1000;
}
var q = countries.Select(Test);

这和通过 from…in…select 语句得到的结果是相同的,事实上,Linq 就是把使用 select 语法的查询综合转换成扩展方法调用形式的。

Linq 实现了很多基于扩展方法的查询,例如 Count、Avg、Take、Skip、Select、OrderBy、GroupBy、Sun、Max、Min 等,在 Linq 的将来版本中,还将实现更丰富的查询函数,开发人员将要并且正在感受到 Linq 给他们带来的无限魅力。

Lambda 表达式(Lambda Expression)

紧接着上一节 Select 的实现阐述。当需要使用 Select 扩展方法时,开发人员需要创建一个布尔函数,并将查询条件放置于此函数体内,如果进行复杂查询,那么这种方法显得非常低效,如果能够使用一种匿名函数体实现这个函数,并将其放置在 Select 函数参数中,则可以大大简化代码。

兰布达表达式(Lanbda)的提出解决了这一问题,Lanbda 表达式类似于匿名方法,即不需要方法声明,只需要提供参数列表和函数体。与匿名方法最大的区别是 Lambda 表达式不利用委托,不明确返回值,而匿名方法则需要显式利用委托和指定返回值(哪怕是 void)。

如果利用 Lambda 表达式重写上一节中的例子,则一个可能的代码段为:
 var q = countries.Select(c => c.Population > 1000);

在 Lambda 表达式中,“=>”符号前方的表达式作为函数参数列表,后方的语句(块)为函数体。这种简化的匿名函数对于查询综合非常有用。参见下例。

var q = c.GroupBy((o, c.Name) => o.Select(item => item.Name == “zhourong”))

这里使用到两个参数的 Lambda 表达式,利用 Select 和 GroupBy扩展选择出结果集。这和以下语句得到的结果完全一致:

var q = from c in countries
  group o by c.Name
  where c.Name == “zhourong”
  select o;

Lambda 表达式为 Linq 提供了简化的调用方式,也使得一种新的语言特性由此而生,Linq 框架对于 Lambda 表达式目前仅支持一种语言(C#),在未来的版本中,Lambda 表达式将被更多语言所引入和升华。

结论

基于语言的查询框架(Linq)引入了一系列新的特性,旨在使基于数据的应用程序开发变得更加轻松。Linq 不仅可以在一部分类型上工作,而且还支持直接访问数据集数据库查询(DLinq、LInq Over DataSet)和 XML 数据库查询(XLinq),可以预见,下一代应用程序开发技术和下一代程序设计语言将更加有利于开发这种数据中心型应用程序,开发人员也必将对这些语言所引入的全新特性赞不绝口。

抱歉!评论已关闭.