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

Я пытаюсь прочитать zip файл, проверить, что у него есть необходимые файлы, а затем записать все действительные файлы в другой zip файл. базовое введение в java.util.zip содержит много Java-измов, и мне бы хотелось сделать мой код более Scala -native, В частности, я хотел бы избежать использования 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.

Ответ 1

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

Однако, если вы хотите - возможно, как упражнение, или, возможно, потому, что он немного разъясняет логику, - чтобы сделать это более функциональным способом без var-free, вы можете это сделать. В 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 и мог повторно использовать код.)


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

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

Ответ 2

Использование 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()
}

Ответ 3

Я бы попробовал что-то вроде этого (да, в значительной степени та же идея 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)

Ответ 4

На основе http://harrah.github.io/browse/samples/compiler/scala/tools/nsc/io/ZipArchive.scala.html:

private[io] class ZipEntryTraversableClass(in: InputStream) extends Traversable[ZipEntry] {
  val zis = new ZipInputStream(in)

  def foreach[U](f: ZipEntry => U) {
    @tailrec
    def loop(x: ZipEntry): Unit = if (x != null) {
      f(x)
      zis.closeEntry()
      loop(zis.getNextEntry())
    }
    loop(zis.getNextEntry())
  }

  def writeCurrentEntryTo(os: OutputStream) {
    IOUtils.copy(zis, os)
  }
}

Ответ 5

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