Как я могу избежать изменяемых переменных в Scala при использовании ZipInputStreams и ZipOutpuStreams?

Я пытаюсь прочитать zip-файл, проверить, что он имеет некоторые необходимые файлы, и затем выпишите все правильные файлы к другому zip-файлу. Основное введение в java.util.zip имеет много измов Java, и я хотел бы сделать свой код более Scala-собственным. А именно, я хотел бы избежать использования vars. Вот то, что я имею:

val fos = new FileOutputStream("new.zip");
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos));

while (zipIn.available == 1) {
  val entry = zipIn.getNextEntry
  if (entryIsValid(entry)) {
    zipOut.putNewEntry(new ZipEntry("subdir/" + entry.getName())
    // read data into the data Array
    var data = Array[Byte](1024)
    var count = zipIn.read(data, 0, 1024)
    while (count != -1) {
      zipOut.write(data, 0, count)
      count = zipIn.read(data, 0, 1024)
    }
  }
  zipIn.close
}
zipOut.close

Я должен добавить, что использую Scala 2.7.7.

16
задан pr1001 17 May 2010 в 14:48
поделиться

4 ответа

d Я не думаю, что есть что-то особенно плохое в использовании классов Java, предназначенных для работы в императивном стиле, в том стиле, в котором они были разработаны. Идиоматическая Scala включает в себя возможность использовать идиоматическую Java так, как она была задумана, даже если стили немного противоречат друг другу.

Однако, если вы хотите - возможно, в качестве упражнения, а возможно, потому что это немного проясняет логику - сделать это более функциональным способом без var, вы можете это сделать. В 2.8 это особенно хорошо, поэтому, даже если вы используете 2.7.7, я дам ответ по 2.8.

Во-первых, нам нужно поставить задачу, что вы сделали не полностью, но давайте предположим, что у нас есть что-то вроде этого:

import java.io._
import java.util.zip._
import scala.collection.immutable.Stream

val fos = new FileOutputStream("new.zip")
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos))
val zipIn = new ZipInputStream(new FileInputStream("old.zip"))
def entryIsValid(ze: ZipEntry) = !ze.isDirectory

Теперь, учитывая это, мы хотим скопировать zip-файл. Трюк, который мы можем использовать - это метод continually в collection.immutable.Stream. Он выполняет цикл с ленивой оценкой. Затем вы можете взять и отфильтровать результаты, чтобы завершить и обработать то, что вам нужно. Это удобный шаблон для использования, когда у вас есть что-то, что вы хотите сделать итератором, но это не так. (Если элемент обновляется сам, вы можете использовать .iterate в Iterable или Iterator- это обычно даже лучше). Вот приложение к этому случаю, используемое дважды: один раз для получения записей, а другой - для чтения/записи фрагментов данных:

val buffer = new Array[Byte](1024)
Stream.continually(zipIn.getNextEntry).
  takeWhile(_ != null).filter(entryIsValid).
  foreach(entry => {
    zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName))
    Stream.continually(zipIn.read(buffer)).takeWhile(_ != -1).
      foreach(count => zipOut.write(buffer,0,count))
  })
}
zipIn.close
zipOut.close

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

На всякий случай, если это непонятно, давайте распакуем одно из применений continually.

Stream.continually(zipIn.read(buffer))

Это просит продолжать вызывать zipIn.read(buffer) столько раз, сколько необходимо, сохраняя целое число, которое получается в результате.

.takeWhile(_ != -1)

Это указывает, сколько раз необходимо, возвращая поток неопределенной длины, который завершится, когда достигнет -1.

.foreach(count => zipOut.write(buffer,0,count))

Это обрабатывает поток, принимая каждый элемент по очереди (счетчик) и используя его для записи в буфер. Это работает немного коварно, поскольку вы полагаетесь на тот факт, что zipIn только что был вызван для получения следующего элемента потока - если бы вы попытались сделать это снова, а не за один проход по потоку, это бы провалилось, поскольку buffer был бы перезаписан. Но здесь все в порядке.

Итак, вот он: немного более компактный, возможно, более простой для понимания, возможно, менее простой для понимания метод, который является более функциональным (хотя побочных эффектов по-прежнему много). В 2.7.7, напротив, я бы сделал это на Java, потому что Stream.continually недоступен, а накладные расходы на создание пользовательского Iterator не стоят того для данного случая. (Однако это стоило бы сделать, если бы я собирался обрабатывать больше zip-файлов и мог повторно использовать код.)


Edit: Метод поиска доступных к переходу к нулю является несколько нечетким для обнаружения конца zip-файла. Я думаю, что "правильный" способ - это ждать, пока вы не получите null обратно от getNextEntry. Учитывая это, я отредактировал предыдущий код (там был takeWhile(_ => zipIn.available==1), который теперь является takeWhile(_ != null)) и предоставил версию 2.7. 7 версию на основе итераторов (обратите внимание, насколько мал главный цикл, как только вы пройдете через работу по определению итераторов, которые, по общему признанию, используют переменные):

val buffer = new Array[Byte](1024)
class ZipIter(zis: ZipInputStream) extends Iterator[ZipEntry] {
  private var entry:ZipEntry = zis.getNextEntry
  private var cached = true
  private def cache { if (entry != null && !cached) {
    cached = true; entry = zis.getNextEntry
  }}
  def hasNext = { cache; entry != null }
  def next = {
    if (!cached) cache
    cached = false
    entry
  }
}
class DataIter(is: InputStream, ab: Array[Byte]) extends Iterator[(Int,Array[Byte])] {
  private var count = 0
  private var waiting = false
  def hasNext = { 
    if (!waiting && count != -1) { count = is.read(ab); waiting=true }
    count != -1
  }
  def next = { waiting=false; (count,ab) }
}
(new ZipIter(zipIn)).filter(entryIsValid).foreach(entry => {
  zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName))
  (new DataIter(zipIn,buffer)).foreach(cb => zipOut.write(cb._2,0,cb._1))
})
zipIn.close
zipOut.close
34
ответ дан 30 November 2019 в 16:18
поделиться

Использование scala2.8 и хвостового рекурсивного вызова:

def copyZip(in: ZipInputStream, out: ZipOutputStream, bufferSize: Int = 1024) {
  val data = new Array[Byte](bufferSize)

  def copyEntry() {
    in getNextEntry match {
      case null =>
      case entry => {
        if (entryIsValid(entry)) {
          out.putNextEntry(new ZipEntry("subdir/" + entry.getName()))

          def copyData() {
            in read data match {
              case -1 =>
              case count => {
                out.write(data, 0, count)
                copyData()
              }
            }
          }
          copyData()
        }
        copyEntry()
      }
    }
  }
  copyEntry()
}
2
ответ дан 30 November 2019 в 16:18
поделиться

Я бы попробовал что-то вроде этого (да, практически та же идея, что и у sblundy):

Iterator.continually {
  val data = new Array[Byte](100)
  zipIn.read(data) match {
    case -1 => Array.empty[Byte]
    case 0  => new Array[Byte](101) // just to filter it out
    case n  => java.util.Arrays.copyOf(data, n)
  }
} filter (_.size != 101) takeWhile (_.nonEmpty)

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

Iterator.continually {
  val data = new Array[Byte](100)
  zipIn.read(data) match {
    case -1 => new Array[Byte](101)
    case n  => java.util.Arrays.copyOf(data, n)
  }
} takeWhile (_.size != 101)
2
ответ дан 30 November 2019 в 16:18
поделиться

Без хвостовой рекурсии я бы избегал рекурсии. Вы рискуете получить переполнение стека. Вы можете обернуть zipIn.read(data) в scala.BufferedIterator[Byte] и идти оттуда.

1
ответ дан 30 November 2019 в 16:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: