Visual Studio 2015高级编程(第6版)
上QQ阅读APP看书,第一时间看更新

16.4 F#

F#(读作F Sharp)是一种语言,源自英国剑桥大学的Microsoft Research,由把泛型引入.NET Framework的Don Syme发明。Visual Studio 2015中附带了F#语言,它是一种多范例的函数式语言。这意味着它主要是一种函数式语言,但支持其他编程风格,如命令式和面向对象的编程风格。

16.4.1 第一个F#程序

启动Visual Studio 2015,创建一个新的F#项目。如图16-2所示,F# Application模板位于New Project对话框的Visual F#节点下。给它命名,并单击OK按钮。

图16-2

F# Application模板仅创建一个F#项目,它只有一个源文件Program.fs,其中只有应用程序的主入口点和一个对F# Developer Center(http://fsharp.net)的引用。如果希望了解F#的更多信息,那么一个很好的起点是F# Tutorial模板。它会创建一个正常的F#项目Tutorial.fs,而不是主源文件,其中包含约280行代码,演示了如何开始使用F#。查看这个文件,找出可用的语言特性是一个有趣的练习。但现在返回Program.fs,快速建立和运行规范的Hello World例子,以了解编译和交互操作可用的各种选项。添加下面的代码:

      #light
      printfn "Hello, F# World!"
      let x = System.Console.ReadLine();

第一条语句#light是一个编译标志,表示代码是用可选的轻型语法编写的。在这个语法中,空白缩进是很重要的,因为不再需要某些标记,如in和;;。第二条语句仅在控制台上输出“Hello, F# World!”。

如果使用F#的以前版本,就会发现代码现在会抛出编译错误。F#诞生于一个研究项目,现在转变为一个商业产品。另外,该语言进行了重构,一些操作从FSharp.Core移到了支持的程序集中。例如,print_endline命令被移到FSharp.PowerPack.dll程序集中。F# Powerpack可以通过http://fsharppowerpack.codeplex.com 或 NuGet下载获得。

运行F#程序有两种方式。第一种方式是按照通常的方式运行应用程序(按F5键开始调试)。这会编译并运行程序,如图16-3所示。

图16-3

运行F#程序的另一种方式是使用Visual Studio中的F# Interactive窗口。这允许在Visual Studio中突出显示和执行代码,并立即查看程序的运行结果,还可以随时修改运行中的程序。

F# Interactive窗口可以通过View | Other Windows | F# Interactive菜单项,或者按下Ctrl+Alt+F组合键来访问,如图16-4所示。

图16-4

在Interactive窗口中,可以通过REPL提示符与F#编译器交互操作。也就是说,输入一行F#代码,就会立即编译并执行它。如果要快速测试某些想法是否正确,并随时修改程序,就可以选择使用REPL。它允许快速试验各种算法,并快速建立原型。

但在F# Interactive窗口的REPL提示符上,没有Visual Studio通过IntelliSense提供的值、代码片段等。最佳方式是把两者结合起来:使用Visual Studio文本编辑器创建程序,把其输出传送到Interactive Prompt上。为此,可以在F#源代码的任意突出显示代码上按下Alt+Enter键。或者使用右击上下文菜单把突出显示的源代码传送到Interactive 窗口中,如图16-5所示。

图16-5

按下Alt+Enter键或者选择Execute in Interactive命令,将突出显示的源代码传送到Interactive窗口中并且立刻执行,如图16-6所示。

图16-6

图16-6还显示了F# Interactive窗口的右击上下文菜单,在其中可以取消Interactive计算(用于长时间运行的操作)或重置Interactive会话(舍弃以前的状态)。

16.4.2 研究F#语言特性

尽管详细介绍F#语言超出了本书的范围,但应研究一下F#支持的一些很不错的语言特性。这些语言特性可以激发起读者对F#的兴趣,也是学习这种杰出的编程语言的催化剂。

在F#中,一个十分常见的数据类型是列表,它是一个有表达性运算符的简单集合类型。可以定义空列表、多维列表和传统的平面列表。F#列表是不能改变的,所以在创建好F#列表后就不能修改它,只能制作它的副本。F#有一个特性List Comprehensions,可使列表的创建、操作和推导更简单、更富有表现力。考虑下面的代码:

      #light
      let countInFives = [ for x in 1 .. 20 do if x % 5 = 0  then yield x ]
      printf "%A" countInFives
      System.Console.ReadLine()

方括号中的表达式对包含元素1~20的列表执行了一个传统的for循环(表达式“..”是创建一个包含元素1~20的列表的缩写形式)。do表示要在列表中的每个元素上执行for循环。这里,它的含义是“当x除以5的余数是0时,就返回x”。方括号是“创建一个新列表,其中包含所有返回的元素”的缩写形式。这是在一行代码中随时定义新列表的一种表现力非常丰富的方式。

F#的Pattern Matching特性是创建控制流的一种灵活、强大的方式。在C#中,有switch语句(或一组嵌套的if else语句),但通常受限于要匹配的类型。F#的模式匹配特性与此类似,但比较灵活,允许测试指定的任意类型或值。例如,下面在F#中使用模式匹配定义了Fibonacci函数:

      let rec fibonacci x =
           match x with
           | 0 | 1 -> x
           | _ -> fibonacci (x - 1) + fibonacci (x - 2)
      printfn "fibonacci 15 = %i" (fibonacci 15)

竖杠运算符“|”指定要匹配函数的输入和竖杠右边的表达式。当x匹配0或1时,第一行返回函数的输入x。第二行返回调用Fibonacci的递归结果(其输入是x–1)与另一个递归调用(其输入是x–2)之和。最后一行把Fibonacci函数的结果写到控制台上。

函数中的模式匹配有一个有趣的副作用:在不同的接收参数类型上的分支和控制流更容易、更清楚。在C#/VB.NET中,传统上要根据参数类型编写一系列重载版本,但在F#中,这是不需要的,因为模式匹配语法允许在一个函数中得到相同的功能。

惰性求值(Lazy evaluation)是函数式语言的又一个共有特性,F#也有这个特性。它表示编译器只在需要时才安排函数或表达式的求值,而不是事先计算它们。因此,只有在绝对需要的情况下才运行代码——执行的循环和工作集越少,速度就越快。

传统上,把一个表达式赋予一个变量时,该表达式会立即执行,以把结果存储在变量中。而使用函数式编程没有副作用的理论,不需要立即计算这个结果(因为不需要按顺序执行),因此应只在实际需要变量结果时才执行表达式。下面是一个简单例子:

      let lazyDiv = lazy ( 10 / 2 )
      printfn "%A" lazyDiv

首先,用lazy关键字表示仅在强制时才执行函数或表达式。第二行把lazyDiv中的内容输出到控制台上。如果执行这个例子,控制台输出就是“(unevaluated)”,这是因为在后台,printfn的输入类似于委托。实际上需要在获得返回结果之前强制执行或调用表达式,如下面的示例所示:

      let lazyDiv2 = lazy ( 10 / 2 )
      let result = lazyDiv2.Force()
      print_any result

lazyDiv2.force函数强制执行lazyDiv2表达式。

在优化应用程序性能时,这个概念非常强大。在提高启动性能和运行性能时,减少应用程序需要的工作集或内存是非常重要的。在处理大量的数据时,也需要惰性求值这个概念。如果需要迭代存储在磁盘上的海量数据,那么可以给该数据编写一个惰性求值包装,仅在需要时处理该数据。Microsoft Research的Applied Games Group详细描述了如何给这种情形使用F#的惰性求值特性,其网址是http://blogs.technet.com/apg/archive/2006/11/04/dealing-with- terabytes-with-f.aspx

16.4.3 类型提供程序

应用于F#的类型提供程序概念相对简单直观。现代的开发工作涉及引入许多不同来源的数据。为处理这些数据,需要将其封送到应用程序可以操作的类和对象中。可以方便地手动创建所有这些类,但这也会增加产生bug的可能性。我们经常使用代码生成器来解决此问题。但是,如果以交互操作模式使用F#,则传统的代码生成器就不是最佳的选择。每次调整服务引用时,就需要重新生成代码,这是非常令人厌烦的工作。

为解决此问题,F#引入了许多生成时类型提供程序来处理常见的数据访问情况,包括访问SQL关系数据库、Open Data(OData)服务和WSDL定义的服务。此外,也能够创建和使用自定义的类型提供程序。

下面的示例介绍了如何使用类型提供程序访问SQL Server数据库:

F#

      #r "System.Data.dll"
      #r "FSharp.Data.TypeProviders.dll"
      #r "System.Data.Linq.dll"
      type dbSchema = SqlDataConnection<"Data Source=.\SQLEXPRESS;Initial
      Catalog=AdventureWorks2014;Integrated Security=SSPI;">
      let db = dbSchema.GetDataContext()
      let qry =
              query {
                  for ow in db.Customers do
                  select row
              }
      qry |> Seq.iter (fun row -> printfn "%s, %s" row.Name row.City)
      ;;

为运行上述代码,需要向你的项目中添加许多引用。即使运行在F#交互操作模式中,情况也是如此。确切地讲,需要添加System.Data和System.Data.Linq程序集。对于SQLDataConnection类和相关的F#数据功能,需要添加FSharp.Data.TypeProviders程序集。

添加必要名称空间的引用后,使用type声明访问类型提供程序。这样就可以把dbSchema变量创建为类型,该变量包含代表AdventureWorksLT2014数据库中数据库表的所有生成类型。调用GetDataContext后,db变量通过其属性拥有所有的表名,从而可以遍历表中的行,并打印出每一行中客户的姓名和所在城市。

16.4.4 查询表达式

早期版本的F#缺乏对LINQ的支持。许多C#和VB开发人员已经发现,LINQ是一种强大的语法,可用于查询许多不同的数据源以及根据应用程序的需要调整产生的数据。在F# 3.0中,可以构建和执行LINQ查询,扩展该语言的表达能力。考虑如下所示的代码片段:

F#

      open System
      open System.Data
      open System.Data.Linq
      open Microsoft.FSharp.Data.TypeProviders
      open Microsoft.FSharp.Linq
      type dbSchema = SqlDataConnection<"Data Source=.\SQLEXPRESS;Initial
      Catalog=AdventureWorks2014;Integrated Security=SSPI;">
      let db = dbSchema.GetDataContext()
      let qry =
              query {
                  for ow in db.Customers do
                  where (row.City == "London")
                  select row
              }
      qry |> Seq.iter (fun row -> printfn "%s, %s" row.Name row.City)

这个代码片段执行与前一节中的对应代码片段几乎完全相同的功能。不同之处在于,此处只显示位于城市London的客户。在查询语句中使用了LINQ语法,从而能够基于指定的条件筛选行。虽然使用的关键字稍有不同,但C#和VB中的大多数LINQ功能基本相同。

16.4.5 自动实现属性

可按两种方式之一在F#中定义属性。两种方式的区别在于是否希望属性具有显式的后备存储。创建属性的“传统”方式是定义存放属性值的私有变量。然后通过属性的get和set方法揭示属性值。另一方面,如果不需要或不希望创建私有变量,则F#可以自动生成该变量。这就是自动实现属性的底层概念。下面的代码片段显示了传统方式和自动实现方式:

F#

      type Person() =
         member val FirstName
            with get () = privateFirstName
            and set (value) = privateFirstNam <- value
         member val LastName = "" with get, set

最后一行实际上就是自动实现属性。与具有显式get和set方法的FirstName属性(使用privateFirstName变量作为后备存储)不同,LastName被定义为默认是空字符串,并且采用编译器生成的变量。