9.3.2 使用 IDisposable 接口清理资源
我们已经使用过几种实现了 IDisposable 的类型,比如, Graphics 和 SolidBrush。我们想使代码尽可能易于理解,所以,当我们使用完对象,就显式调用 Dispose 方法。
C# 中包含了这个语法糖,以 using 语句的形式,即使语句体内抛出异常,也确保调用 Dispose。F# 有一个相似的结构,用 use 关键字。清单 9.11 显示了一个简单的处理文件的示例。
Listing 9.11 Working with files and the use keyword (F# Interactive)
> open System.IO;;
> let readFile() =
use reader = new StreamReader("C:\\test.txt")
let text = reader.ReadToEnd()
Console.Write(text)
;;
val readFile : unit �C> unit
> readFile();;
Hello world!
Ahoj svete!
当创建 StreamReader(实现了 IDisposable 接口)时,我们使用 use 关键字声明它。注意,这个语法类似于在通常的 let 中,使用 let 关键字。所不同的是,F# 编译器会自动在这个函数的结尾,添加对 Dispose 方法的调用,所以,在我们处理完 StreamReader 之后,它会自动释放。编译器还插入一个 try �C finally 块,以确保清理运行,即使在发生异常时。
在 C# 中,using 结构和 F# 中的 use 关键字之间的重要区别,是在 C# 中,我们必须明确地使用花括号指定 using 的范围,而在 F# 中,Dispose 方法是在这个值可见的范围结束时被调用。这就是我们通常需要做的,所以,它使很多代码片断很容易写了。清单 9.12 展示了 C# 和 F# 的版本。
Listing 9.12 Cleaning up resources in F# and C#
// F# version
let test() =
use reader = new StreamReader("C:\\test.txt")
let text = reader.ReadToEnd()
Console.Write(text)
// C# version
void Test() {
using(var reader = new StreamReader("C:\\test.txt")) {
var text = reader.ReadToEnd();
Console.Write(text);
}
}
在两种语言中,当执行离开 reader 值可以访问的范围时,对象被释放。在 F# 中,默认情况下,这种情况发生在函数结束,这通常就是我们所需要的的。当该函数的代码能够连续运行很长时间时,它更好地保证了资源尽早释放。让我们说,我们想关闭该文件,关闭后,打印内容到控制台。在 C# 中,我们就必须在函数内部创建一个局部变量,并在 using 块内给它赋值。在 F# 中,可以更容易做到,因为,我们可以使用缩进显式指定范围:
let test() =
let text =
use reader = new StreamReader("C:\\test.txt")
reader.ReadToEnd()
Console.Write(text)
这个语法可能有些出人意料,但是,一旦我们理解了,它就变得清晰了,在 F# 中,每一个代码块就是一个表达式。在前面的代码中,我们在这个表达式构造中指定的方式,与我们写 (1+2)*3,而不是默认的1+(2*3) 的方式 是相同的。这样,我们可以限制值 reader 的范围到初始化值 text 表达式。
虽然 use 关键字开始时主要用于处理持有一些资源 .NET 对象,但是,它能够用于更广泛的情景。让我们来看一个例子。
用关键字 use 编程
正如我们已经看到的,如果我们用关键字 use 创建了一个值,编译器会自动插入对 Dispose 方法的调用,在声明这个值的函数的结尾。这对于资源来说,是有用的,但也有其他情况,我们需要两个函数调用之间加上一段代码。
假设我们希望以不同的颜色输出文本到控制台,然后,再恢复原来的颜色。传统上,我们必须保存原来的颜色,设置新的,将输出发送到控制台,并恢复原来的颜色。
使用 use 关键字,可以做同样的事情,但更优雅。我们可以写一个函数,改变控制台的颜色,并返回一个 IDisposable 值。此值包含 Dispose 方法,调用它恢复原来的颜色,由于 use 关键字,该方法将被自动调用。清单 9.13 显示了这个函数和对它的使用的演示。
Listing 9.13 Setting console color using IDisposable (F# Interactive)
> open System;;
> let changeColor(clr) =
let orig = Console.ForegroundColor
Console.ForegroundColor <- clr
{ new IDisposable with
member x.Dispose() =
Console.ForegroundColor <- orig };;
val changeColor : ConsoleColor �C> IDisposable
> let hello() =
use clr = changeColor(ConsoleColor.Red)
Console.WriteLine("Hello world!")
;;
val hello : unit �C> unit
最有趣的代码是 changeColor 函数。我们可以想像一下,它包含两段代码。第一部分在调用该函数时立即执行,第二部分被返回,并在以后执行。代码的第一部分,首先保存原来的颜色,然后,设置新的。
第二部分需要作为结果返回。我们把它作为一个函数返回(可能使用 lambda 函数语法),但随后,调用者将必须显式调用它。相反,我们使用对象表达式创建了一个 IDisposable 值,E.的地方,把恢复原来颜色的代码放在 Dispose 方法中。
当 changeColor 函数使用时,第一部分代码(设置新的颜色)立即执行。我们使用 use 关键字,保存结果,以便在函数结束时,Dispose 方法被调用,并恢复原来的颜色。你可以在图 9.2 中看到,在 F# Interactive 控制台窗口中运行此代码的结果。注意,我们必须使用独立的 F# Interactive 控制台版本,而不是集成在 Visual Studio 窗口的,它不支持改变文本的颜色。
图 9.2 使用 changeColor 函数改变控制台文字的颜色。你可以看到,颜色改变只在 Hello 函数里面,然后,原来颜色就恢复了。
同样的想法是在其他情况下也是非常有用的,比如,临时改变 GUI 中光标,以配合“请等待”的指标,或暂时改变当前的线程的语言,到一个特定的值,当对特定语言代码进行单元测试时。在这里的用是暂时,这意味着,“改变一些东西,做一些工作,恢复原始值“模式,对于 use 关键字最理想!
在所有的面向对象的特性的例子中,我们使用标准的 F# 类型,接口和对象表达式。在以函数方式使用 F# 时,这是很正常的,但是,这个语言还支持其他面向对象的特性。正如这本书主要是有关函数编程的,我们不会讨论所有的内容,但我们将看几个最重要的例子。