F#学习之路(3) 如何组织程序(下)
二、名称空间(namespace)
名称空间,将一组逻辑上相关的类型、模块放在一起,主要是为了解决名称冲突的问题,同时也便于更好的理解程序结构。F#的名称空间概念及定义与C#基本相似。
1、定义名称空间
在F#中定义名称空间,使用关键字namespace。
namespace FSharpLearning
名称空间声明的位置应在源文件的开头,除注释、部分指令外,namespace声明前不能有语句或表达式。
名称空间的作用域为定义开始到文件结束,除非遇到下一个名称空间的定义。在F#中一个源代码文件中可以定义多个名称空间。名称空间没有嵌套的概念,对F#来说,名称空间只是为了给名称空间下的元素定义一个完全限定名称而已。
C#可以在一个名称空间中嵌套定义名称空间,而F#不支持这种定义方式。
namespace N1
{
namespace N2
{
public class Test{}
}
}
这在c#中定义了一个N1.N2的名称空间。
不过F#支持使用.号分隔(与C#一致),表示一种逻辑上的层次关系,值得说明的是,对于编译器来说名称空间只是为了生成长名称(long name)而已,并没有真正的层次关系。
namespace FSharpLearning.StringExtensions
名称空间的作用域中只能定义类型、模块、异常(使用exception关键字定义的特殊联合类型,编译器会生成继承于System.Exception的类),不能定义值,所以在名称空间中不能出现let绑定,表达式。上一篇博客中讲到了在模块中是可以定义let绑定,函数,表达式。
名称空间可以使用open关键字打开。在F#中,open的使用与c#的using也有不同。在c#中using语句后跟的名称空间必须是全名称,但在F#中不仅支持这种写法,还有另一种使用方法。
open FSharpLearning
open StringExtensions(*open FSharpLearning.StringExtensions*)
在F#中虽然没有嵌套定义名称空间的语法,但却能使用open打开逻辑上嵌套的名称空间。
现在我来回答上一篇博客中留下来的问题。
由于使用了open关键字,使用未限定名访问,所以出现了同名的标识,F#会以最后一个open打开的名称空间及模块的名称,同名的会隐藏。后者隐藏前者。你当然可以使用完全限定名来访问隐藏的名称,其次你也可能使用别名的方式访问。
module sw=System.Windows.Forms
module se=FSharpLearning.StringExtensions
open se
let s="Test".Link
sw.MessageBox.Show("good")|>ignore
2、定义名称空间下的子模块
上一篇博客中只讲到了顶级模块的定义,未讲到子模块的定义。F#子模块的定义仍然使用module。
namespace FSharpLearning
module DateTimeExtensions =
open Math
type System.DateTime with
member x.ToNumber =
BigNum.of_bigint << BigInt.FromInt64 <| x.Ticks
你注意到不同了吗?子模块的定义于顶级模块不同在于,子模块的名称后面接着一个等号(=)。
module submodulename =begin
(*模块中可以定义子模块、类型、函数、值*)
end
begin end在#light指令下可以省略。
子模块与顶级模块从概念上来讲,子模块是指嵌套在模块之下的模块。顶级模块在一个名称空间下只可以定义一个,并且必须是名称空间下第一条语句。
子模块却可以定义多个。一个名称空间下可以定义多个子模块。
3、名称空间、模块之间的区别
名称空间主要用于组织逻辑上相关的类型。而模块主要用于定义对一个类型相关的行为。
名称空间就是为一组相关的类型定义一个完全限定名称,一是从逻辑上表示相关,二是为了解决名称冲突
模块则是为了模块化代码。在函数式语言中,通常把对某一个类型的操作放入到一个模块中,主要为了表示逻辑上依附于某一个类型,也部分解决名称冲突
从语义上来讲名称空间与模块没有什么不同,都是为了解决名称冲突与模块化编程而引入的语法。历史上来看,名称空间是后引入的,模块早于名称空间出现。
三、再议F#如何编译及确定入口点
上一篇博客讲过F#编译器依据传递给他的源代码文件名顺序依次编译、连接,以及编译为可执行文件时,以传递给他的最后一个文件名为入口点。
然后知道这一些仍然不够,很模糊,也并不完全正确。下面我用C#概念来描述一下规则
F#在编译的过程中,与源代码文件有很大关系。根据每一个源代码文件,F#会生成一个程序集访问级别的静态类(internal static),为便于描述,我把这个静态类称为内部静态类。每一个模块生成一个静态类。为了初始化这些静态类,F#会为每个静态类生成一个静态构造函数。内部静态类会初始化整个源代码文件中所有的静态成员。换句话如果一个源代码文件中有多个模块,那么这些模块都会被初始化。
F#以传递他的最后一个文件名作为程序入口点,会生成一个_main的方法。这是生成可执行文件默认的规则。显然生成dll,就没有入口点的说法,也就不会生成这个_main方法了。
不过F#还允许你指定入口方法。
[<EntryPoint>]
let test (a:string[]) =
printfn "Hello world"
System.Console.ReadKey(true) |>ignore
使用EntryPointAttribute属性指定一个方法为程序入口点,这种定义方法与C#很相似。不过,这个函数必须位于你传递给编译器最后一个文件中,且必须位于文件的最后。
对于可执行文件来说,程序执行时,入口点所在的源代码文件(编译器为我们生成的内部静态类)会首先加载,如果这个内部静态类引用了其他源代码文件,那么就会加载那个引用的源代码文件的内部静态类,如果引用的是类型,会加载相关类型,如果是模块,则会执行模块的静态构造函数,这种方式称为按需加载原则。对于dll,由于没有入口点,所以程序集加载时不会强制执行静态初始化,只会发生在调用时,当引用某个类型或模块时,相应的类型或模块所在的源代码文件的内部静态类静态构造器会被执行。
四、总结:
1、名称空间、模块的命名应以大写字母开头。
2、对于模块不要使用open,但使用可选类型扩展的模块除外,可选类型扩展的模块应使用类型名加上Extensions。
名称空间下主要包含类型及模块,类型和模块都有很好的封装能力,因此大多数情况下不会发生名称冲突的问题。而使用open打开了模块,就增大了名称冲突,并且也会为阅读代码造成影响,前面说过模块主要是为封装某一类型的行为或某一组逻辑上相同的行为,从某种意义上说,是模拟OO中的类的职责。F#库的List,Map,Seq这些模块如果使用open打开,就会造成阅读上的困难,也会造成名称隐藏。
但可选类型扩展则可以使用open打开。
type System.DateTime with
member x.ToNumber =
BigNum.of_bigint << BigInt.FromInt64 <| x.Ticks
可选类型扩展,与C#3.0的扩展方法相似,可以扩展某一类型的功能。可选类型扩展,必须定义在模块中,且扩展的类型名必须使用完全限定名称。
3、源代码文件名要使用字母开头。
4、不要轻易使用open的未限定名称方法,这很难看出完整的名称空间。
5、顶级模块不与命名空间一同使用,命名空间与子模块一起使用。
6、不要把不相关的模块放到一个源代码文件中,即使是同一个名称空间下的一组相关的模块
F#在生成内部静态类时是以源代码文件为单位的,不管调用了哪个模块中的值、类型、函数,任何一个在这个源代码文件中定义的模块,都会被静态初始化。