Итераторы и генераторы последовательностей.



Глава 7. Файловая система и динамические структуры данных

Работа с файлами с помощью языка Node . js .

Запись в текстовый файл

Когда мы составляем программу на каком-либо языке программирования и запускаем на выбранной платформе (внутри операционной системы), то зачастую эта операционная система предоставляет нам доступ к файловой системе компьютера. Благодаря этому мы можем вводить данные не только вручную с клавиатуры, а получать их из текстового (числового) или даже двоичного файла. Результаты, полученные в программе, тоже можно записать в файл (текстовый или графический, например).

Но у браузера напрямую нет доступа к файловой системе компьютера клиента. Данные можно загрузить или из другой веб-страницы, или прибегнуть к помощи программирования для серверов.

Все задачи, рассматриваемые в этом пункте, не будут запускаться и работать на JavaScript, язык не предназначен для этих целей, но можно воспользоваться возможностями Node.js.

Чтение и запись в файл производятся в двух режимах, синхронном и асинхронном, но чаще используется асинхронный режим.

Рассмотрим конкретную задачу: записать фразу «Hello, world!» в текстовый файл hello.txt.

Для простоты будем использовать имитацию файловой системы в сеансе работы Replit. После запуска программы в левом поле появится дополнительный файл:

Теперь сам скрипт:

var fs = require('fs');

fs.writeFile('hello.txt', 'Hello, world!', function(err) {

if(err) return console.error(err);

});

В первой строке происходит подключение модуля для работы с файловой системой – fs. После подключения этого модуля с помощью команды require будут доступны методы работы с файлами: открытие файла, чтение и запись, копирование, переименование и многие другие.

В данном случае нас интересует запись текста из строковой переменной в файл. Соответствующий метод имеет вид:

writeFile('имя файла с расширением, 'Строка, которую надо записать')

Но почему такой скрипт:

var fs = require('fs');

fs.writeFile('hello.txt', 'Hello, world!');

не запускается?

Дело в том, что данный метод требует обратного вызова. Вдруг файловая система защищена от записи или недоступна? Необходимо предусмотреть способы защиты от ошибок. Вот почему в описание метода добавлена функция:

function(err)

{

if(err) return console.error(err);

});

Она возвращает код ошибки при проблемах создания файла.

Это упрощенный пример. Во всех языках программирования работа с файлами строится по определенному алгоритму:

1) открытие файла с параметрами доступа для чтения, для записи, для чтения или перезаписи;

2) непосредственно чтение или запись данных;

3) завершение работы с файлом.

Благодаря «сборщикам мусора», последний пункт стал необязательным.

Приведем новый скрипт с записью в файл hello1.txt того же текста:

const fs = require("fs");

fs.open("hello1.txt",'w', (error) => {

   if(error) throw error;

   console.log('File created');

});

fs.writeFile("hello1.txt", "Hello, World!", function(error){

if(error) throw error; // если возникла ошибка

} )

Результатом работы этого скрипта станет сообщение «File created» в консоли и сам текстовый файл:

Содержимое этого текстового файла можно менять вручную, как в текстовом редакторе, но при повторном выполнении скрипта информация снова перезапишется.

В этом скрипте использовалась инструкция throw, которая позволяет генерировать исключения, определяемые пользователем.

Есть и синхронная версия того же метода для записи файла — fs.writeFileSync(), а работу с исключениями можно произвести через операторы try/catch:

const fs = require('fs')

const content = 'Some content!'

try {

const data = fs.writeFileSync('test.txt', content)

//файл записан успешно

} catch (err) {

console.error(err)

}

Эти методы записи, по умолчанию, заменяют содержимое существующих файлов. Изменить их стандартное поведение можно, воспользовавшись соответствующим флагом:

fs.writeFile('test.txt', content, { flag: 'a+' }, (err) => {})

В этом случае новая информация добавится в конец существующего файла.

Запишем с помощью этого флага числа циклической последовательности:

const fs = require("fs");

fs.open("hello1.txt",'w', (error) => {

   if(error) throw error;

   console.log('File created');

});

for (let i=1;i<=3;i++)

{

fs.writeFile("hello1.txt", i, { flag: 'a+' }, function(error){

if(error) throw error; // если возникла ошибка

} )

}

Без флага ‘a+’ в файл запишется только последнее число, а предыдущие значения сотрутся.

 

Чтение из текстового файла

Допустим, у нас уже есть текстовый файл с нужными числовыми значениями:

Выведем эти значения на экран:

const fs = require("fs");

fs.readFile("hello1.txt", "utf8",

function(error,data){               

if(error) throw error; // если возникла ошибка

console.log(data); // выводим считанные данные

});

Усложним задачу. Пусть необходимо посчитать сумму полученных чисел и вывести на экран консоли:

const fs = require("fs");

let s=0

let fileContent = fs.readFileSync("hello.txt", "utf8");

s=s+parseInt(fileContent)

console.log(s)

В этой задаче меняется только метод на синхронный, позволяющий считать информацию из файла в переменную:

имя переменной = fs.readFileSync("имя файла", "кодировка");

Проверять на ошибки файл не обязательно. Если он создан, то скрипт считает нужную информацию, а если нет, то возникнет сообщение об ошибке.

Несмотря на то, что в нашем файле записаны верные данные, сумма посчиталась неправильно, и в ответе получилась «единица», а не «шестерка».

Дело в том, что файлы обрабатываются целиком, функция parseInt() считывает только первое число, игнорируя пробелы, знаки абзаца и другие символы.

Для решения задачи либо придется разбивать считанную строку на отдельные элементы массива, либо использовать модуль Readline.

Readline - это собственный модуль Node.js, он был разработан специально для чтения содержимого построчно из любого читаемого потока. Его можно использовать для чтения данных из командной строки.

Первый вариант решения:

const fs = require("fs");

let s=0

let fileContent = fs.readFileSync("hello.txt", "utf8");

var arr = fileContent.split(' ')

for (var i in arr)

{s=s+parseInt(arr[i])}

console.log(s)

 

Второй вариант решения:

const fs = require( 'fs' );

const readline = require( 'readline' );

const file = readline.createInterface({

input: fs.createReadStream( 'hello.txt' ),

output: process.stdout,

terminal: false

});

let s=0;

file.on('line' , (line) => {

s=s+parseInt(line);

console.log(s);

} );

 

Методы модуля fs

fs.access(): проверяет существование файла и возможность доступа к нему с учётом разрешений.

fs.appendFile(): присоединяет данные к файлу. Если файл не существует — он будет создан.

fs.chmod(): изменяет разрешения для заданного файла. Похожие методы: fs.lchmod(), fs.fchmod().

fs.chown(): изменяет владельца и группу для заданного файла. Похожие методы: fs.fchown(), fs.lchown().

fs.close(): закрывает дескриптор файла.

fs.copyFile(): копирует файл.

fs.createReadStream(): создаёт поток чтения файла.

fs.createWriteStream(): создаёт поток записи файла.

fs.link(): создаёт новую жёсткую ссылку на файл.

fs.mkdir(): создаёт новую директорию.

fs.mkdtemp(): создаёт временную директорию.

fs.open(): открывает файл.

fs.readdir(): читает содержимое директории.

fs.readFile(): считывает содержимое файла. Похожий метод: fs.read().

fs.readlink(): считывает значение символической ссылки.

fs.realpath(): разрешает относительный путь к файлу, построенный с использованием символов «.» и «..», в полный путь.

fs.rename(): переименовывает файл или папку.

fs.rmdir(): удаляет папку.

fs.stat(): возвращает сведения о файле. Похожие методы: fs.fstat(), fs.lstat().

fs.symlink(): создаёт новую символическую ссылку на файл.

fs.truncate(): обрезает файл до заданной длины. Похожий метод: fs.ftruncate().

fs.unlink(): удаляет файл или символическую ссылку.

fs.unwatchFile(): отключает наблюдение за изменениями файла.

fs.utimes(): изменяет временную отметку файла. Похожий метод: fs.futimes().

fs.watchFile(): включает наблюдение за изменениями файла. Похожий метод: fs.watch().

fs.writeFile(): записывает данные в файл. Похожий метод: fs.write().

Интересной особенностью модуля fs является тот факт, что все его методы, по умолчанию, являются асинхронными, но существуют и их синхронные версии, имена которых получаются путём добавления слова Sync к именам асинхронных методов.

Например:

fs.rename() - fs.renameSync()

fs.write() - fs.writeSync()

Рассмотрим пример переименования файлов с помощью асинхронной версии с использованием callback (возвратов):

const fs = require('fs')

fs.rename('test.txt', 'after.txt', (err) => {

if (err) {

return console.error(err)

}

})

При использовании его синхронной версии для обработки ошибок используется конструкция try/catch:

const fs = require('fs')

try {

fs.renameSync('after.txt', 'test.txt')

} catch (err) {

console.error(err)

}

Основное различие между этими вариантами использования данного метода заключается в том, что во втором случае выполнение скрипта будет заблокировано до завершения файловой операции.

 

Коллекции данных

Ассоциативный массив

В настоящее время среди документов получили распространение базы данных, а программы, в которых можно производить действия над данными, называются СУБД – системы управления базами данных.

Для работы в сетях можно напрямую подключаться к нужной базе данных, а языки веб-программирования тесно взаимодействуют с СУБД, работающими в сетевом режиме. Например, Node.js поддерживает работу с MySQL, а также со многими сторонними СУБД.

Но для небольших задач гораздо удобнее было бы пользоваться массивами данных, если не одно но… Допустим, у нас есть сведения о студентах некоторой группы. В одном массиве хранятся фамилии, а в другом – оценка каждого студента в том же порядке следования элементов. Если массив со списком отсортировать в алфавитном порядке, то оценки не будут соответствовать их получателям. В этом случае придется производить обмен данными при сортировке обоих массивов. А если у нас и третий параметр, например, даты рождения?

В языках программирования высокого уровня содержатся более удобные средства для работы со сложными данными – записи (Паскаль) или структуры (С++). В объектно-ориентированных средах программирования можно создавать более сложные структурированные данные, например, словари, в которых каждому значению противопоставляется его ключ.

Аналогичный объект используется и в JavaScript. Он называется ассоциативным массивом.

Имя и ключ разделяются двоеточием:

let student=

{

name: "Иванов", mark: "5"

}

console.log(student.name)

console.log(student.mark)

На экране мы получим следующее:

Основное отличие ассоциативных массивов от обычных состоит в том, что в качестве индексов используются не числа, а строки.

Добавим в этот массив новое свойство – дату рождения, дописав соответствующую строку:

Вывести все элементы ассоциативного массива можно двумя способами: 1) написать имя массива; 2) использовать цикл for..in:

Для нахождения длины массива, т.е. определения количества элементов, метод length напрямую не действует. Но можно использовать метод Object.keys(), который возвращает массив из собственных перечисляемых свойств переданного объекта, в том же порядке, в котором они бы обходились циклом for...in:

В первом случае (строка 12) метод возвращает длину строки – значения элемента, а во втором (строка 13) – количество всех элементов.

Студенческая группа состоит. Как правило, более, чем из одного студента, поэтому для описания всех студентов можно использовать массив, элементами которого являются ассоциативные массивы. Над такими структурами можно выполнять различные операции: суммирование элементов, сортировка, нахождение элемента по определенному параметру. Для выполнения этих действий в цикле необходимо после имени массива через точку записывать его ключ.

Элементами ассоциативных массивов могут быть и числа, но индексы являются строками. Например:

let m=new Array()

m["one"]=1

m["two"]=2

m["three"]=3

for (let i in m)

console.log(m[i])

 

Множество

Множества (sets) представляют структуру данных, которая может хранить только уникальные значения. В JavaScript функционал множества определяет объект Set. Для создания множества применяется конструктор этого объекта.

Также можно передать в конструктор массив значений, которыми будет инициализировано множество.

Для добавления применяется метод add(). Его результатом является измененное множество. Для удаления элементов применяется метод delete(), а для удаления всех элементов - clear().

Если нужно проверить, если ли элемент в множестве, то используется метод has(). Если элемент есть, то метод возвращает true, иначе возвращает false. Для перебора элементов множества применяется метод forEach(). Также для перебора множества можно использовать цикл for...of.

Рассмотрим множество первых целых чисел на примере:

1 способ 2 способ
const mySet = new Set(); mySet.add(1); mySet.add(2).add(3); console.log(mySet) const arr = [1, 1, 2, 3, 2]; const numbers = new Set(arr); console.log(numbers);

 

Два способа создания множества дают одинаковый результат.

Рассмотрим операции над элементами:

console.log(numbers.size);

numbers.delete(3);

console.log(numbers);

let isDeleted = numbers.delete(3);

console.log(isDeleted);

console.log(numbers.has(3));

numbers.forEach(function(value1, value2, set){

console.log(value1);

})

for(n of numbers){

console.log(n);

}

 

Результат:

 

Стандартные операции над множествами:

- Union (Объединение). Объединяет все элементы из двух разных множеств и возвращает результат, как новый набор (без дубликатов).

- Intersection (Пересечение). Если заданы два множества, эта функция вернет другое множество, содержащее элементы, которые имеются и в первом и во втором множестве.

- Difference  (Разница). Вернет список элементов, которые находятся в одном множестве, но НЕ повторяются в другом.

- Subset(Подмножество) - возвращает булево значение, показывающее, содержит ли одно множество все элементы другого множества.

Рассмотрим пример с предыдущими множествами:

const mySet = new Set();

mySet.add(1);

mySet.add(2).add(3);

const arr = [1, 1, 2, 3, 2];

const numbers = new Set(arr);

numbers.delete(3);

flag=false

const unionSet=new Set();

const interSet=new Set();

const subSet=new Set();

for (element of mySet)

unionSet.add(element)

for (element of numbers)

{

if (!unionSet.has(element))

unionSet.add(element)

}

console.log(unionSet)

for (element of mySet)

{

for (element2 of numbers)

if (element==element2)

interSet.add(element)

}

console.log(interSet)

for (element of mySet)

subSet.add(element)

for (element of numbers)

{

if (subSet.has(element))

subSet.delete(element)

}

console.log(subSet)

for (element of mySet)

{

for (element2 of numbers) 

if (element==element2 && numbers.size<mySet.size)

flag=true

}

 if (flag) console.log("numbers underset mySet")

 

Словарь

Словарь Map – это коллекция ключ/значение, как и ассоциативный массив. Но основное отличие в том, что Map позволяет использовать ключи любого типа.

Методы и свойства:

new Map() – создаёт коллекцию.

map.set(key, value) – записывает по ключу key значение value.

map.get(key) – возвращает значение по ключу или undefined, если ключ key отсутствует.

map.has(key) – возвращает true, если ключ key присутствует в коллекции, иначе false.

map.delete(key) – удаляет элемент по ключу key.

map.clear() – очищает коллекцию от всех элементов.

map.size – возвращает текущее количество элементов.

Для перебора коллекции Map есть 3 метода:

map.keys() – возвращает итерируемый объект по ключам,

map.values() – возвращает итерируемый объект по значениям,

map.entries() – возвращает итерируемый объект по парам вида [ключ, значение], этот вариант используется по умолчанию в for..of.

Рассмотрим пример:

let map = new Map();

map.set("1", "str1"); // строка в качестве ключа

map.set(1, "num1"); // цифра как ключ

map.set(2, "num1")

map.set(3, "num1")

console.log(map.get(1));

console.log(map.get("1"));

map.delete("1")

console.log(map.size);

console.log(map.values())

console.log(map.keys())

 

Классы и объекты

В отличии от структуры С++ ассоциативный массив в JavaScript – это объект, поэтому рассмотрим создание любого типа объектов подробнее.

Любой объект можно воспринимать как совокупность данных и методов (функций для их обработки). Рассмотрим тот же пример с базой данных, но установим, что объект – это студент. У этого объекта в нашем примере будут две характеристики – имя и оценка. Они называются свойствами объекта. Фактически, свойство – это имя переменной, указывающее на его характеристики. Обратиться к свойству можно, указав его через точку:

Объект.свойство

Например, student.name

Метод – это функция для выполнения действий над данными. Например, можно вывести все свойства объекта или произвести расчеты по каким-либо признакам.

Обратиться к методу можно так же, как и к свойству, но с указанием параметров функции:

Объект.метод(параметры)

Например, student.getInfo()

С методом можно связать саму функцию, которая и будет обрабатываться.

Поясним на конкретном примере. Создадим функцию и соотнесем ее в качестве метода getInfo. Она будет определять, в каком порядке выводить значения данного объекта student:

var student=new Object()

student.name="Иванов"

student.mark="5"

student.getInfo=function()

{

let info=this.name+" - "+this.mark

return info

}

console.log(student.getInfo())

В первой строке этого скрипта мы создали объект student с помощью встроенного класса Object. После того, как объект создан, в переменной student сохраняется на него ссылка. В качестве значения свойства объекта можно использовать любой тип данных, например, число, массив, другой объект. А если в качестве значения указана ссылка на функцию, то такое свойство становится методом объекта. Внутри этого метода доступен указатель на текущий объект – this.

Создать подобный объект можно другим способом, определив свойства и методы в фигурных скобках, как в языке С++:

const student={

name:"Иванов",

mark:"5",

getInfo:function()

{

let info=this.name+" - "+this.mark

return info

} }

console.log(student.getInfo())

 

Объекты можно группировать в классы. Класс – это шаблон (тип) объекта, включающий в себя свойства (переменные) и методы (функции). Каждый объект в классе называется экземпляром.

Создание экземпляра класса имеет вид:

Экземпляр = new имя_класса(параметры)

Как и в примере с ассоциативным массивом, в студенческой группе обучается более одного человека. Поэтому класс – это вся группа, а объект – это один конкретный студент со своей фамилией и оценкой.

В JavaScript классы создаются с помощью функции-конструктора, например:

function student(name, mark)

{

this.name=name

this.mark=mark

this.getInfo=function(){

let info=this.name+" - "+this.mark

return info

} }

let student1=new student("Иванов","5")

let student2=new student("Петров","4")

console.log(student1.getInfo())

Внутри функции-конструктора могут располагаться методы – внутренние функции класса, манипулирующие свойствами объекта. С помощью них можно изменять и выводить значения свойств, например, входящих переменных и других объектов.

Если свойства объектов будут содержать несколько значений, а метод один и тот же, то можно определить метод вне конструктора с помощью прототипа.

Для добавления метода в прототип используется свойство prototype. Например:

function chislo(m)

{

this.znachenie=m;

}

chislo.prototype.getZnach=function(){

return this.znachenie

}

let num1=new chislo(1)

let num2=new chislo(2)

let num3=new chislo(3)

console.log(num1.getZnach())

console.log(num1.znachenie)

В этом примере значение «1» выведется и при прямом обращении к свойству, и при использовании функции-прототипа.

 

Итераторы и генераторы последовательностей.

Итераторы и генераторы пригодятся, когда вы хотите постепенно обрабатывать коллекцию. Вы получаете прирост в производительности, отслеживая только состояние коллекции, а не загружая все элементы. Элементы в коллекции оцениваются по одному, а оценка остальной коллекции откладывается до следующего обращения.

Итераторы обеспечивают эффективный способ перемещения и управления большими списками. Генераторы обеспечивают эффективный способ создания списков.

В JavaScript итератор - это объект, который предоставляет метод next(), возвращающий следующий элемент последовательности. Этот метод возвращает объект с двумя свойствами: done и value.

Допустим, у нас есть объект, в котором надо перечислить элементы. Рассмотрим последовательность первых трех чисел с помощью итератора:

let range = {

from: 1,

to: 3

}

// сделаем объект range итерируемым

range[Symbol.iterator] = function() {

let current = this.from;

let last = this.to;

// метод должен вернуть объект с методом next()

return {

next() {

if (current <= last) {

   return {

     done: false,

     value: current++

   };

} else {

   return {

     done: true

   };

}

}

}

};

for (let num of range) {

console.log(num); // 1, затем 2, 3

}

Перебираемый объект range сам не реализует методы для своего перебора. Для этого создаётся другой объект, который хранит текущее состояние перебора и возвращает значение. Этот объект называется итератором и возвращается при вызове метода range[Symbol.iterator].

У итератора должен быть метод next(), который при каждом вызове возвращает объект со свойствами:

value – очередное значение,

done – равно false если есть ещё значения, и true – в конце.

Конструкция for..of в начале своего выполнения автоматически вызывает Symbol.iterator(), получает итератор и далее вызывает метод next() до получения done: true. Внешний код при переборе через for..of видит только значения.

Генераторы — это особый класс функций, которые упрощают задачу написания итераторов.

Генератор — это процесс, который может быть остановлен и возобновлен, и может выдать несколько значений. Генаратор в JavaScript состоит из функции генераторов, которая возвращает элемент Generator, поддерживающий итерации.

Генераторы могут поддерживать состояние и обеспечивать эффективный способ создания итераторов, а также позволяют работать с бесконечным потоком данных. Оператор yield может приостановить функцию генератора и вернуть значение, которое следует за yield, тем самым обеспечивая более простой способ итерации значений.

Для демонстрации генератора рассмотрим аналогичный пример с последовательностью из трех чисел:

function* idGenerator() {

let i = 1;

while (i<=3) {

yield i++;

}

}

const ids = idGenerator();

 for (let i of idGenerator())

console.log(ids.next(i).value);

 

Динамические структуры

Стек

Стек  (stack) – это линейная структура данных, в которой элементы располагаются по принципу: первым зашел, последним вышел.

Стек имеет следующие методы:

Push - добавить новый элемент

Pop - удалить верхний элемент, вернуть его

Peek - вернуть верхний элемент

Length - вернуть количество элементов в стеке

Рассмотрим пример, в котором в стек добавляются последовательно три элемента, а затем они поочередно удаляются:

var stack = [];

for (let i=1;i<=3;i++)

console.log(stack.push(i))

stack.pop()

console.log(stack.push())

stack.pop()

console.log(stack.push())

Результат будет таким:

В первом выводе отобразилась последовательность 1, 2, 3. Но внутри стека элементы расположены в обратном порядке: 3, 2, 1 (первый элемент внизу).

Затем удалили первый элемент, т.е. удалили 3, а первым стал 2.

Снова удалили первый элемент, остался последний – 1.

Фактически, стек представляет собой стопку – пока дотянулись до нижней вещи, надо снять все верхние.

 

Очередь

Очередь (Queue) – аналогичная стеку структура, но расположение элементов – первым зашел, первым и вышел.

Это означает, что после добавления нового элемента все элементы, которые были добавлены до этого, должны быть удалены до того, как новый элемент будет удален.

Очередь имеет следующие методы:

enqueue: войти в очередь, добавить элемент в конец

dequeue: покинуть очередь, удалить первый элемент и вернуть его

front: получить первый элемент

isEmpty: проверить, пуста ли очередь

size: получить количество элементов в очереди

Создать очередь можно вручную как объект и описать прототипы функций для работы с ней:

function Node(data) {

this.data = data;

this.next = null;

}

function Queue() {

this.head = null;

this.tail = null;

}

Queue.prototype.enqueue = function(data) {

var newNode = new Node(data);

if (this.head === null) {

this.head = newNode;

this.tail = newNode;

} else {

this.tail.next = newNode;

this.tail = newNode;

}

}

Queue.prototype.dequeue = function() {

var newNode;

if (this.head !== null) {

newNode = this.head.data;

this.head = this.head.next;

}

return newNode;

}

Queue.prototype.print = function() {

var curr = this.head;

while (curr) {

console.log(curr.data);

curr = curr.next;

}

}

var q = new Queue();

q.enqueue(1);

q.enqueue(2);

q.enqueue(3);

q.print();

q.dequeue();

q.print();

q.dequeue();

q.print();

Результат:

Числа удалились в том же порядке, в каком и следовали.

 


Дата добавления: 2021-12-10; просмотров: 18; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!