Файловые потоки

Последнее обновление: 12.04.2023

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

Если файл большой, его можно прочитать как поток. Это позволяет вам начать обработку данных быстрее, так как не нужно ждать, пока будет прочитан весь файл. Потому что при чтении файла в виде потока считывание файла производится отдельными кусочками или чанками (chunks). Размер каждого такого чанка зависит от конкретной системы, в рамках которой работает приложение на Dart, но обычно это 65 536 байт. Если файл меньшего размера, то не будет большой разницы - считывать файл целиком или как поток.

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

  • openRead(): возвращает поток для чтения файла в виде объекта Stream<T>

    Stream<List<int>> openRead([int? start, int? end]);

    Первый параметр - start представляет индекс байта от начала файла, начиная с которого надо считывать данные (по умолчанию данные считываются с самого начала). Второй параметр - end представляет индекс байта в файле, до которого надо считывать данные (по умолчанию данные считываются до конца файла).

  • openWrite(): возвращает поток для чтения файла в виде объекта IOSink

    IOSink openWrite({FileMode mode = FileMode.write, Encoding encoding = utf8});

    Первый параметр - mode представляет режим открытия файла. По умолчанию это значение FileMode.write, которое представляет перезапись файла. Если же надо выполнить дозапись, применяется значение FileMode.append

    Второй параметр - encoding устанавливает кодировку записи. По умолчанию это UTF-8.

  • open(): возвращает поток для произвольного доступа к файлу в виде объекта RandomAccessFile

    Future open({FileMode mode = FileMode.read});

    В качестве параметра принимает режим открытия файла. По умолчанию это значение FileMode.read, то есть файл открывается для чтения.

Запись в поток

Поток для записи представляет тип IOSink, который представляет ряд методов записи данных:

  • void write(Object? object): записывает в поток произвольный объект

  • void writeAll(Iterable objects, [String separator = ""]): записывает в поток набор objects. Для отделения объектов можно передать separator - разделитель

  • void writeCharCode(int charCode): записывает в поток числовой код символа

  • void writeln([Object? object = ""]) : преобразует объект в строку, вызывая у него метод toString() и записывает строку в поток. К строке добавляется символ перевода строки \n.

Например, запишем в файл строку:

import 'dart:io';

void main() async {
  final file = File("file.txt");
  var sink = file.openWrite();
  sink.write("Hello Metanit.com\n");

  // закрываем IOSink и освобождаем все ресурсы системы
  await sink.close();
  print("File has been written");
}

В данном случае для записи строки применяем метод write(), в который передаем записываемую строку.

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

Чтобы открыть файл для записи, применяется режим открытия FileMode.append:

import 'dart:io';

void main() async {
  final file = File("file.txt");
  // открываем файл для дозаписи
  var sink = file.openWrite(mode: FileMode.append);
  sink.write("Hello Metanit.com\n");
  await sink.close();
  print("File has been written");
}

При записи для объектов классов в файл записывается строковое представление объекта, которое определяется с помощью метода toString(). Например:

import 'dart:io';

void main() async{

  Person tom = Person("Tom", 38);
  final file = File("person.dat");
  var sink = file.openWrite();
  sink.write(tom);
  await sink.close();
  print("File has been written");
}
class Person{
  String name;
  int age;
  Person(this.name, this.age);
  void display() => print("Name: $name \tAge: $age");
  @override
  toString() => "{name: $name, age: $age}";
}

В данном случае при записи объекта tom в файл будет записана строка "{name: Tom, age: 38}".

Чтение из потока

Для чтения данных применяется поток Stream<List<int>>, который передает данные в виде списка байт. Причем получение даннз из потока происходит через подписку на уведомления - когда в потоке появляется новая порция данных, поток посылает уведомление. И чтобы подписаться на уведомления, надо использовать метод listen() класса Stream:

StreamSubscription<T> listen(void onData(T event)?,
    {Function? onError, void onDone()?, bool? cancelOnError});

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

Дополнительно метод принимает функции onError() (вызывается при возникновении ошибки) и onDone() (вызывается после закрытия потока)

Последний параметр - cancelOnError позволяет отменить подписку при генерации оишбки (при значении true)

Например, пусть у нас есть файл file.txt, в который записана строка "Hello Metanit.com". Считаем его через поток:

import 'dart:io';

void main(){
  final file = File("file.txt");
  final stream = file.openRead();
  stream.listen( (data) => print(data));
}

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

[72, 101, 108, 108, 111, 32, 77, 101, 116, 97, 110, 105, 116, 46, 99, 111, 109, 10]

Поскольку поток для чтения получает данные в виде списка байт, то в анонимную функцию в качестве аргумента передается список байт, который потом выводится на консоль. То есть фактически этот список байт представляет строку "Hello Metanit.com". А отдельные числа в этом списке - это числовые кода символов строки.

Поскольку записанные данные в реальности представляют строку, а не бинарные данные, то мы можем их преобразовать в строку с помощью встроенной функции utf8.decode() из пакета dart:convert:

import 'dart:io';
import 'dart:convert';

void main(){
  final file = File("file.txt");
  final stream = file.openRead();
  // преобразуем в строку и выводим на консоль
  stream.listen( (data) => print(utf8.decode(data))); 
}

Получение данных в цикле for-in

Также есть еще один способ получения данных - в цикле for-in:

import 'dart:io';
import 'dart:convert';

void main() {
  final file = File("file.txt");
  final stream = file.openRead();
  await for (var data in stream) {
    print(utf8.decode(data));
  }
}

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

Использование коллбеков и обработка ошибок

Метод listen() класса Stream позволяет через необязательные параметры передать функции обратного вызова, которые будут срабатывать при возникновении ошибки или при завершение получения данных

import 'dart:io';
import 'dart:convert';

void main() {
  final file = File("file.txt");
  final stream = file.openRead();
  stream.listen( 
    (data) => print(utf8.decode(data)),
    onError: (error) {print(error);},       // срабатывает при ошибке
    onDone: () { print("End of reading");}  // срабатывает при завершении
  );
}

Обработчик onError принимает объект ошибки и срабатывает при возникновении ошибки.

Обработчик onDone не принимает параметров и срабатывает при завершении работы функции.

Последний необязательный параметр - cancelOnError прекращает работу потока, если этот параметр равен true:

stream.listen( 
    (data) => print(utf8.decode(data)),
    onError: (error) {print(error);},       // срабатывает при ошибке
    onDone: () { print("End of reading");},  // срабатывает при завершении
    cancelOnError: true         // прекращаем работу потока при возникновении ошибки
  );

По умолчанию cancelOnError равен false, что означает, что даже если возникнет ошибка, обработчик onDone все равно будет срабатывать.

Естественно для обработки ошибки также можно использовать конструкцию try-catch, особенно если мы считываем данные в цикле for

import 'dart:io';
import 'dart:convert';

void main(){
  try {
    final file = File("file.txt");
    final stream = file.openRead();
    await for (var data in stream) {
      print(utf8.decode(data));
    }
  } on Exception catch (error) {
    print(error);
  } finally {
    print("End of reading");
  }
}

Преобразование потока

В примерах выше считываемый файл представлял по сути текст. Однако поскольку для потока Stream все считываемые данные представляют набор байт - тип List<int>, то, чтобы получить собственно строку, нам приходилось применять к набору байтов функцию utf8.decode().

Класс Stream с помошью метода transform() позволяет автоматически преобразовать данные в нужную форму. В качестве параметра он принимает объект StreamTransformer, который управляет преобразованием данных. Применим метод transform():

import 'dart:io';
import 'dart:convert';

void main(){
  final file = File("file.txt");
  final stream = file.openRead();
  await for (var data in stream.transform(utf8.decoder)) {
    print(data);  // data - уже представляет строку
  }
}

В данном случае в метод stream.transform() передается объект utf8.decoder, который преобразует данные из байтов в строку. Поэтому на выходе мы получим строку.

Аналогичное преобразование ср считыванием с помошью метода stream.listen()

import 'dart:io';
import 'dart:convert';

void main(){
  final file = File("file.txt");
  final stream = file.openRead();
  stream
    .transform(utf8.decoder)  // преобразование данных
    .listen((data) => print(data) );
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850