语义化版本(Semantic Versioning)有时颇具误导性。虽然F# 4.1向后兼容4.0版,但是它完全不是一个小的版本。F# 4.1预览版自发布以来,得到了来自Microsoft以及更大程度上来自于社区的贡献,因此F# 4.1在性能、互操作性和便利性等方面上新增了一些特性。
F# 4.1发布的重头是使用结构体(structs)的能力。结构体也称为值类型(value type),它并非引用类型(reference type)。结构体能从堆栈上分配值并将嵌入到其它对象中,使用正确时可对性能产生巨大影响。
结构体中首先要介绍的是结构体元组。对于F#和其它函数式编程语言而言,在惯用代码中元组是非常重要的。一个对F#实现的主要批评是“System.Tuple”元组是引用类型的,这意味着每次创建一个元组时,可能需要进行代价昂贵的内存分配。作为不可变对象,这是时常发生的。
通过在.NET中引入ValueTuple类型,这一问题得到了解决。VB和C#也使用这一值类型,当内存具有压力和垃圾回收周期成为问题时,它会改进性能。但在使用中应该慎重,因为重复拷贝16个字节以上的ValueTuples可能会带来其它的性能损失。
在F#中,使用struct标注可以将一个元组定义结构体元组,而非标准元组。该定义所生成的类型与标准元组的工作机制类似,但是两者并不兼容,两者间的转换是一种破坏性更改。例如:
如果出于性能的原因而采用了结构体元组,进行测试是十分重要的。由于元组在F#中广为使用,因此编译器对元组有特殊的优化机制,有时会完全地清除元组。这样的优化机制可能不必用于结构体元组。正如Arbil在原始提案中所写的:“据我们的测试,如果考虑上垃圾回收的代价,短结构体元组的性能可达标准元组的25倍。”
该特性可扩展为一种称为“结构推演”的特性。想想下面的代码:
在F#中,该代码可能会产生编译器错误。这是因为origin是一个结构体元组,表达式(x0,y0)表示一个引用元组。如果能实现结构推演,那么此代码中会隐含地使用struct关键字。
鉴于这是一个编译器错误,为避免对编译器做破坏性更改,该特性可能会在今后的版本中实现。由于它会对语义和编译器产生大量影响,因此并不保证该特性将一定会出现。
另一个F#编程中的重要概念是使用记录类型。记录类型在很多方面上类似于元组,例如都是不可变的,都具有固定的大小。但是两者间的最大差别在于,记录中的每个域都具有不同的名字,而元组则依赖于实际位置区分各个域。
一般说来,软件库开发人员更愿意在公开API中使用记录,而非元组,因为命名的域更易于应用开发人员的理解。
不幸的是,记录面对着和元组同样的问题,即它们通常都是值类型,或者曾经作为值类型使用。F#的贡献者Will Smith(网名TIHan)在创建了结构体记录时,部分参考了结构体元组的工作。
要将一个类型标识为结构体记录,而不是一般情况下的引用类型记录,必须使用[<Struct>]属性。你可能会疑惑为什么不能使用struct关键字。对此网友Dsyme是这样解释的:
警告:F# 4.0并不兼容结构体记录。这是编译器的一个瑕疵,该瑕疵导致编译器将结构体记录看成是一种引用类型,而非值类型。如果你的库有可能被使用旧版本编译器的人调用,就不要使用这个特性。
继续F#结构体这一话题,现在我们看一下结构体差别联合(Struct Discriminated Unions)。差别联合在本质上等价于C++等语言中的联合类型,只是额外具有一些句法上的小技巧。例如,可以使用类似于“case标识符”的形式在差别联合中有效地定义新类型,例如:
在上面的例子中,Shape联合具有三个子类型,即Rectangle、Circle和Prism,它们只存在于Shaple的上下文中。一个指定的Shape实例中,只能包含三个子类型中的一类。
可能你并不熟悉F#的语法,在类型定义中,各个域是通过星号“*”分隔的。因此子类型Rectangle具有两个域,Circle具有一个域,而Prism具有三个域(其中有一个域未命名)。
如果某个“case标识符”具有一个以上的域,就实现为一个元组。这会使我们回想起这一特性的初衷所在。差别联合允许实现为值类型,而不是引用类型。
警告:正如对结构体记录一样,F# 4.0编译器将不能正确地解释结构体差异联合。
C# 7中添加了一个称为“ref locals”的新特性,允许指向值的安全指针。值可以是一个对象内部由ref关键字所指定的参数,在一些情况下也可以指向堆栈上的值。想想如下的简单例子:
实现同样功能的F#代码类似于:
在该特性的公告和RFC中,均称F#已通过“引用单元”(Reference Cells)支持ref locals。虽然这种说法并不正确,但是也可以理解,因为该特性的语法的确类似于C#的ref locals。例如:
但是在查看引用单元的源代码后,事情就变得十分清楚了,该特性实际上只是包装了一个可变值。相关的源代码如下:
因此在上面的例子中,命名为y的变量并未真正地引用了数组a中的元素。y仅是在FSharpRef<int>对象中存储的一个拷贝。如果不是因为“ref locals”的语法与“引用单元”差别不大,则会引发混淆。
F# 4.1突出强调的另一个方面,就是确保F#代码能与其它语言所编写的库进行良好交互。因为.NET已深入挂接到C、COM及一些动态编程语言中,这意味着仅使用C#软件库是不够的。
该特性只对那些需要从F#调用C库的开发人员有用。如果要将一个数据结构传递给C库,并且该C库需要保持该结构,这时你会碰到一些严重的问题。不同于.NET语言,C并不希望背后有垃圾回收器移动内存中的对象。
解决方案是将对象“钉”在内存中,以防止垃圾回收器移动对象。开发人员必须谨慎,不要滥用这一特性,因为它会对内存使用产生消极影响。
在F#中,该功能是使用use关键字和fixed关键字联合实现的。这可能会对一些编程人员造成困惑,因为use关键字非常类似于C#的using关键字,通常用于IDisposable对象上。在这种情况下,use关键字仅提供关联变量的范围,并确保了在该范围之外会解除内存的钉住状态。
在.NET中,Caller信息是使用由CallerFilePath、CallerLineNumber或CallerMemberName属性装饰的可选参数实现的,主要用于日志,也可在其他的场景中看到,例如支持WPF/XAML应用中的属性更改通知。
在F#中,无需特别介绍该特性。根据RFC,F#需要该特性以符合.NET标准,因此必须要实现该特性。
如果简单地将.NET风格的可选参数放入F#中,它并不会正确的工作。理论上,你可以将[<Optional;DefaultParameterValue<(...)>]置于参数上,并获得与VB和C#中同样的可选参数行为。但是F# 4.0及更早的版本并不能正确地编译DefaultParameterValue属性。这意味着该属性在所有语言中被忽略了。
与此相关的问题是,虽然F#可以使用其它库编译后的可选参数和默认参数,但是它不能在同一组装中的代码中使用它们。这一问题只会影响到.NET风格的可选参数,F#风格的可选参数仍按预期工作。
在“RFC FS-1027 Optiona