第六章 组织、注释、引用代码(二)

第六章 组织、注释、引用代码(二)


条件编译(Optional Compilation)

 

[

Conditional Compilation。可选编译,或编译选项。

]

 

条件编译,可以让编译器忽略源文件中的不同部分,大多数编程语言都支持某种条件编译。它可能很方便,例如,如果你想让生成(build)的库函数支持 .NET 1.1 和 2.0,并且,想包括额外的值和类型,以利用 2.0 版中的新功能。然而,不应该滥用这一技术,要很慎重,因为,这会使理解与维护代码变得相当困难。

F# 中的条件编译,使用编译器开关 --define FLAG,并在源文件中使用 #if FLAG 指令。

也可以使用 Visual Studio 的生成配置菜单(参见图 6-3)。

第六章 组织、注释、引用代码(二)_第1张图片图 6-3 Visual Studio 的生成配置菜单

 

注意,使用 Visual Studio,可以添加两个预定义的开关 [ 不是编译符号 ]:DEBUG 和 TRACE。这是两个专门的开关,因为它们会影响某些框架的方法。比如:通过调用 Assert.Debug的断言,只在定义了 DEBUG 符号时才触发。

下面的代码演示了如何定义一个语句的两个不同版本,其中一个是用于编译代码的,另一个是作为 F# 交互脚本的代码(如果你打算编译这段代码,需要引用 System.Windows.Forms.dll):

 

open System.Windows.Forms

 

// define a form

let form = new Form()

 

// do something different depending ifwe're runing

// as a compiled program of as a script

#if COMPILED

Application.Run form

#else

form.Show()

#endif

 

在示例中,我们不必要定义符号 COMPILED,因为,当编译程序时,F# 已经为我们定义好了。类似的,F# 交互定义了符号 INTERACTIVE,因此,可以用来测试是否在交互模式。

 

 

注释

 

F# 提供两种注释。单行注释,使用两个斜线(//)开始,直到这一行的结束。如下面的示例:

 

// this is a single-line comment

 

在 F# 中,单选注释通常是首选,因为这个字符的输入更快、更容易。

多行注释,以左括号和星号开始,以星号和右括号结束。如下面的示例:

 

(* this is a comment *)

 

 或者

 

(* this

is a

comment

*)

 

通常,多行注释只用于临时注释掉大段代码。与其他大多数语言不同,F# 可以嵌套多行注释,与 O'Caml 的注释相同。如果多行注释不封闭,会编译出错。

 

 

文档注释(Doc Comments)

 

文档注释,可以从源文件中提取注释,以 XML 或 HTML 的格式。这是非常有用的,因为,这样程序员就可以浏览代码的注释而不看源代码。对于 API 的提供者尤其方便,因为可以不提供源代码,只提供有关的文档;而且,这种方法更方便的地方在于,不要打开源文件,就能浏览文档。另外,文档随源文件保存,当修改代码时,有更多的机会更新文档。

文档注释使用三个斜线(///)开始,不是两个。它只和顶层定义的值、类型相关联,并且,只和紧随其后的值、类型相关联。下面的代码使注释 this is an explanation 和值 myString 相关联:

 

/// this is an explanation

let myString = "this is a string"

 

为了把文档注释提取成 XML 文件,使用 -doc编译开关。比如,上面的例子保存为prog.fs,提取文档注释的命令如下:

 

fsc -doc doc.xml Prog.fs

 

产生的 XML 如下:

 

    Prog

   

       

         

               this is an explanation

         

      

     

    

  

 

然后,就可以用各种工具来处理这个 XML 文件,比如,Sandcastle (http://www.codeplex.com/Sandcastle),它可以把 XML 文件转换成更可读的格式。我发现,把 Sandcastle 和 Sandcastle 帮助文件生成器(http://www.codeplex.com/SHFB)一起使用会更好。编译器也支持直接从文档注释生成 HTML;虽然,它不如 XML 灵活,但不要额外的努力就能产生更有用的文档;在某些情况下,也可以产生更好的结果,因为,文档生成工具对有些记号的支持不好,比如,泛型和联合类型等。在第十二章将学习有关生成 HTML 的编译器开关。

在 F# 中,不需要显式添加任何 XML 标记,例如,

标记会自动添加。这很有用,因为,这可节能大量的输入,避免了浪费源文件中的空间。然而,如果你愿意,也可以控制并显式写上 XML 标记。下面是一个文档注释,其中有显式写出的标记:

 

///

/// divides the given parameter by 10

///

/// the thing to be divided by10

let divTen x = x / 10

 

这会产生如下的 XML:

 

   AnotherProg

  

     

         

               divides the given parameter by10

         

         the thing to be divided by 10

     

     

     

  

 

如果这个模块文件没有函数类型表示文件,那么,文档注释就会直接从模块文件本身中提取;如果有函数类型表示文件,那么文档注释就会从函数类型表示文件产生。就是说,如果为编译器提供了这个模块的函数类型表示文件,即使模块文件中有文档注释,也不会包含在结果的 XML 或 HTML 中。

 

 

交叉编译的注释(Comments for Cross Compilation)

 

为了能够更方便地在 F# 与 O'Caml 之间进行交叉编译,F# 把某些注释标签当作条件编译符号(optional compilationsflags)看待。放在注释标签 (*F# F#*) 之间的所有代码将会被编译,就好像这个注释标签根本不存在;而这些代码对 O'Caml 编译器而言,是作为正常的注释出现,因此,将会被忽略。类似地,F# 编译器将会忽略(*IF-OCAML*) (*ENDIF-OCAML*) 之间的代码,就扑面是普通的注释;然而,O'Caml 编译器把其中的内容看作正常的代码。这种简单而在效的机制化解两种之间的细小差异,使交叉编译更方便。下面的示例展示了这种注释,如果你使用 F# 语法的 O'Caml 兼容版本,保存文件的扩展名为 .ml,而不是.fs:

 

(*F#

printfn "This will be printed by an F#program"

F#*)

(*IF-OCAML*)

Format.printf "This will be printed byan O'Caml program"

(*ENDIF-OCAML*)

 

用 F# 编译前面的代码,会得到下面的结果:

 

This will be printed by an F# program

 

 

自定义特征(Custom Attributes)

 

[

实际上,这里就涉及到 property of the attribute 的区别了。

说实话,我还是分不清这两者的区别的。

因此,如果这两个词碰不到一起时,直接都译成属性;但是,碰到一起又怎么办呢?

第一,区别是肯定有的;

第二,两个词所要表达的内容,可能既有时间上差异,也有空间上的差异,即,既有一个变化过程,另外,在不种的语言之间,甩要表达的也不同;

第三,具体地讲,正如本文下面要讲到的,attribute 的本质上是类,用来修饰类型、类型的成员,顶层的值和 do 语句的特征的,且是编译时的特征。因此,译成特征或特性。

property 是类的成员。

 

]

 

自定义特征把信息添加到代码中,将编译成程序集,并随同值和类型一起保存;这些信息能够以编程方面,通过反映(reflection),或者运行时自身读取。

特征可以和类型、类型的成员、以及顶层值相关联,也可以和 do 子句相关联。特征定义,使用中括号([]),特征名放在尖括号(<>)中。例如:

 

[]

 

按约定,特征名都以字符串 Attribute 结尾,因此,特征 Obsolete 的实际名字就是 ObsoleteAttribute。

特征必须直接放在它修饰对象的前面。下面的代码标记函数 functionOne 为 obsolete:

 

open System

[]

let functionOne () = ()

 

从本质上说,特征就是类;使用特征,就是对它的构造函数的调用。在前面的例子中,Obsolete 有一个无参数的构造函数,可以用带括号或不带括号的形式调用它。这里,我们不带括号调用它。如果想传递参数给特征的构造函数,那么,必须用括号,多个参数用逗号隔开。例如:

 

open System

[]

let functionTwo () = ()

 

有时,特征的构造函数并不暴露它的所有属性。如果想设置,就需要指定这个属性,并为它指定值。指定属性名,加等号,加值,放在构造函数的其他参数后面。下面的例子设置特征 PrintingPermission 的 Unrestricted 属性值为真:

 

open System.Drawing.Printing

open System.Security.Permissions

 

[]

let functionThree () = ()

 

可以有多个特征,之间用分号隔开:

 

open System

open System.Drawing.Printing

open System.Security.Permissions

 

[]

let functionFive () = ()

 

到此,我们还只是用了只有值的特征,但是,有类型或类型成员的特征一样简单。下面的例子标记一个类型和它所有的成员都是 obsolete。

 

open System

 

[]

type OOThing = class

  []

  val stringThing : string

  []

  new() = {stringThing = ""}

  []

  member x.GetTheString () = x.string_thing

end

 

如果打算在自己的程序中使用 WinForms 或 Windows Presentation Foundation (WPF)图表,就必须确保该程序是单线程(single-thread apartment)。这是因为,在提供图表组件的库函数表面之下,使用了非托管(unmanaged,不是由 CLR 编译的)代码;最容易的方法是用 STAThread 特征进行修饰;必须修饰最后传递给编译器文件的第一个 do 语句,即,程序运行时,第一个执行的语句。看下面的例子:

 

open System

open System.Windows.Forms

 

let form = new Form()

 

[]

do Application.Run(form)

 

一旦特征附加到类型和值以后,就可以使用反映去找到那些用特征标记的值和类型。通常用System.Reflection.MemberInfo 类的 IsDefined 或 GetCustomAttributes 方法,就是说,这些方法在大多数用于反映的对象上是可用的,包括 System.Type。下面的例子就是查找所有标记有 Obsolete 特征的类型:

 

open System

 

// create a list of all obsolete types

let obsolete =

  AppDomain.CurrentDomain.GetAssemblies()

    |>List.ofArray

    |>List.map (fun assm -> assm.GetTypes())

    |>Array.concat

    |>List.ofArray

    |>List.filter (fun m ->

       m.IsDefined(typeof, true))

 

// print the lists

printfn "%A" obsolete

 

结果如下:

 

[System.ContextMarshalException;System.Collections.IHashCodeProvider;

System.Collections.CaseInsensitiveHashCodeProvider;

System.Runtime.InteropServices.IDispatchImplType;

System.Runtime.InteropServices.IDispatchImplAttribute;

System.Runtime.InteropServices.SetWin32ContextInIDispatchAttribute;

System.Runtime.InteropServices.BIND_OPTS;

System.Runtime.InteropServices.UCOMIBindCtx;

System.Runtime.InteropServices.UCOMIConnectionPointContainer;

...

 

我们已经知道怎样使用特征和反映去检查代码了。现在,让我们看一个类似的但更强大的技术,去分析编译的代码,叫做引用()。

 

 

引用代码(Quoted Code)

 

引用(quotation),它告诉编译器“不要为这一段源文件产生代码,而是把它转换成数据结构,表达式树(expression tree)。”然后,这个表达式树可以有多种方式进行解释、转换、优化,编译成其他的语言,或者干脆忽略。

要引用表达式,把它放在 <@ @> 运算符之间:

 

// quote the integer one

let quotedInt = <@ 1 @>

 

// print the quoted integer

printfn "%A" quotedInt

 

前面代码的运行结果如下:

 

Value (1)

 

下面的代码定义一个标识符,并用于引用:

 

// define an identifier n

let n = 1

// quote the identifier

let quotedId = <@ n @>

 

// print the quoted identifier

printfn "%A" quotedId

 

前面代码的运行结果如下:

 

PropGet (None, Int32 n, [])

 

下面,我们可以引用对值的函数应用。注意,引用两项,这个引用的结果分成两部分,第一部分表示函数,第二部分表示被应用的值:

 

// define a function

let inc x = x + 1

// quote the function applied to a value

let quotedFun = <@ inc 1 @>

 

// print the quotation

printfn "%A" quotedFun

 

前面代码的运行结果如下:

 

Call (None, Int32 inc(Int32), [Value (1)])

 

下面的示例演示了如何把运算符应用到两值。注意,返回的表达式与函数调用的很相似,这是因为运算符本质也就是函数调用:

 

open Microsoft.FSharp.Quotations

 

// quote an operator applied to twooperands

let quotedOp = <@ 1 + 1 @>

 

// print the quotation

printfn "%A" quotedOp

 

前面代码的运行结果如下:

 

Call (None, Int32op_Addition[Int32,Int32,Int32](Int32, Int32),

  [Value(1), Value (1)])

 

下面的示例引用了一个匿名函数,需要注意的是,现在的结果是 Lambda 表达式:

 

open Microsoft.FSharp.Quotations

 

// quote an anonymous function

let quotedAnonFun = <@ fun x -> x + 1@>

 

// print the quotation

printfn "%A" quotedAnonFun

 

前面代码的运行结果如下:

 

Lambda (x,

  Call(None, Int32 op_Addition[Int32,Int32,Int32](Int32, Int32),

    [x,Value (1)]))

 

引用与 Microsoft.FSharp.Quotations.Expr 的差别联合(discriminating union)很相似,处理引用也很简单,就是对它进行模式匹配。下面的示例定义一个函数 interpretInt,检查传递给它的表达式,看是否是一个整数;如果是,就输出这个整数的值;否则,输出“not an int”:

 

open Microsoft.FSharp.Quotations.Patterns

 

// a function to interpret very simplequotations

let interpretInt exp =

  matchexp with

  |Value (x, typ) when typ = typeof -> printfn "%d" (x:?> int)

  | _-> printfn "not an int"

 

// test the function

interpretInt <@ 1 @>

interpretInt <@ 1 + 1 @>

 

运行的结果如下:

 

1

not an int

 

interpretInt 输出了两个表达式,第一个是整数值,因此,输出的是这个整数的值;第二个不是整数,虽然它包含一个整数;像这样对引用的模式匹配有点冗长,因此,F# 的库函数定义了大量的活动模式来帮助完成匹配工作;这些活动模式定义在命名空间 Microsoft.FSharp.Quotations.DerivedPatterns下。下面的示例演示如何使用活动模式 SpecificCall 去识别对加法的调用:

 

open Microsoft.FSharp.Quotations.Patterns

openMicrosoft.FSharp.Quotations.DerivedPatterns

 

// a function to interpret very simplequotations

let rec interpret exp =

  matchexp with

  |Value (x, typ) when typ = typeof -> printfn "%d" (x:?> int)

  |SpecificCall <@ (+) @> (_, _, [l;r]) -> interpret l

                                            printfn "+"

                                            interpretr

  | _-> printfn "not supported"

 

// test the function

interpret <@ 1 @>

interpret <@ 1 + 1 @>

 

运行的结果如下:

 

1

1

+

1

 

注意,使用活动模式 SpecificCall,除了可以识别运算符以个,还可以识别函数调用。

 

现在,还不存这样的库函数,能把引用编译回 F#,并执行,虽然这个功能可能会出现在未来的版本中;相反,你可以把任意的顶层函数,用特征ReflectedDefinition 进行标记。这个特征会告诉编译器,除了生成表达式树以外,还要生成函数或值;然后,可以使用<@@ @@> 运算符取回这个引用,这与引用运算符(&&)相似。下面的示例演示了特征ReflectedDefinition 的用法,注意如何使用对 inc 的引用,也可以直接使用函数 inc:

 

// this defines a function and quotes it

[]

let inc n = n + 1

 

// fetch the quoted defintion

let incQuote = <@@ inc @@>

 

// print the quotation

printfn "%A" incQuote

// use the function

printfn "inc 1: %i" (inc 1)

 

代码的运行结果如下:

 

Lambda (n@5, Call (None, Int32 inc(Int32),[n@5]))

inc 1: 2

 

这个示例似乎功能有限,但是,这项技术的潜力巨大,它可以使你在调用函数之前,对其进行运行时分析;也可以广泛用于 F# 的网站工具包(http://www.codeplex.com/fswebtools),由Tomas Petricek(F# 的大拿)开发。

引用是一个非常大的主题,不可能在这一节,甚至是这本书,把它完整说透。然而,我们会在第十一章再学习有关内容。

 

 

第六章 小结

 

在这一章,我们学习了如何在 F# 中组织代码,注释(comment),注解(annotate),引用代码,但仅接触注释、引用的表面。

至此,结束 F# 语言核心的旅程。本书的余下部分将集中关注如何使用 F#,从使用关系型数据库到创建用户界面。下一章将讨论 F# 的核心库。

 


你可能感兴趣的:(F#基础,F#,函数编程)