Joel Pobar
本文讨论:
|
本文使用了以下技术: C#、C++、F#、IronPython、IronRuby、Visual Basic |
目录
Windows 操作系统 对编程人员而言是再合适不过的平台了。针对 Windows® 的语言有数百种,它们有的直接通过 Win32® API 或 CLR 来实现,但更多的还是通过您的编写来构建。
CLR 的目标之一就是要在一个兼容的生态系统中让百花齐放,将语言和 API 无缝集成在同一运行时中。到目前为止它已取得了巨大的成功——新的语言层出不穷。像 Ruby (IronRuby)、Python (IronPython) 和 PHP (Phalanger) 等 Microsoft 实现属于动态语言范畴,它们现在是 Microsoft® .NET Framework 语言中的一流成员。最近还引入了被称为 F# 的函数化语言。尽管您很可能曾经听到过有关这些新语言和语言模式的讨论,但可能还是想知道其具体含义。
介绍这些语言模式并讲述其中一些重要实现的内容不但能够解答您的疑问,而且还有助于说明其中的一些新语言和旧模式将会如何影响 C# 和 Visual Basic® 语言未来版本的设计和实现。
要了解新设计所表现出来的变化,您需要了解传统语言(如 C# 和 Visual Basic)与新语言(如 F#、IronPython 和 IronRuby)之间的差异。此方面主要涉及三大主题:面向对象的编程(C# 和 Visual Basic 这二者都利用的模型)、函数式编程 (F#) 以及动态编程(IronPython 和 IronRuby)。让我们看一看这些模式并探究一下他们的区别特征。
面向对象
面向对象 (OO) 是您可能最为熟悉的一种模式。通过它您可以描述对象与绑定它们之间交互的约定所构成的环境。OO 利用类型约定、多态性以及精细的可见性等多种功能来提供出色的重用和封装特性。
通常,面向对象的语言采用静态类型系统,因此它们被称为静态类型语言。这意味着程序创建和使用的所有类型都在编译时进行检查;这可防止您对 Duck 类型的对象调用方法 Moo(在此方法不存在的情况下)。在代码运行之前,编译器可检测各种类型之间被破坏和误用的约定,从理论上讲,这样做可减少运行时错误。
但是 OO 也存在一些缺陷。类型安全性可能会使编程人员过分依赖编译器来捕获错误,而不是亲自创建必要的测试基础结构。同时,OO 还会促使编程人员预先定义自己的约定,而这往往是与快速原型编程和多用户编程环境背道而驰的。在大型软件项目中,组件之间的约定(通常由不同的团队所拥有)往往在整个开发周期中不断演变,这就要求约定的使用者不断更新其代码。由于上述这些问题,OO 语言可能显得有些复杂和冗长。
函数式编程
函数式编程将程序计算视为数学函数的计算。将两个数字相加即为一个函数。假定有两个输入值,比方说是 5 和 10,则输出值便为 15。为解决某个问题,函数式编程人员会将该问题细分为多个可使用函数表示的较小的块,然后再将这些函数进行组合以生成预期的输出。
函数式编程通常会避开状态(类似于变量和对象等内容)和状态变异。这实际上是与 OO 相左的,后者的主要目的恰恰是为了创建和操作状态(对象)。由于避开了状态,函数式程序往往更加准确、精密而且可验证。这是由于它很少会产生不可预知的副作用——当程序(如某个变量)的内部状态在各操作之间发生变化时可能会产生一些副作用,这些副作用会导致在一个或多个操作中产生非预期的结果。面向对象的语言和动态语言依赖编程人员来封装和保护状态,以减少不可预知的副作用,因为这些副作用会不可避免地导致更多错误的发生。
函数式语言可以是很纯粹的,也就是说没有任何副作用或状态。但是,大多数流行的函数式语言都具有状态操作功能,这有利于促进与外部 API、操作系统以及其他语言的互操作性。针对某些程序必须使用一定量的状态信息来表示问题这一事实,它们也有相应的考虑。
有一些很有用的功能,它们对函数式语言是通用的。就我个人而言,我比较喜欢高阶函数,此函数可以将另一个函数作为参数,并且返回结果可以是函数,这为代码重用提供了强大的支持。虽然大多数语言已嵌入了此机制,但函数式语言更倾向于将其提升为首要的功能。
下面将以一个典型的 Map API 为例来详细介绍高阶函数的功能,此示例对数组或数据列表中的每个元素都执行(或映射)一个函数。首先,我们使用 JavaScript(一种常用的 Web 脚本编写语言)来编写此函数。给定此数组
var data = [1, 2, 3, 4, 5];
现在编写将对数组中的每个元素执行代码的函数,如下所示:
function map (func, array) { var returnData = new Array(array.length); for (i = 0; i< array.length; i++) { returnData[i] = func(array[i]); } return returnData; }
下面的函数将按 1 递增某个数值:
function increment(element) { return element++; }
现在让我们开始使用它:
print (map (incremenent, data)); output: [2,3,4,5,6]
此处的 Map API 示例虽然非常简单,但却演示了高阶函数的功能。Map 有一个函数和一个数组,它对每个数组元素都执行该函数,并在一个新数组中返回结果。
假定没有变异的状态(为返回的结果创建一个新数组),并且没有函数结果依赖于先前的结果,则现在即可开始考虑将这一映射示例扩展到多个处理内核(通过将数组分成两部分并将得到的两个数组连接起来)乃至多个计算机(跨 n 个计算机拆分序列化数据、将代码函数传递到计算机、在一个单独主机中执行并连接序列化结果),而不必担心诸如状态管理等并发操作问题。如此强大的功能用在这里真的是大材小用了!
编程语言 F# 最初是由 Microsoft Research 的 Don Syme 所开发,是 Microsoft 对有关 .NET Framework 函数化编程需求的回应。在发布时,它将是在 Visual Studio® 中受到全面支持的语言。F# 和 Visual Studio 集成包均可从 go.microsoft.com/fwlink/?LinkId=112376 下载。另外,由 Robert Pickering 编写的 Foundations of F# 对该语言做了精彩的介绍。
言归正传,下面我们将使用 F# 来探究函数化语言环境的内幕。我们仍以 Map 为例,但这次是在 F# 中:
// increment function let increment x = x + 1 // data let data = [1 .. 5] // map (func, myList) let map func myList = { for x in myList -> func x } print_any (map increment data)
的确,它非常简洁。第一行定义一个简单的递增函数,它有一个参数 x,此函数将计算 x + 1。然后,将数据定义为一个不可变异的列表,其中包含整数 1 到 5。此映射函数以函数和列表作为参数,使用传统编程中的喷淋方法来遍历列表并执行 func 函数参数,然后将列表中的当前元素传递给它。接下来,print_any 函数使用增量函数和数据列表作为参数来执行映射函数,随后将结果打印到屏幕。
类型在哪里?在本例中并不存在。实质上,变量(数据)实际被类型化为 System.Int32 的列表,但并不需要将此告知编译器,因为它使用类型推断功能即可推断出这一情况。编译器做的工作越多,您的工作就会越少。这听起来非常不错。使用列表推导功能,您在一行内就可以轻松地重写先前的部分代码:
print_any { for x in 1..5 -> x + 1 }
另一个很酷的 F# 功能是模式匹配:
let booleanToString x = match x with false -> "False" | _ -> "True"
此函数具有一个 Boolean 类型,此类型可与 false 或任何其他值匹配,并返回相应的字符串。如果向 booleanToString 函数传递一个字符串,会出现什么结果?同样,编译器承担了繁重的工作,将 x 参数的类型定义为类型 bool。它通过 x 在此函数中的用法推断出这一点(在这种情况下,它仅与 bool 类型相匹配)。
模式匹配还可以用于构建强大的函数调度机制,以便在 OO 及其他环境中轻松地重现虚拟方法调度。当您需要根据接收方(将在此对象中调用虚拟方法)和方法参数的变化来改变行为时,虚拟方法才真正开始起作用。访问者模式即是为了帮助解决这种情况而设计的,但是在 F# 中基本不需要(已包含在模式匹配中)。
支持用户定义的类型,通常由记录(类似于 OO 环境中的类)或聚合(通常是一种有序序列类型)提供。以下是用户定义的队员和球队记录:
type player = { firstName : string; lastName : string; } type soccerTeam = { name : string; members : player list; location : string; }
延迟计算是另一种常见的函数式语言功能,其功能源自这样一种理论,即函数化编程中没有明显的副作用。延迟计算依赖于编译器和编程人员选择表达式计算顺序的能力,它可以使计算延迟到所需的时间点。编译器和编程人员都可以使用延迟计算技术作为精确的性能优化手段,因为它可以避免一些不必要的计算。在处理无限(或极大)数据集的计算时,它也是一种有用的技术。实际上,Microsoft Research 的 Applied Games 研究组曾在 F# 中使用此技术解析过数 TB 字节的 Xbox LIVE® 日志数据。
以下是 F# 中的延迟计算:
let lazyTwoTimesTwo = lazy (2 * 2) let actualValue = Lazy.force lazyTwoTimesTwo
此代码执行时,lazyTwoTimesTwo 只是在运行时充当指向执行 2 * 2 的函数的轻型指针。仅当实际强制执行此函数时,才能获得结果。
虽然函数化编程模型可能不太容易理解(可能需要 30-40 小时的准备时间),但一旦掌握它,您的编程能力就会有质的飞跃。您只需使用很少的代码就可以解决问题并减少错误。此模式通过最大程度减少意外的副作用来保护代码的安全,并通过执行积极的优化措施使其保持快速运行状态。此外,简单性往往代表出色的扩展性——您只需看一下 Map 代码,想想该代码如何能够轻松分布到成千上万台计算机中,就可以了解其中的缘由了。
至此,您已经看到了函数化编程的一些非常出色的功能,例如类型推断、高阶函数、模式匹配和用户定义类型等。现在让我们了解一下动态语言。
动态语言
动态编程早在 20 世纪 90 年代中期就开始流行了,当时正是基于 Web 的应用程序大行其道的时候。在将服务器上的交互式动态元素添加到它们所驱动的网站上时,Perl 和 PHP 是最常用的两种动态语言。从那以后,动态语言开始获得越来越多的驱动力,为新一代软件开发方法(如敏捷开发)指明了技术出路。
在编译和执行程序代码方面,动态编程语言与静态语言(C# 和 Visual Basic .NET 均为面向对象的静态语言)有所不同。使用动态语言时,代码编译通常会被延迟到运行时,即当实际开始运行程序的时候。在其他情况下,只是简单地对代码进行解释。通过编译过程中的这些延迟功能,程序代码可以包括并执行诸如动态扩展对象等行为,并允许编程人员根据需要来处理类型系统。
实际上,正是将进程延迟到最后一刻的实时 (JIT) 编译和执行技术为动态语言赋予了强大而又丰富的功能集。因此,动态语言通常被称为后期绑定,因为所有操作绑定(如调用方法或获取属性)都是在运行时而非编译时完成的。为了说明动态语言的一些功能,下面我们将详细介绍一下 IronPython 和 IronRuby。
IronPython 是 .NET Framework 中 Python 编程语言的 Microsoft 实现。它是 Jim Hugunin 智慧的结晶,其原型最早是诞生在 Jim Hugunin 的车库中。六个月后,Jim Hugunin 加入了 Microsoft 并构建了 IronPython 编译器。您可以从 go.microsoft.com/fwlink/?LinkId=112377 下载 IronPython 的完整源代码和安装程序。
IronRuby 是为 Ruby 编程语言的 Microsoft 实现所赋予的名称。John Lam 是一名活跃的 Ruby 黑客,他编写了名为 RubyCLR 的 Ruby-to-CLR 桥梁。后来他加入了 Microsoft 的 IronRuby 团队。您可以从 ironruby.net 下载完整的 IronRuby 源代码。
接下来,我们将开始使用 IronPython,介绍一项名为 Read Eval Print Loop (REPL) 的出色功能。REPL 循环被设计用于逐行输入代码并执行。它实际展示的是动态语言(如 Python)的后期绑定性质。图 1 显示了 IronPython 的命令行 REPL 循环。
图 1 IronPython REPL 循环 (单击该图像获得较大视图)
REPL 循环接受 Python 代码作为输入,然后它将立即执行代码。在本例中,我想让它将 "Hello, World!" 打印到屏幕上。只要编程人员敲击一下 Enter 键,IronPython 编译器就会将该语句编译为中间语言 (IL) 并执行(全部都在运行时中)。
REPL 循环还遵循该语言的全部作用域规则,在本例中,我将表达式 1 + 2 赋给了变量 "a",在下一行中我将可以引用此变量。REPL 输入内容的编译是在进程内进行的。无需生成 .dll 或 exe 文件;IronPython 编译器仅使用 CLR 的内存代码生成功能,即轻量级代码生成 (LCG)。
动态语言的类型系统非常灵活,而且要求宽松。通常它可以支持面向对象的概念(如类和接口),不过需要注意:在静态语言中存在的严格的约定强制执行情况在这里一般不会出现。对于这一相对较为宽松的类型系统,一个典型示例就是鸭子的类型化概念,这是一种受到广泛支持的动态语言类型系统功能。在鸭子类型化中,如果某个类型看上去像鸭子并且叫起来也像鸭子,则编译器就会假定它是鸭子。图 2 使用 Ruby 显示了运行中的这一概念,如果愿意,您也可以在 IronRuby 中运行此代码。
Figure 2 Ruby 中的鸭子类型化
图 2 显示了两个类定义(Duck 和 Cow)、一个名为 QuackQuack 的方法以及用来实例化这些对象的代码,然后又将对象传递给 QuackQuack 方法。此代码在加载并运行后会输出以下内容:
Quack! Cow's don't quack, they Mooo!
QuackQuack 方法通过方法参数调用 Quack 方法,它并不关心该参数的类型,只关心该参数是否有方法 Quack。Quack 方法的查找和调用是在此参数的运行时而非编译时进行的,这将允许编程语言先确保该参数看上去形似鸭子,然后再考虑其叫声。
在静态类型化环境中,接口就类似于上面所说的鸭子类型化。如果某个类型要在静态环境中实现某个接口,则静态类型系统会强制其遵循全部完整的接口结构,以使对任何实现类型的接口方法调用都能够得到保证。鸭子类型化仅关注类型的结构部分,即所处理的方法名称。它通过后期绑定来实现该目的——编译器只生成代码,对对象执行方法查找(通常使用基于字符串的方法名称),然后再调用该方法(如果查找成功)。
在 IronPython 和 IronRuby 等动态语言中,语言功能并非是唯一的亮点。动态语言甚至还允许您托管 API,这意味着您可以在自己的应用程序中托管语言、编译器甚至 REPL 交互式功能。假设您正在构建一个庞大而复杂的应用程序(例如照片处理工具),而且您希望为用户提供一个自动化或脚本处理工具。您可以托管 IronPython 或 IronRuby 并允许用户创建 Python 或 Ruby 脚本,使其可以通过编程方式来操作您的应用程序。为此您只需将应用程序的内部对象和 API 提供给托管 API 的语言、创建一个文本窗口来托管 REPL 循环、然后在应用程序中启用脚本编写功能即可。
使用诸如命令行驱动 REPL 循环等功能可以在生产力方面带来非常不错的连带效应:您可以轻松地构造工作原型而不必执行保存-编译-运行这一周期。我们提倡动态修改和扩展类型,这可以使您快速获得解决方案(从构想到代码),而无需担心接口或类定义约定的形式。取得进展后,您即可从动态语言原型转到更严格、更安全的静态语言环境(如 C#)中。
实际上,灵活的类型系统还擅长处理非结构化的数据(如 Web 上的 HTML),对于可能会随着时间的推移而更改版本的严格接口而言,可以很容易地将其用作防更改粘合剂。在基于 Web 的计算等新兴应用程序模式中,动态语言提供了一种用于处理不确定情形的强大解决方案。
托管 API 会为您的应用程序提供一个最佳的扩展点;您可以提供与 Microsoft Excel® 和 VBScript 宏类似的应用程序体验。
安全实用
安全性和实用性也是在语言选择中需要考虑的重要事项。语言的实用性可通过其易用性、高效性等因素来衡量。语言安全性的衡量标准包括其类型安全性(是否允许无效转换)、编程屏障(是否会跨越数组边界并影响堆栈中的其他内存内容)、安全功能等等。有时您可以牺牲实用性来换取安全性,反之亦然。
有趣的是,您会发现 C#、C++、Visual Basic .NET、Python 及类似的语言实用性很强,但缺少安全分类。虽然托管代码语言(如 C#)要比传统的本机代码安全一些,但您仍可能会遇到麻烦。由于这些语言关注的是状态操作,因此它们通常都有一些未知的副作用——在并发环境下工作时,这将会是一个问题。
在单核计算机中可以正常运行的一个 C#/Visual Basic .NET 多线程程序,在多核计算机中运行时可能会崩溃,这是由于争用条件引发了一个编程人员所不知道的副作用。另外,尽管这些语言具有良好的一般等级的类型安全性,但是它们仍然无法防止编程人员在没有察觉的情况下将某个对象转换为 void*。
Haskell(学术界使用的一种纯函数式语言)属于虽然安全但实用性稍差的一类。由于 Haskell 没有副作用(代码块是函数,变量永远不会变化),而且它会强制编程人员预先定义其类型和意图,因此它被视为一种安全语言。遗憾的是,Haskell 极难理解,甚至对技术精湛的编程人员也是如此,因此通常很难读懂他人所编写的 Haskell 代码。
在我看来,考虑到与 .NET Framework 的集成、可选的过程语法、处理面向对象环境下的 Objects 的能力等特点,因此 F# 要比 Haskell 更为实用。很显然,下一代语言会很好地借鉴我所介绍的这些语言的长处。如果有一种语言能够将呆板、强制性的静态类型系统动态地变为动态、灵活的类型系统,不但具有函数化编程功能而且可以避免并发问题,那有谁会不喜欢这种语言呢?
令人欣慰的是,由于通用类型系统 (CTS) 的出现,现在可以很容易地从 C# 转换到 F# 和 IronPython,然后再重新转换回来。而且 C# 和 Visual Basic .NET 的设计人员借鉴了动态模式和函数化模式的长处,将其直接集成到了这些语言中作为优先功能。接下来让我们了解一下其中的几个功能,看一看它们是如何与您已经了解的各种语言及其模式相关联的。
LINQ
LINQ 是 .NET Framework 中的一项新功能,可直接用作 .NET 中任何语言的 API,也可以通过一组 LINQ 语言扩展来实际作为一种语言功能。通过提供一组与 T-SQL 语言类似的查询运算符,LINQ 可以使用一种与存储无关的方式来查询数据。.NET Framework 3.5 中的 Visual C#® 2008 和 Visual Basic 2008 均支持 LINQ 作为一流成员。
使用 Visual C# 2008 执行的典型 LINQ 查询如下所示:
List<Customer> customers = new List<Customer>(); // ... add some customers var q = from c in customers where c.Country == "Australia" select c;
此代码会遍历客户列表,查找包含 Country 属性且属性值为 "Australia" 的 Customer 对象,然后将其添加到新列表中。首先您会注意到关键字 "var",它是 Visual C# 2008 中新增的一个关键字,用来告诉编译器执行类型推断。然后您会看到操作中的 C# LINQ 扩展,其中包含 from、where 和 select 关键字。这些构成了一个完整的客户列表(列表推导),正如您在 F# 语言中看到的,这是动态语言中的一个常见功能。
由于您是在此 LINQ 查询执行时新建列表,因此实际上没有任何副作用,原始客户列表不会发生改变。副作用对每个并发程序而言都是一件很麻烦的事,但这种编程风格会在副作用中为您带来安全性,同时维护代码的可读性和整体实用性。假定没有任何副作用,则您可根据需要对查询范围进行扩展。
实际上,这正是 Parallel LINQ (PLINQ) 的目的所在,它是对 LINQ 的扩展,作为社区技术预览通过 Parallel FX 库 (go.microsoft.com/fwlink/?LinkId=112368) 提供给用户——跨多个 CPU 核心并行执行 LINQ 查询,这样不但使查询速度几乎呈线性增加,而且不会增加编程成本!这与函数式编程部分列举的 Map API 示例非常相似。总之,LINQ 在 C# 和 Visual Basic 中的集成不但增加了一个函数类型,还引入了一些活力,它使这些语言更具表现力,而且更简练。
Visual Basic 9.0 中的内嵌 XML
有人说内嵌 XML 是 Visual Basic 中添加的最佳功能,我完全同意。虽然它不像是源自函数式或动态模式的内容,但它的实现是直接在动态语言环境中完成的。图 3 显示了在 Visual Basic 中内嵌的 XML。
Figure 3 操作中的内嵌 XML
此代码使用 Visual Basic LINQ to XML 实现来查询客户列表中那些居住在美国的客户,并返回 XML 元素的列表作为 RSS 源的基础。然后,此代码定义 RSS 源架构并使用 Visual Basic 中非常奇妙的 "..." 运算符来遍历 XML 文档以查找 "<channel>" 元素。接下来,代码对该 <channel> 元素调用 ReplaceAll,以包括在先前创建的所有 XML 元素。大功告成。
在这里需要注意的是 ... 运算符以及内嵌 XML 元素的使用。请注意,Visual Basic 并不知道 XML <channel> 元素(实际上,您可以将其改为 <foo>,它也会正常编译),因此说,如果 Visual Basic 不知道这一点,可能就无法静态检查该调用点是否正确。相反,在这里编译代码将会在运行时执行动态检查(类似于您在动态模式下见到的检查)。它利用的是后期绑定中的动态语言概念。
对于早餐准备享用 IL 的用户,让我们打开程序集,看一看 "...<channel>" 语句的代码:
ldloc.1 ldstr "channel" ldstr "" call class [System.Xml.Linq]System.Xml.Linq.XName [System.Xml.Linq]System.Xml.Linq.XName::Get(string, string) callvirt instance class Generic.IEnumerable'1<XElement> XContainer::Descendants(System.Xml.Linq.XName)
Ldloc.1 加载 rssFeed 变量、加载 channel 字符串,然后调用 XName.Get 方法,此方法返回 Xname。此 XName 结果被传递给 Descendants 方法,然后此方法返回与 <channel> 名称匹配的所有 XML 文档后代。此机制是典型的后期绑定动态调度,它直接借用了动态语言的内容。
别急,还有一些内容需要讨论。如果将 RSS 架构定义置入 Visual Studio 中并添加对此架构的 "Imports" 引用,则 Visual Basic .NET 编译器将提取此架构并加以分析,然后对符合此架构的所有 XML 启用完整的 IntelliSense®。因此,如果您进入 RSS XML 架构中,"...<channel>" 语句会立即显示一个 IntelliSense 窗口,其中包含根据架构定义提供的各种可能的后代选项。太棒了!
更多资源
有关 C# 和 Visual Basic 等语言如何与其他模式和语言的最佳元素相结合的内容,我仅提及了少数几个示例。现在,语言设计团队正在向其语言中增加新的类型,以应对未来编程的挑战。
虽然我们已经探讨了几个高级别的编程模式类别,但这些只是其中的一部分。声明性编程(描述某种事物的方法)使用 XML 作为描述符,对通过框架(如 Windows Presentation Foundation 和 Windows Workflow Foundation)进行的开发有着巨大的影响。它们还非常擅长描述和操作数据。随着组织多年来所创建的数据资产的不断增长,如果今后想尝试通过编程方式来开发这些资产,我想声明性语言会成为其中的驱动力。还有一种逻辑编程(利用计算机编程逻辑),虽然我没有提及这种编程方法,但它与 Prolog 等语言一起被广泛地应用在人工智能和数据挖掘领域。
随着不断向所有这些语言和模式中添加各种新功能,您也必将从中获益。通过这种取长补短,您所喜爱的语言会变得更加完善,而您也可以获得更大的灵活性和更高的生产率。如果您想使用其他语言中的一些比较出色的功能,这也好办;您所喜爱的运行时和框架已经开始支持这种互操作情形。作为一名 .NET 编程人员真是太棒了。
Joel Pobar 以前是 Microsoft CLR 团队的一名项目经理。他现在住在澳大利亚的黄金海岸,潜心钻研编译器、语言和其他一些有趣的东西。您可以在 callvirt.net/blog 上查看他的最新 .NET 随笔。