Есть ли способ обработать последний случай по-разному в цикле Scala for?

Например, предположим, что у меня

  for (line <- myData) {
    println("}, {")
  }

Есть ли способ получить последнюю строку для печати

println("}")

Ответы

Ответ 1

Прежде чем продолжить, я рекомендую вам избегать println для понимания. Иногда это может быть полезно для отслеживания ошибки, которая встречается в середине коллекции, но в противном случае приводит к созданию кода, который сложнее реорганизовать и протестировать.

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

for (line <- myData) {
  println("}, {")
}

Вы можете написать:

val lines = for (line <- myData) yield "}, {"
println(lines mkString "\n")

Я также собираюсь предположить, что вы хотите, чтобы содержимое каждой строки выводилось!

val lines = for (line <- myData) yield (line + "}, {")
println(lines mkString "\n")

Хотя вам было бы еще лучше, если бы вы просто использовали mkString напрямую - для чего это!

val lines = myData.mkString("{", "\n}, {", "}")
println(lines)

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

Ответ 2

Можете ли вы реорганизовать свой код, чтобы воспользоваться встроенным mkString?

scala> List(1, 2, 3).mkString("{", "}, {", "}")
res1: String = {1}, {2}, {3}

Ответ 3

Я полностью согласен с тем, что было сказано ранее об использовании mkstring, и отличая первую итерацию, а не последнюю. Вы все еще должны были бы различать последние, scala коллекции имеют метод init, который возвращает все элементы, кроме последнего. Таким образом, вы можете сделать

for(x <- coll.init) workOnNonLast(x)
workOnLast(coll.last)

(init и last являются своего рода противоположностью головы и хвоста, которые являются первыми и все, кроме первого). Обратите внимание, однако, что они могут быть дорогостоящими, чем в зависимости от структуры. На Vector все они быстры. На List, в то время как голова и хвост в основном свободны, init и last являются линейными по длине списка. headOption и lastOption могут помочь вам, когда коллекция может быть пустой, заменив workOnlast на

for (x <- coll.lastOption) workOnLast(x)

Ответ 4

В качестве примера вы можете использовать addString функцию TraversableOnce.

def addString(b: StringBuilder, start: String, sep: String, end: String): StringBuilder = {
  var first = true

  b append start   
  for (x <- self) {
    if (first) {
      b append x
      first = false
    } else {
      b append sep
      b append x
    }
  }
  b append end

  b
}

В вашем случае разделитель }, {, а конец }

Ответ 5

Если вы не хотите использовать встроенную функцию mkString, вы можете сделать что-то вроде

for (line <- lines)
  if (line == lines.last) println("last")
  else println(line)

ОБНОВЛЕНИЕ: Как didierd, упомянутое в комментариях, это решение неверно, потому что последнее значение может возникать несколько раз, он обеспечивает лучшее решение в ответе .

Это нормально для Vectors, потому что функция last принимает для них "эффективное постоянное время", так как для Lists это занимает линейное время, поэтому вы можете использовать сопоставление с образцом

@tailrec
def printLines[A](l: List[A]) {
  l match {
    case Nil => 
    case x :: Nil => println("last")
    case x :: xs => println(x); printLines(xs)
  }
}

Ответ 6

Другие ответы по праву указаны на mkString, и для нормального объема данных я также использовал бы это.

Однако mkString создает (накапливает) конечный результат в памяти через StringBuilder. Это не всегда желательно, в зависимости от объема данных, которые у нас есть.

В этом случае, если все, что мы хотим, это "распечатать", нам не нужно сначала строить большой результат (и, возможно, мы даже хотим избежать этого).

Рассмотрим реализацию этой вспомогательной функции:

def forEachIsLast[A](iterator: Iterator[A])(operation: (A, Boolean) => Unit): Unit = {
  while(iterator.hasNext) {
    val element = iterator.next()
    val isLast = !iterator.hasNext // if there is no "next", this is the last one
    operation(element, isLast)
  }
}    

Он выполняет итерацию по всем элементам и вызывает operation передачу каждого элемента поочередно с булевым значением. Значение true, если прошедший элемент является последним.

В вашем случае его можно использовать следующим образом:

forEachIsLast(myData) { (line, isLast) =>
  if(isLast)
    println("}")
  else
    println("}, {")
}

У нас есть следующие преимущества:

  • Он работает по каждому элементу один за другим, не обязательно накапливая результат в памяти (если вы этого не хотите).
  • Поскольку ему не нужно загружать всю коллекцию в память, чтобы проверить ее размер, достаточно спросить Итератора, если он исчерпан или нет. Вы можете читать данные из большого файла или из сети и т.д.