Wednesday, August 23, 2006

什么是闭包

有很多人问我,“闭包是什么意思”。是否可以通过实际的开发语言举一个具体的例子,闭包到底意味着什么?
如果你没使用过闭包进行编程,或者你在过去的10年里对Java接触的较少的话,闭包的含义确实较难理解。下面让我从一个实际的例子开始,逐步的向你介绍什么是闭包。

问题的提出

假设你现在要开发一个这样的应用程序,它需要维护保存在一个列表中的文档,每一个使用这个程序的用户需要对这些文档添加注释。为了讲解闭包的原理,在这个例子中假设有两组互不相干的文档列表。这时,如果你从一个用户那里获得一组文档的注释,需要从上面的两组文档列表中获得相应的文档注释内容:

class Document { ... }
class DocAnnotation { ... }
class Person { ... }
class Documents {
    static List allDocuments();
}
class Persons {
    static Set allPersons();
    static Person GEORGE_W_BUSH = ...;
}
class DocAnnotations {
    static Map> allAnnotations = ...;
}

现在,针对上面的程序你可能会问诸如:乔治.布什在这些文档中是否提到有关伊拉克的机密问题。为了实现这个需求,在这个假想的程序中我们可以这样作:

boolean bushMarkedAnyIraqDocsSecret() {
    Iterator docI = Documents.allDocuments().iterator();
    Iterator annI =
        DocAnnotations.allAnnotations.get(GEORGE_W_BUSH).iterator();
    while (docI.hasNext() && annI.hasNext()) {
        Document doc = docI.next();
        DocAnnotation docAnn = annI.next();
        if (doc.mentions("Iraq") && docAnn.marked("secret")) {
            return true;
        }
    }
    return false;
}

我们可以将上面的需求进一步抽象为,我们要找的文档是什么,我们要找的文档注释是谁写的以及我们要找的文档注释属于哪种类型,抽象后的方法如下:

boolean personAnnotatedOnKeyword(Person person, String ann, String key) {
    Iterator docI = Documents.allDocuments().iterator();
    Iterator annI =
        DocAnnotations.allAnnotations.get(person).iterator();
    while (docI.hasNext() && annI.hasNext()) {
        Document doc = docI.next();
        DocAnnotation docAnn = annI.next();
        if (doc.mentions(key) && docAnn.marked(ann)) {
            return true;
        }
    }
    return false;
}
通过上面的抽象,我们已经尽可能的避免了代码的重复,如果再提出类似的问题,只要变化一下参数就可以了。对方法或类的抽象是非常必要的,因为这可以在我们需要重构的时候尽量减少代码的修改。比如,如果我们需要把一个人的注释表现形式改成Map而不是List,又或者希望DocAnnotation包含一个Document的引用,那么我们就必须修改上面方法中的循环。所以如果将这段循环放在一个单独的地方那么维护起来就方便的多了。

JDK5中对循环进行抽象
下一步要作的是对循环本身进行抽象。Java中提供了很多方便的循环结构,比如最近提供的包含Iterable 接口的for-each循环。在这里可以使用它,但是我们需要介绍在上面的例子中我们需要进行迭代循环的是什么类型:

class DocAndAnnotation {
    final Document doc;
    final DocAnnotation docAnn;
    DocAndAnnotation(Document doc, DocAnnotation docAnn) {
        this.doc = doc;
        this.docAnn = docAnn;
    }
}


现在我们可以提供一个只进行一次循环就包含了文档和注释的集合。


Collection docsWithAnnotations(Person person) {
    Iterator docI = Documents.allDocuments().iterator();
    Iterator annI =
        DocAnnotations.allAnnotations.get(person).iterator();
    List result =
        new ArrayList();
    while (docI.hasNext() && annI.hasNext()) {
        Document doc = docI.next();
        DocAnnotation docAnn = annI.next();
        result.add(new DocAndAnnotation(doc, docAnn));
    }
    return result;
}


这样我们就可以写一个更特别的循环,就像下面这样:


boolean personAnnotatedOnKeyword(Person person, String ann, String key) {
    for (DocAndAnnotation docAndAnn : docsWithAnnotations(person)) {
        if (docWithAnn.doc.mentions(key) && docWithAnn.docAnn.marked(ann)) {
            return true;
        }
    }
    return false;
}
目前为止,上面的方法存在一个问题:每次它都会把全部的文档和注释装入列表集合中,
尽管可能我们的答案在第一个注释文档中就可以找到。我们可以作一些改进,那就是创建一个
“懒加载”的迭代器取代上面的集合。我们重写了docsWithAnnotations如下:
Iterable docsWithAnnotations(Person person) {
final Iterator docI =
Documents.allDocuments().iterator();
final Iterator annI =

DocAnnotations.allAnnotations.get(person).iterator();

return new Iterable() {

public Iterator iterator() {

return new Iterator() {

public boolean hasNext() {

return docI.hasNext() && annI.hasNext();

}

public DocAndAnnotation next() {

return new DocAndAnnotation(docI.next(), annI.next());

}
public void remove() {
throw new UnsupportedOperationException();

}

};

}

};
}
现在你不需要修改personAnnotatedOnKeyword它同样可以运行。
但是新的问题又出现了,上面的方法产生了一大堆小的、临时的垃圾对象。
Iterator, Iterable, DocAndAnnotation对象的存在只是临时性的,
它们的作用就是简单的把数据从一个地方搬到另一个地方。
这是不是什么大问题。
HotSpot有很多垃圾回收机制,
它们可以很好的将这些临时对象进行重新整理和释放。但是在有些应用中它们可能是性能的瓶颈。


有没有好的循环方式可以避免这一问题呢?答案是肯定的,Java有一个标准的习惯用法用于处理这一问题。方法就是控制循环的内部回路。较之在personAnnotatedOnKeyword方法中执行循环更好的方式是,我们提供一个接口,由接口中的方法执行循环并把值传给由personAnnotatedOnKeyword提供哦的一段代码。就像这样:

interface WithDocumentAndAnnotation {
void doIt(Document doc, DocAnnotation docAnn);
}void docsWithAnnotations(
Person person, WithDocumentAndAnnotation body)
{

Iterator docI = Documents.allDocuments().iterator();

Iterator annI =

DocAnnotations.allAnnotations.get(person).iterator();

while (docI.hasNext() && annI.hasNext()) {

Document doc = docI.next();

DocAnnotation docAnn = annI.next();

body.doIt(doc, docAnn);

}

}


客户端可以通过提供一个实现这个接口内部类来执行循环:

boolean personAnnotatedOnKeyword(
        Person person, final String ann, final String key) {
    class MyBody implements WithDocumentAndAnnotation {
        boolean result = false;
        public void doIt(Document doc, DocAnnotation docAnn) {
            if (doc.mentions(key) && docAnn.marked(ann)) {
                result = true;
            }
        }
    }
    MyBody body = new MyBody();
    docsWithAnnotations(person, body);
    return body.result;
}

这种方法解决了临时对象内存分配的问题(尽管它还存在问题),但是这种方法还是没有解决我们开始提出的问题,那就是不管是否我们需要找的文档就在第一个文档中它还是会装入所有的文法。我们可以通过修改WithDocumentAndAnnotation接口的方法,让它返回一个boolean值,用来判读循环是否需要继续执行。这就需要修改客户段程序中docsWithAnnotations的很多部分来适应新的API,然而有时这是不可行的。看来我们应该修改接口的功能,但是我们发现客户端使用不同的控制流程来迭代整个文档和它们的注释。现在,我们向读者描述了上面练习的详细内容。

通过闭包抽象上面的循环
闭包提供了更便利的方法来抽象上面的循环:

void docsWithAnnotations(Person person, void(Document,DocAnnotation) block) {
    Iterator docI = Documents.allDocuments().iterator();
    Iterator annI =
        DocAnnotations.allAnnotations.get(person).iterator();
    while (docI.hasNext() && annI.hasNext()) {
        Document doc = docI.next();
        DocAnnotation docAnn = annI.next();
        block(doc, docAnn);
    }
}

上面的方法中除了没有WithDocumentAndAnnotation接口之外和我们最后一个JDK5版本的例子非常类似。
让我们来看看客户端是什么样子:
boolean personAnnotatedOnKeyword(
Person person, final String ann, final String key) {

docsWithAnnotations(person, (Document doc, DocAnnotation docAnn) {

if (doc.mentions(key) && docAnn.marked(ann)) {

return personAnnotatedOnKeyword: true;
}
});

return false;
}


就这样,如果我们采纳功能修改建议的话,客户端还可以是这样:

boolean personAnnotatedOnKeyword(
Person person, final String ann, final String key) {


docsWithAnnotations(person) (
Document doc, DocAnnotation docAnn) {

if (doc.mentions(key) && docAnn.marked(ann)) {

return true;

}

}

return false;

}
上面代码中仅有的临时对象是闭包对象和两个用于实现docsWithAnnotations迭代器对象。如你所见,docsWithAnnotations的调用者可以使用控制层的操作,比如docsWithAnnotations的实现必须首先确定哪一个类型的控制层是必须的,否则什么都不返回。
这样,只有很少的临时对象需要处理。因此HotSpot虚拟机只需要处理很少的异常,避免了内存的开销。
上面几个版本的迭代向我们展示了闭包的优美之处:它可以让你抛开繁复的代码写出更好的抽象。




1 comment:

一切空白 said...

楼主很努力,但讲的太麻烦了,没看明白