Понимание генераторов TypeScript
Обычные функции выполняются сверху вниз и затем завершаются. Генераторные функции также выполняются сверху вниз, но их можно приостановить во время выполнения и возобновить позже с той же точки. Так продолжается до конца процесса, после чего они завершаются. В этой статье мы узнаем, как использовать функции-генераторы в TypeScript, рассмотрим несколько различных примеров и случаев использования. Давайте начнём!
Создание функции-генератора в TypeScript
Обычные функции выполняются охотно, в то время как генераторы — лениво, то есть их можно попросить выполнить в более поздний момент времени. Чтобы создать функцию-генератор, мы воспользуемся командой function *
. Функции-генераторы выглядят как обычные функции, но ведут себя немного по-другому. Взгляните на следующий пример:
function normalFunction() {
console.log("Это нормальная функция");
}
function* generatorFunction() {
console.log("Это функция-генератор");
}
normalFunction(); // "Это нормальная функция"
generatorFunction();
Хотя он написан и выполняется как обычная функция, при вызове generatorFunction
мы не получаем никаких логов в консоли. Проще говоря, вызов генератора не приводит к выполнению кода:
Вы заметили, что функция генератора возвращает тип Generator
; мы подробно рассмотрим это в следующем разделе. Чтобы заставить генератор выполнить наш код, мы сделаем следующее:
function* generatorFunction() {
console.log("Это функция-генератор");
}
const a = generatorFunction();
a.next();
Обратите внимание, что метод next
возвращает IteratorResult
. Таким образом, если бы мы хотели вернуть число из generatorFunction
, мы бы получили доступ к значению следующим образом:
function* generatorFunction() {
console.log("Это функция-генератор");
return 3;
}
const a = generatorFunction();
const b = a.next();
console.log(b); // {"value": 3, "done": true}
console.log(b.value); // 3
Интерфейс генератора расширяет Iterator
, что позволяет нам вызывать next
. Он также имеет свойство [Symbol.iterator]
, что делает его итератором.
Понимание итераций и итераторов JavaScript
Итерируемые объекты — объекты, которые можно итерировать с помощью for...of
. Они должны реализовывать метод Symbol.iterator
; например, массивы в JavaScript являются встроенными итераторами, поэтому они должны иметь итератор:
const a = [1,2,3,4];
const it: Iterator<number> = a[Symbol.iterator]();
while (true) {
let next = it.next()
if (!next.done) {
console.log(next.value)
} else {
break;
}
}
Итератор позволяет выполнять итерацию по итерируемой таблице. Посмотрите на следующий код, который представляет собой очень простую реализацию итератора:
function naturalNumbers() {
let n = 0;
return {
next: function() {
n += 1;
return {value:n, done:false};
}
};
}
const iterable = naturalNumbers();
iterable.next().value; // 1
iterable.next().value; // 2
iterable.next().value; // 3
iterable.next().value; // 4
Как упоминалось выше, итератор — объект, обладающий свойством Symbol.iterator
. Таким образом, если мы назначим функцию, возвращающую функцию next()
, как в примере выше, наш объект станет JavaScript итерируемым, что позволит нам выполнять итерацию по нему, используя синтаксис for..of
.
Очевидно, что существует сходство между функцией генератора, которую мы рассматривали ранее, и примером выше. Фактически, поскольку генераторы вычисляют по одному значению за раз, мы можем легко использовать генераторы для реализации итераторов.
Работа с генераторами в TypeScript
Самое интересное в генераторах то, что вы можете приостановить выполнение с помощью оператора yield
, чего мы не сделали в предыдущем примере. Когда вызывается next
, генератор выполняет код синхронно, пока не встретится yield
, в этот момент он приостанавливает выполнение. Если next
будет вызван снова, он возобновит выполнение с того места, где оно было приостановлено. Давайте рассмотрим пример:
function* iterator() {
yield 1
yield 2
yield 3
}
for(let x of iterator()) {
console.log(x)
}
yield
, по сути, позволяет нам возвращаться из функции несколько раз. Кроме того, массив никогда не будет создан в памяти, что позволяет нам создавать бесконечные последовательности очень экономно расходуя память. В следующем примере будут созданы бесконечные чётные числа:
function* evenNumbers() {
let n = 0;
while(true) {
yield n += 2;
}
}
const gen = evenNumbers();
console.log(gen.next().value); //2
console.log(gen.next().value); //4
console.log(gen.next().value); //6
console.log(gen.next().value); //8
console.log(gen.next().value); //10
Мы также можем модифицировать приведённый выше пример так, чтобы он принимал параметр и выдавал чётные числа, начиная с указанного числа:
function* evenNumbers(start: number) {
let n = start;
while(true) {
if (start === 0) {
yield n += 2;
} else {
yield n;
n += 2;
}
}
}
const gen = evenNumbers(6);
console.log(gen.next().value); //6
console.log(gen.next().value); //8
console.log(gen.next().value); //10
console.log(gen.next().value); //12
console.log(gen.next().value); //14
Рекурсивное использование генераторов
Эффективные для памяти свойства генераторов можно использовать для чего-то более полезного, например, для рекурсивного чтения имён файлов внутри каталога. На самом деле, рекурсивный обход вложенных структур — это то, что приходит мне в голову, когда я думаю о генераторах.
Поскольку yield
является выражением, yield*
можно использовать для делегирования другому итерируемому объекту, как показано в следующем примере:
function* readFilesRecursive(dir: string): Generator<string> {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
yield* readFilesRecursive(path.join(dir, file.name));
} else {
yield path.join(dir, file.name);
}
}
}
Мы можем использовать нашу функцию следующим образом:
for (const file of readFilesRecursive('/path/to/directory')) {
console.log(file);
}
Мы также можем использовать yield
для передачи значения генератору. Взгляните на следующий пример:
function* sumNaturalNumbers(): Generator<number, any, number> {
let value = 1;
while(true) {
const input = yield value;
value += input;
}
}
const it = sumNaturalNumbers();
it.next();
console.log(it.next(2).value); //3
console.log(it.next(3).value); //6
console.log(it.next(4).value); //10
console.log(it.next(5).value); //15
Когда вызывается next(2)
, входным данным присваивается значение 2
; аналогично, когда вызывается next(3)
, входным данным присваивается значение 3
.
Обработка ошибок
Обработка исключений и управление потоком выполнения — важная концепция, которую необходимо обсудить, если вы хотите работать с генераторами. Генераторы в основном выглядят как обычные функции, поэтому синтаксис у них такой же.
Когда генератор сталкивается с ошибкой, он может выбросить исключение, используя ключевое слово throw
. Это исключение может быть поймано и обработано с помощью блока try...catch
внутри функции генератора или вне её при использовании генератора:
function* generateValues(): Generator<number, void, string> {
try {
yield 1;
yield 2;
throw new Error('Something went wrong');
yield 3; // Это не будет достигнуто
} catch (error) {
yield* handleError(error); // Обрабатываем ошибку и продолжаем работу
}
}
function* handleError(error: Error): Generator<number, void, string> {
yield 0; // Продолжить со значением по умолчанию
yield* generateFallbackValues(); // Выдача значений по умолчанию
throw `Error handled: ${error.message}`; // Выброс новой ошибки или повторный выброс существующей ошибки
}
const generator = generateValues();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // Не пойманная ошибка: Something went wrong
console.log(generator.next()); // { value: 0, done: false }
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // Something went wrong
В этом примере функция генератора generateValues
выбрасывает ошибку после получения значения 2
. Блок catch
внутри генератора перехватывает ошибку, и управление передаётся функции генератора handleError
, которая выдаёт запасные значения. Наконец, функция handleError
выбрасывает новую ошибку или повторно выбрасывает существующую.
При использовании генератора вы можете отлавливать возникающие ошибки с помощью блока try...catch
:
const generator = generateValues();
try {
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
} catch (error) {
console.error('Caught error:', error);
}
В этом случае ошибка будет поймана блоком catch
, и вы сможете обработать её соответствующим образом.
Заключение
Генераторы можно использовать для множества интересных целей; эффективность использования памяти делает их действительно полезными в некоторых особых случаях. В этой статье мы узнали, как использовать генераторы в TypeScript, рассмотрели их синтаксис и основы итераторов и итерируемых элементов JavaScript. Мы также узнали, как использовать генераторы TypeScript рекурсивно и обрабатывать ошибки с помощью генераторов. Надеюсь, вам понравилась эта статья, и не забудьте оставить комментарий, если у вас возникли вопросы.