11.3.5 为 C# 实现延迟值
在 11.3.3 节,我们使用函数来表示 C# 中的延迟计算。我们刚才在 F# 中探讨了Lazy<T> 类型,它为计算过的值,添加了缓存功能。从Visual Studio 2010 开始,在核心的 .NET 库下的System.Lazy <T> 就有了这种类型,因此,我们不必自己实现。
清单 11.18 是简化的 Lazy<T> 类。代码在许多方面做了简化,它不是线程安全的,不处理任何异常,只表达了核心概念。
[
清单的序号终于正常了。
]
清单11.18 表示延迟值的类 (C#)
public class Lazy<T> {
readonly Func<T> func;
bool evaluated = false; | 表示缓存的状态
Tvalue; |
public Lazy(Func<T> func) { [1]
this.func = func;
}
public T Value { [2]
get {
if (!evaluated) {
value = func(); | 计算值,修改状态
evaluated = true; |
}
return value;
}
}
}
public class Lazy { <-- 在创建值时,启用类型推断
public static Lazy<T> Create<T>(Func<T> func) {
return new Lazy<T>(func);
}
}
这个类的第一个重要部分是构造函数[1],它的参数为函数,并将其存储在只读字段里面;而这个函数没有任何参数,只在调用时,才计算这个值,所以,我们使用 Func<T> 委托。在非泛型类型中,还有一个静态的方法,让我们创建延迟值时,更容易使用 C# 的类型推断。
延迟值用一个标志来表示值是否已经计算过。注意,我们将使用泛型,因此,使用 null 值来表示不可能很容易,虽然,我们添加了约束,来强制 T 是引用类型,我们需要允许这种可能性,函数可能返回 null 作为计算值。
使用缓存值的大部分代码是在 Value 属性[2]的 getter 中。从用户的角度来看,这是类的第二个重要部分。它首先测试是否已进行计算过函数。如果已经做过,我们就可以使用前面计算过的值;如果还没有,就调用函数,并作标志,保证不会多次计算。
现在,我们看一段简单代码,看看是如何使用这个类型的:
var lazy = Lazy.Create(() => Foo(10));
Console.WriteLine(lazy.Value); // Prints ‘Foo(10)‘ and ‘True‘
Console.WriteLine(lazy.Value); // Prints only ‘True‘
如果尝试此代码,可以看到,其行为与 F# 版本完全相同。当创建延迟值时,我们给它一个函数:这时,Foo 方法不会调用。第一次调用Value,计算函数并调用 Foo;后序再调用 Value ,就使用前面计算过的缓存值,可以看最后一行的打印结果。
到目前为止,我们讨论延迟值的动机是实现或运算符的延迟版本的难点。在下一节,我们将看两个更复杂的实际使用延迟值的示例。