有效的编程。第1部分:迭代器和生成器

根据许多站点的版本(例如Github),Java语言是当前最流行的编程语言。同时,它是最先进还是最喜欢的语言?它缺少其他语言不可或缺的结构:扩展的标准库,不变性,宏。但是,我认为其中有一个细节没有引起足够的重视-发电机。



此外,向读者提供了一篇文章,在积极回应的情况下,该文章可以发展为一个周期。如果我成功地编写了这个循环,并且读者已经成功地掌握了它,那么下面的代码不仅可以清楚地知道它的作用,而且可以知道它的工作原理:



while (true) {
    const data = yield getNextChunk(); //   
    const processed = processData(data);
    try {
        yield sendProcessedData(processed);
        showOkResult();
    } catch (err) {
        showError();
    }
}


这是第一个试验部分:迭代器和生成器。



迭代器



因此,迭代器是一个提供对数据的顺序访问接口



如您所见,该定义没有说明数据或内存结构。实际上,未定义的序列可以表示为迭代器,而不会占用存储空间。



我建议读者回答这个问题:数组是迭代器吗?



回答
. shift pop .



那么,如果数组(一种语言的基本结构)允许您按顺序并以任意顺序使用数据,那么为什么需要迭代器呢?



假设我们需要一个实现一系列自然数的迭代器。或斐波那契数。或任何其他无尽的序列。很难在一个数组中放置一个无穷无尽的序列;您需要一种机制来逐渐用数据填充数组,以及删除旧数据以免填充整个进程内存。这是不必要的复杂性,尽管没有数组的解决方案可以分为几行,但它带来了实现和支持的额外复杂性:



const getNaturalRow = () => {
    let current = 0;
    return () => ++current;
};


同样,迭代器可以表示从外部通道(例如Websocket)接收数据。



在javascript中,迭代器是具有next()方法的任何对象,该方法返回具有以下字段的结构:值value-迭代器的当前值并完成-指示序列结束的标志(此约定在ECMAScript语言标准中进行了描述)。这样的对象实现了Iterator接口。让我们以这种格式重写前面的示例:



const getNaturalRow = () => ({
    _current: 0,
    next() { return {
        value: ++this._current,
        done: false,
    }},
});


Javascript还具有一个Iterable接口,该接口是一个具有@@迭代器方法(此常量可作为Symbol.iterator使用)的对象,该方法返回一个迭代器。对于实现此类接口的对象,可以使用运算符遍历for..of让我们再重写一次示例,仅这次是作为Iterable实现:



const naturalRowIterator = {
    [Symbol.iterator]: () => ({
        _current: 0,
        next() { return {
            value: ++this._current,
            done: this._current > 3,
       }},
   }),
}

for (num of naturalRowIterator) {
    console.log(num);
}
// : 1, 2, 3


如您所见,我们必须使完成标志在某个时候变为正数,否则循环将是无限的。



发电机



生成器成为迭代器发展的下一阶段。他们提供语法糖来返回迭代器值,就像函数值一样。生成器是一个返回迭代器的函数(用星号:function *声明)。在这种情况下,没有显式返回迭代器,函数仅使用yield语句返回迭代器的值当函数完成执行时,迭代器被视为完成(对下一个方法的后续调用的结果的完成标志将等于true)



function* naturalRowGenerator() {
    let current = 1;
    while (current <= 3) {
        yield current;
        current++;
    }
}

for (num of naturalRowGenerator()) {
    console.log(num);
}
// : 1, 2, 3


在这个简单的示例中,生成器的主要细微差别已经可以肉眼看到:生成器函数内部的代码不是同步执行的生成器代码是分阶段执行的,这是在相应迭代器上调用next()的结果。让我们看看在前面的示例中如何执行生成器代码。我们将使用特殊的光标标记生成器停止的位置。



调用naturalRowGenerator时,将创建一个迭代器。



function* naturalRowGenerator() {let current = 1;
    while (current <= 3) {
        yield current;
        current++;
    }
}


此外,当我们前三次调用next方法时,或者在本例中,我们遍历循环,则光标位于yield语句之后。



function* naturalRowGenerator() {
    let current = 1;
    while (current <= 3) {
        yield current; ▷
        current++;
    }
}


对于随后所有对next的调用以及退出循环后,生成器将结束其执行,并且调用next的结果将是 { value: undefined, done: true }



将参数传递给迭代器



想象一下,我们需要增加重置当前计数器并从头开始计数到自然数迭代器的功能。



naturalRowIterator.next() // 1
naturalRowIterator.next() // 2
naturalRowIterator.next(true) // 1
naturalRowIterator.next() // 2


很明显,如何在自写的迭代器中处理此类参数,但是生成器呢?

事实证明,生成器支持参数传递!



function* naturalRowGenerator() {
    let current = 1;
    while (true) {
        const reset = yield current;
        if (reset) {
          current = 1;
        } else {
          current++;
        }
    }
}


由于yield语句,使传递的参数可用。让我们尝试使用游标方法来增加清晰度。创建迭代器后,没有任何变化。接下来是对next()方法的第一次调用:



function* naturalRowGenerator() {
    let current = 1;
    while (true) {
        const reset = ▷yield current;
        if (reset) {
          current = 1;
        } else {
          current++;
        }
    }
}


从yield语句返回时,光标冻结。在下次调用next时,传递给函数的值将设置reset变量的值。由于尚未调用yield,传递给对next的第一次调用的值在哪里结束?无处!它会溶解在巨大的垃圾收集器中。如果您需要将一些初始值传递给生成器,则可以使用生成器本身的参数来完成。例:



function* naturalRowGenerator(start = 1) {
    let current = start;
    while (true) {
        const reset = yield current;
        if (reset) {
          current = start;
        } else {
          current++;
        }
    }
}

const iterator = naturalRowGenerator(10);
iterator.next() // 10
iterator.next() // 11
iterator.next(true) // 10


结论



我们已经讨论了迭代器的概念及其在javascript语言中的实现。我们还研究了生成器-用于方便实现迭代器的语法结构。



尽管我在本文中提供了带有数字序列的示例,但是JavaScript迭代器可以做更多的事情。它们可以表示任何数据序列,甚至可以表示许多有限状态机。在下一篇文章中,我想谈谈如何使用生成器来构建异步进程(协程,goroutine,csp等)。



All Articles