В 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 { ... }
Что довольно просто — я ожидаю, что этот код даст мне последовательность, которая открывает файл и читает строки, анализирует их и дает ItemEntry
s, а когда используется, закрывает файл.
Однако я получаю IOException("Stream closed")
.
Каким-то образом, еще до того, как будет прочитан первый элемент (я отлаживал), где-то внутри оберток Котлина reader.in
становится нулевым, поэтому это исключение выбрасывается в hasNext()
.
Я видел аналогичный вопрос здесь: Kotlin для объединения нескольких последовательностей из разных InputStream?
Это включает в себя много шаблонов Java, которых я хотел бы избежать.
Как мне закодировать эту последовательность, используя Path.useLines()
?
Каждый помощник Kotlin с «использованием» в имени закрывает базовый ресурс в конце передаваемой вами лямбды (по крайней мере, насколько я знаю, это соглашение в stdlib). Самый распространенный пример — AutoCloseable.use.
Расширение Path.useLines не исключение:
Вызывает обратный вызов блока, передавая ему последовательность всех строк в этом файле, и закрывает средство чтения после завершения обработки. [выделено мной]
Это означает, что useLines
закрывает последовательность строк после завершения блока, и, таким образом, вы не можете вернуть из нее ленивую последовательность, потому что вы не можете использовать ее после возврата из функции useLines
.
Итак, если вы хотите вернуть последовательность строк для последующего использования, вы не можете напрямую вернуть преобразованную последовательность из последовательности useLines
. Последовательности на самом деле не могут определить, когда кто-то закончил их использовать, поэтому useLines
нужна лямбда, чтобы дать «область действия» или «время жизни» последовательности и знать, когда закрыть основной считыватель.
Если вы хотите обернуть это, у вас есть 2 основных варианта: либо разделить операцию последовательности и операцию закрытия (сделать ваш PipeSeparatedItemsReader
закрывающимся), либо использовать лямбду для обработки вещей на месте в readItems()
так же, как это делает useLines
.
Спасибо! Но тогда термин processing
вводит в заблуждение. Обрабатывая последовательность, я понимаю, что перебрал все элементы, которые должна дать последовательность. Поэтому я ожидал, что файл будет закрыт после последней строки. Есть ли какой-нибудь набор оберток вокруг файлов, которые будут работать таким образом, без моего написания шаблона? Я имею в виду, вполне разумно ожидать, что можно последовательно обрабатывать большие файлы с помощью всего 1 строки-оболочки, не так ли?
Кроме того, есть ли какой-нибудь словарь, объясняющий, что KDoc подразумевает под такими вещами, как «обработка последовательности»? Или обработка относится к блоку? Это следует уточнить.
На самом деле я сделал ошибку в своем первоначальном «исправлении», см. обновленный ответ. Термин «обработка» действительно вводит в заблуждение, документы можно было бы улучшить, уточнив тот факт, что ридер закрывается в конце переданной лямбды.
Поэтому я ожидал, что файл будет закрыт после последней строки. - проблема в том, что с последовательностью нельзя гарантировать, что потребитель последовательности дойдет до последней строки. Например, использование take
или first
или других помощников быстрого замыкания приведет к преждевременному завершению коллекции и не дойдет до конца, и в этом случае вы не хотите, чтобы считыватель зависал.
Правильно, это открыло бы код для утечек памяти/FD. Вот мне тоже было интересно. Я предлагаю добавить ссылку в KDoc на некоторые общие ссылки на утилиты последовательностей - многим это может сэкономить время. (Хотя в коде всего 2 клика, если знать, что искать...)
Итак: 1) Нет такой вещи, как ClosableSequence
, которая могла бы распространять запрос на закрытие, верно? 2) Следовательно, для ленивой итерации мне нужно будет использовать seqence { ... }
и некоторую форму yield()
, чтобы лямбда могла «выжить» во время итерации? Что также означает, что я беру на себя ответственность за полное использование последовательности? И как должно проходить досрочное закрытие? Должен ли я слить Reader
из лямбды и сохранить его в настоящее время Closable
PipeSeparatedItemsReader
? Это кажется странным.
Потому что теперь у меня есть return Files.newBufferedReader(filePath).use { ... }
. Но это касается той же проблемы. Так что я думаю, что вообще не могу использовать use*
для создания ленивого «конвейера» через мой API. use
— это просто вещь, которая блокирует и закрывает, по идее. Правильный?
Извините за задержку. Да у тебя тут все правильно. Итак, как я сказал в ответе, я думаю, что вы можете либо сохранить читатель как свойство в PipeSeparatedItemsReader
, и сделать класс-оболочку закрывающимся, чтобы закрыть базовый читатель, либо вы можете заставить свой метод принимать лямбда вместо возврата последовательности, чтобы сдвинуть проблема вверх (надеюсь, к месту, где люди контролируют всю жизнь последовательности).
Короче: не могу. Помощники
use*
по своей природе закрывают ресурс в конце своего вызова, а не во время обработки последовательности (как это может звучать в текущем KDoc). Спасибо @Joffrey за разъяснение.