Kotlin Path.useLines {} — Как не получить IOException («Поток закрыт»)?

В Kotlin есть хорошие обертки и ярлыки, но иногда я не понимаю их.

У меня есть этот упрощенный код:

class PipeSeparatedItemsReader (private val filePath: Path) : ItemsReader {
    override fun readItems(): Sequence<ItemEntry> {

        return filePath.useLines { lines -> 
            lines.map { ItemEntry("A","B","C","D",) } 
        }
}

И тогда у меня есть:

    val itemsPath = Path(...).resolve()
    val itemsReader = PipeSeparatedItemsReader(itemsPath)

    for (itemEntry in itemsReader.readItems())
        updateItem(itemEntry)

    // I have also tried itemsReader.readItems().forEach { ... }

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

Однако я получаю IOException("Stream closed").

Каким-то образом, еще до того, как будет прочитан первый элемент (я отлаживал), где-то внутри оберток Котлина reader.in становится нулевым, поэтому это исключение выбрасывается в hasNext().

Я видел аналогичный вопрос здесь: Kotlin для объединения нескольких последовательностей из разных InputStream?

Это включает в себя много шаблонов Java, которых я хотел бы избежать.

Как мне закодировать эту последовательность, используя Path.useLines()?

Короче: не могу. Помощники use* по своей природе закрывают ресурс в конце своего вызова, а не во время обработки последовательности (как это может звучать в текущем KDoc). Спасибо @Joffrey за разъяснение.

Ondra Žižka 03.11.2022 16:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
1
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Каждый помощник Kotlin с «использованием» в имени закрывает базовый ресурс в конце передаваемой вами лямбды (по крайней мере, насколько я знаю, это соглашение в stdlib). Самый распространенный пример — AutoCloseable.use.

Расширение Path.useLines не исключение:

Вызывает обратный вызов блока, передавая ему последовательность всех строк в этом файле, и закрывает средство чтения после завершения обработки. [выделено мной]

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

Итак, если вы хотите вернуть последовательность строк для последующего использования, вы не можете напрямую вернуть преобразованную последовательность из последовательности useLines. Последовательности на самом деле не могут определить, когда кто-то закончил их использовать, поэтому useLines нужна лямбда, чтобы дать «область действия» или «время жизни» последовательности и знать, когда закрыть основной считыватель.

Если вы хотите обернуть это, у вас есть 2 основных варианта: либо разделить операцию последовательности и операцию закрытия (сделать ваш PipeSeparatedItemsReader закрывающимся), либо использовать лямбду для обработки вещей на месте в readItems() так же, как это делает useLines.

Спасибо! Но тогда термин processing вводит в заблуждение. Обрабатывая последовательность, я понимаю, что перебрал все элементы, которые должна дать последовательность. Поэтому я ожидал, что файл будет закрыт после последней строки. Есть ли какой-нибудь набор оберток вокруг файлов, которые будут работать таким образом, без моего написания шаблона? Я имею в виду, вполне разумно ожидать, что можно последовательно обрабатывать большие файлы с помощью всего 1 строки-оболочки, не так ли?

Ondra Žižka 03.11.2022 14:14

Кроме того, есть ли какой-нибудь словарь, объясняющий, что KDoc подразумевает под такими вещами, как «обработка последовательности»? Или обработка относится к блоку? Это следует уточнить.

Ondra Žižka 03.11.2022 14:15

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

Joffrey 03.11.2022 14:17

Поэтому я ожидал, что файл будет закрыт после последней строки. - проблема в том, что с последовательностью нельзя гарантировать, что потребитель последовательности дойдет до последней строки. Например, использование take или first или других помощников быстрого замыкания приведет к преждевременному завершению коллекции и не дойдет до конца, и в этом случае вы не хотите, чтобы считыватель зависал.

Joffrey 03.11.2022 14:20

Правильно, это открыло бы код для утечек памяти/FD. Вот мне тоже было интересно. Я предлагаю добавить ссылку в KDoc на некоторые общие ссылки на утилиты последовательностей - многим это может сэкономить время. (Хотя в коде всего 2 клика, если знать, что искать...)

Ondra Žižka 03.11.2022 14:26

Итак: 1) Нет такой вещи, как ClosableSequence, которая могла бы распространять запрос на закрытие, верно? 2) Следовательно, для ленивой итерации мне нужно будет использовать seqence { ... } и некоторую форму yield(), чтобы лямбда могла «выжить» во время итерации? Что также означает, что я беру на себя ответственность за полное использование последовательности? И как должно проходить досрочное закрытие? Должен ли я слить Reader из лямбды и сохранить его в настоящее время ClosablePipeSeparatedItemsReader? Это кажется странным.

Ondra Žižka 03.11.2022 14:45

Потому что теперь у меня есть return Files.newBufferedReader(filePath).use { ... } . Но это касается той же проблемы. Так что я думаю, что вообще не могу использовать use* для создания ленивого «конвейера» через мой API. use — это просто вещь, которая блокирует и закрывает, по идее. Правильный?

Ondra Žižka 03.11.2022 14:49

Извините за задержку. Да у тебя тут все правильно. Итак, как я сказал в ответе, я думаю, что вы можете либо сохранить читатель как свойство в PipeSeparatedItemsReader, и сделать класс-оболочку закрывающимся, чтобы закрыть базовый читатель, либо вы можете заставить свой метод принимать лямбда вместо возврата последовательности, чтобы сдвинуть проблема вверх (надеюсь, к месту, где люди контролируют всю жизнь последовательности).

Joffrey 03.11.2022 16:29

Другие вопросы по теме