当你采用lazy evaluation(缓式评估)后,采用此方法的类推迟计算工作知道系统需要这些计算结果,如果不需要,将不进行计算。
lazy evaluation广泛用于各种应用领域,比如如下四种情况:
- 引用计数
class String{}; //string类
String s1 = "Hello";
String s2 = s1;
通过string拷贝赋值函数让啥s2被s1初始化,s1和s2都有自己“Hello”的拷贝。这种拷贝赋值函数会引起较大的开销,制作s1的值拷贝,赋值给s2,这通常涉及到new操作符分配堆内存,需要调用strcpy函数拷贝s1的数据到s2。这是eager evaluation。此时s2并不需要拷贝这个值,因为s2还没被使用。
懒惰的做法就是不应该把s1赋值给s2,而是让s1和s2共享一个值。
对于如下的例子没啥子差别,因为它们是读数据:
cout << s1;
cout << s1 + s2;
仅仅当s1和s2的值被改变的时候,共享一个值才造成差异:
s2.convertToUpperCase();
上述语句不能再懒惰了:必须为s2值制作拷贝,如果不修改s2,我们不需要制作拷贝。
lazy evaluation:除非你确实需要,不去为任何东西制作拷贝。我们应该懒惰的,只要可能就共享使用其它值。在一些领域,你经常可以这么做。
- 区别对待读取和写入
如下代码:
String s = "Homer's Iliad"; //一个引用技术的String
...
cout << s[3]; //调用operator[]读取s[3]
s[3] = 'x'; //调用operator[]写入s[3]
上述代码,读取reference-counted string是很容易的,而写入这个string则需要在写入前对该string制作一个新拷贝。
这里有个困难是:我们不能区别出调用operator[]是读还是写。通过lazy evaluation和proxy class,我们可以推迟做出是读还是写的决定,直到我们能判断出正确的答案。
- Lazy Fetching(懒惰提取)
第三个例子:假设你的程序使用了包含许多对象的大型对象。这些对象的生存期超越了程序的运行期,所以它们必须被存储在数据库里,每一个对象都有唯一的标识符,用来从数据库重新获取对象:
class LargeObject
{
public:
LargeObject(ObjectID id); //从磁盘恢复对象
const string& field1() const;
int field2() const;
double field3() const;
double string& field4() const;
double string& field5() const;
...
};
现在从磁盘恢复对象:
void restoreAndProcessObject(ObjectID id)
{
LargeObject object(id);
...
}
因为LargeObject对象实例很大,获取所有的数据,数据库的操作开销非常大,修改成如下情况下,不需要读取所有的数据:
void restoreAndProcessObject(ObjectID id)
{
LargeObject object(id);
if(object.field2() == 0)
cout << "Object" << id << ":null field2.\n";
}
这里仅仅需要field2的值,所以获取其他字段而付出的努力都是浪费。
当LargeObject对象被建立,不需要读取所有的数据,这种懒惰法解决了这个问题,当需要某个数据的时候,这个数据才从数据库读取,这种“demand-paged”(按需分页)对象初始化的的实现方法:
class LargeObject
{
public:
LargeObject(ObjectID id); //从磁盘恢复对象
const string& field1() const;
int field2() const;
double field3() const;
double string& field4() const;
double string& field5() const;
...
private:
ObjectID oid;
mutable string* field1Value;
mutable int* field2Value;
mutable double* field3Value;
mutable string* field4Valuel
...
};
LargeObject::LargeObject(ObjectID id)
:oid(id),field1Value(0),field2Value(0),field3Value(0)...
{}
const string& LargeObject::field1() const
{
if(field1Value == 0)
{
//从数据库中读取field1的值,使field1Value指向这个值
}
return *field1Value;
}
void restoreAndProcessObject(ObjectID id)
{
LargeObject object(id);
if(object.field2() == 0)
cout << "Object" << id << ":null field2.\n";
}
对象中每个字段都用一个指向数据的指针来表示,LargeObject构造函数把每个指针初始化为空。这些空指针表示字段还没从数据库中读取数值。每个LargeObject成员函数在访问字段指针的所指向的数据之前必须检查字段指针的状态。如果为空,在对数据进行操作之前必须从数据库中读取对应的数据。
mutable:表示在任何函数里它们都能被修改,甚至const成员函数。
如果不支持mutable(现在编译器一般都支持)可以通过叫做“fake this”(伪造this指针),你建立一个指向non-const指针,指向的对象与this指针一样,当你想修改成员数据,弄通过“fake this”访问它:
class LargeObject
{
public:
const string& field1() const;
...
private:
string * field1Value;
}
string& LargeObject::field1() const
{
//声明指针,去掉了对象的常属性
LargeObject* const fakeThis =
const_cast<LargeObject* const>(this);
if(field1Value == 0)
{
fakeThis->field1Value = the appropriatedata
from the database;
}
return *field1Value;
}
这个函数使用const_cast去掉了*this的const属性。在LargeObject里的指针,必须把这些指针初始化为空,然后每次测试,这令人厌烦而且容易错误。可以使用智能指针代替,智能指针不再需要使用mutable声明指针。
- Lazy Expression Evaluation(懒惰表达式计算)
有关lazy evaluation的数字程序:
template<class T>
class Matrix{...}; //for homogeneous matrices
Matrix<int> m1(1000,1000); //一个1000*1000的矩阵
Matrix<int> m2(1000,1000);
...
Matrix<int> m3 = m1 + m2;
通过operator 的实现是eagar evaluation:在这种情况下,它会计算和返回m1 与 m2的和。这个计算量非常大(100000次加法运算),当然系统会分配内存来存储这个值。
lazy evaluation方法就是建立一个数据结构来表示m3的值是m1和m2的和,再用一个enum来表示它们之间的加法,这样数据结构要比m1和m2相加快许多,也能节约大脸的内存。
Matrix<int> m4(1000,1000);
Matrix<int> m3 = m4*m1;
此时不需要计算m3的值,因为我们是懒惰的。
- 总结
以上展示了lazy evaluation各个领域的作用:能避免不需要的对象拷贝,通过使用operator[]区分读写,避免不需要的数据库读取操作,避免不需要的数字操作。但是它并不是总是有用的。就像如果你父母总是要来检测你房间,那么拖延战术将不会减少工作量。实际上,如果你的计算都是重要的,lazy evaluation可能减慢速度并增加内存的使用。
如果直接用eager方法来实现一个类,在但是你通过profiler调查显示出类的实现存在一个性能瓶颈,就可以使用lazy evaluation的类来替换它,对于使用者来说索改变的仅是性能的提高,这是使用者喜欢的软件升级方式,它使你完全可以为懒惰而骄傲。