第六章 组织、注释、引用代码(二)
条件编译(Optional Compilation)
[
Conditional Compilation。可选编译,或编译选项。
]
条件编译,可以让编译器忽略源文件中的不同部分,大多数编程语言都支持某种条件编译。它可能很方便,例如,如果你想让生成(build)的库函数支持 .NET 1.1 和 2.0,并且,想包括额外的值和类型,以利用 2.0 版中的新功能。然而,不应该滥用这一技术,要很慎重,因为,这会使理解与维护代码变得相当困难。
F# 中的条件编译,使用编译器开关 --define FLAG,并在源文件中使用 #if FLAG 指令。
也可以使用 Visual Studio 的生成配置菜单(参见图 6-3)。
注意,使用 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 如下:
this is an explanation
然后,就可以用各种工具来处理这个 XML 文件,比如,Sandcastle (http://www.codeplex.com/Sandcastle),它可以把 XML 文件转换成更可读的格式。我发现,把 Sandcastle 和 Sandcastle 帮助文件生成器(http://www.codeplex.com/SHFB)一起使用会更好。编译器也支持直接从文档注释生成 HTML;虽然,它不如 XML 灵活,但不要额外的努力就能产生更有用的文档;在某些情况下,也可以产生更好的结果,因为,文档生成工具对有些记号的支持不好,比如,泛型和联合类型等。在第十二章将学习有关生成 HTML 的编译器开关。
在 F# 中,不需要显式添加任何 XML 标记,例如,
///
/// divides the given parameter by 10
///
/// the thing to be divided by10
let divTen x = x / 10
这会产生如下的 XML:
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
// 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 "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
|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# 的核心库。