Scala 3 — Book

Структуры управления

Language

В Scala есть все структуры управления, которые вы ожидаете найти в языке программирования, в том числе:

  • if/then/else
  • циклы for
  • циклы while
  • try/catch/finally

Здесь также есть две другие мощные конструкции, которые вы, возможно, не видели раньше, в зависимости от вашего опыта программирования:

  • for выражения (также известные как for comprehensions)
  • match выражения

Все они продемонстрированы в следующих разделах.

Конструкция if/then/else

Однострочный Scala оператор if выглядит так:

if (x == 1) println(x)
if x == 1 then println(x)

Когда необходимо выполнить несколько строк кода после if, используется синтаксис:

if (x == 1) {
  println("x is 1, as you can see:")
  println(x)
}
if x == 1 then
  println("x is 1, as you can see:")
  println(x)

if/else синтаксис выглядит так:

if (x == 1) {
  println("x is 1, as you can see:")
  println(x)
} else {
  println("x was not 1")
}
if x == 1 then
  println("x is 1, as you can see:")
  println(x)
else
  println("x was not 1")

А это синтаксис if/else if/else:

if (x < 0)
  println("negative")
else if (x == 0)
  println("zero")
else
  println("positive")
if x < 0 then
  println("negative")
else if x == 0 then
  println("zero")
else
  println("positive")

Утверждение end if

  Это новое в Scala 3 и не поддерживается в Scala 2.

При желании можно дополнительно включить оператор end if в конце каждого выражения:

if x == 1 then
  println("x is 1, as you can see:")
  println(x)
end if

if/else выражения всегда возвращают результат

Сравнения if/else образуют выражения - это означает, что они возвращают значение, которое можно присвоить переменной. Поэтому нет необходимости в специальном тернарном операторе:

val minValue = if (a < b) a else b
val minValue = if a < b then a else b

Поскольку эти выражения возвращают значение, то выражения if/else можно использовать в качестве тела метода:

def compare(a: Int, b: Int): Int =
  if (a < b)
    -1
  else if (a == b)
    0
  else
    1
def compare(a: Int, b: Int): Int =
  if a < b then
    -1
  else if a == b then
    0
  else
    1

В сторону: программирование, ориентированное на выражения

Кратко о программировании в целом: когда каждое написанное вами выражение возвращает значение, такой стиль называется программированием, ориентированным на выражения, или EOP (expression-oriented programming). Например, это выражение:

val minValue = if (a < b) a else b
val minValue = if a < b then a else b

И наоборот, строки кода, которые не возвращают значения, называются операторами и используются для получения побочных эффектов. Например, эти строки кода не возвращают значения, поэтому они используются для побочных эффектов:

if (a == b) action()
println("Hello")
if a == b then action()
println("Hello")

В первом примере метод action запускается как побочный эффект, когда a равно b. Второй пример используется для побочного эффекта печати строки в STDOUT. Когда вы узнаете больше о Scala, то обнаружите, что пишете больше выражений и меньше операторов.

Циклы for

В самом простом случае цикл for в Scala можно использовать для перебора элементов в коллекции. Например, имея последовательность целых чисел, вы можете перебрать ее элементы и вывести их значения следующим образом:

val ints = Seq(1, 2, 3)
for (i <- ints) println(i)
val ints = Seq(1, 2, 3)
for i <- ints do println(i)

Код i <- ints называется генератором.

Вот как выглядит результат в Scala REPL:

scala> val ints = Seq(1,2,3)
ints: Seq[Int] = List(1, 2, 3)

scala> for (i <- ints) println(i)
1
2
3
scala> val ints = Seq(1,2,3)
ints: Seq[Int] = List(1, 2, 3)

scala> for i <- ints do println(i)
1
2
3

Если вам нужен многострочный блок кода после генератора for, используйте следующий синтаксис:

for (i <- ints) {
  val x = i * 2
  println(s"i = $i, x = $x")
}
for i <- ints
do
  val x = i * 2
  println(s"i = $i, x = $x")

Несколько генераторов

Циклы for могут иметь несколько генераторов, как показано в этом примере:

for {
  i <- 1 to 2
  j <- 'a' to 'b'
  k <- 1 to 10 by 5
} {
  println(s"i = $i, j = $j, k = $k")
}
for
  i <- 1 to 2
  j <- 'a' to 'b'
  k <- 1 to 10 by 5
do
  println(s"i = $i, j = $j, k = $k")

Это выражение выводит следующее:

i = 1, j = a, k = 1
i = 1, j = a, k = 6
i = 1, j = b, k = 1
i = 1, j = b, k = 6
i = 2, j = a, k = 1
i = 2, j = a, k = 6
i = 2, j = b, k = 1
i = 2, j = b, k = 6

“Ограничители”

Циклы for также могут содержать операторы if, известные как ограничители (guards):

for {
  i <- 1 to 5
  if i % 2 == 0
} {
  println(i)
}
for
  i <- 1 to 5
  if i % 2 == 0
do
  println(i)

Результат этого цикла:

2
4

Цикл for может содержать столько стражников, сколько необходимо. В этом примере показан один из способов печати числа 4:

for {
  i <- 1 to 10
  if i > 3
  if i < 6
  if i % 2 == 0
} {
  println(i)
}
for
  i <- 1 to 10
  if i > 3
  if i < 6
  if i % 2 == 0
do
  println(i)

Использование for с Maps

Вы также можете использовать циклы for с Map. Например, если задана такая Map с аббревиатурами штатов и их полными названиями:

val states = Map(
  "AK" -> "Alaska",
  "AL" -> "Alabama", 
  "AR" -> "Arizona"
)

, то можно распечатать ключи и значения, используя for. Например:

for ((abbrev, fullName) <- states) println(s"$abbrev: $fullName")
for (abbrev, fullName) <- states do println(s"$abbrev: $fullName")

Вот как это выглядит в REPL:

scala> for ((abbrev, fullName) <- states) println(s"$abbrev: $fullName")
AK: Alaska
AL: Alabama
AR: Arizona
scala> for (abbrev, fullName) <- states do println(s"$abbrev: $fullName")
AK: Alaska
AL: Alabama
AR: Arizona

Когда цикл for перебирает мапу, каждая пара ключ/значение привязывается к переменным abbrev и fullName, которые находятся в кортеже:

(abbrev, fullName) <- states

По мере выполнения цикла переменная abbrev принимает значение текущего ключа в мапе, а переменная fullName - соответствующему ключу значению.

Выражение for

В предыдущих примерах циклов for все эти циклы использовались для побочных эффектов, в частности для вывода этих значений в STDOUT с помощью println.

Важно знать, что вы также можете создавать выражения for, которые возвращают значения. Вы создаете выражение for, добавляя ключевое слово yield и возвращаемое выражение, например:

val list =
  for (i <- 10 to 12)
  yield i * 2

// list: IndexedSeq[Int] = Vector(20, 22, 24)
val list =
  for i <- 10 to 12
  yield i * 2

// list: IndexedSeq[Int] = Vector(20, 22, 24)

После выполнения этого выражения for переменная list содержит Vector с отображаемыми значениями. Вот как работает выражение:

  1. Выражение for начинает перебирать значения в диапазоне (10, 11, 12). Сначала оно работает со значением 10, умножает его на 2, затем выдает результат - 20.
  2. Далее берет 11 — второе значение в диапазоне. Умножает его на 2, а затем выдает значение 22. Можно представить эти полученные значения как накопление во временном хранилище.
  3. Наконец, цикл берет число 12 из диапазона, умножает его на 2, получая число 24. Цикл завершается в этой точке и выдает конечный результат - Vector(20, 22, 24).

Хотя целью этого раздела является демонстрация выражений for, полезно знать, что показанное выражение for эквивалентно вызову метода map:

val list = (10 to 12).map(i => i * 2)

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

Вот пример, который показывает, как использовать блок кода после yield:

val names = List("_olivia", "_walter", "_peter")

val capNames = for (name <- names) yield { 
  val nameWithoutUnderscore = name.drop(1)
  val capName = nameWithoutUnderscore.capitalize
  capName
}

// capNames: List[String] = List(Olivia, Walter, Peter)
val names = List("_olivia", "_walter", "_peter")

val capNames = for name <- names yield
  val nameWithoutUnderscore = name.drop(1)
  val capName = nameWithoutUnderscore.capitalize
  capName

// capNames: List[String] = List(Olivia, Walter, Peter)

Использование выражения for в качестве тела метода

Поскольку выражение for возвращает результат, его можно использовать в качестве тела метода, который возвращает полезное значение. Этот метод возвращает все значения в заданном списке целых чисел, которые находятся между 3 и 10:

def between3and10(xs: List[Int]): List[Int] =
  for {
    x <- xs
    if x >= 3
    if x <= 10
  } yield x

between3and10(List(1, 3, 7, 11))   // : List[Int] = List(3, 7)
def between3and10(xs: List[Int]): List[Int] =
  for
    x <- xs
    if x >= 3
    if x <= 10
  yield x

between3and10(List(1, 3, 7, 11))   // : List[Int] = List(3, 7)

Циклы while

Синтаксис цикла while в Scala выглядит следующим образом:

var i = 0

while (i < 3) {
  println(i)
  i += 1
}
var i = 0

while i < 3 do
  println(i)
  i += 1

match выражения

Сопоставление с образцом (pattern matching) является основой функциональных языков программирования. Scala включает в себя pattern matching, обладающий множеством возможностей.

В самом простом случае можно использовать выражение match, подобное оператору Java switch, сопоставляя на основе целочисленного значения. Как и предыдущие структуры, сопоставление с образцом - это действительно выражение, поскольку оно вычисляет результат:

// `i` is an integer
val day = i match {
  case 0 => "Sunday"
  case 1 => "Monday"
  case 2 => "Tuesday"
  case 3 => "Wednesday"
  case 4 => "Thursday"
  case 5 => "Friday"
  case 6 => "Saturday"
  case _ => "invalid day"   // по умолчанию, перехватывает остальное
}
// `i` is an integer
val day = i match
  case 0 => "Sunday"
  case 1 => "Monday"
  case 2 => "Tuesday"
  case 3 => "Wednesday"
  case 4 => "Thursday"
  case 5 => "Friday"
  case 6 => "Saturday"
  case _ => "invalid day"   // по умолчанию, перехватывает остальное

В этом примере переменная i сопоставляется с заданными числами. Если она находится между 0 и 6, day принимает значение строки, представляющей день недели. В противном случае она соответствует подстановочному знаку, представленному символом _, и day принимает значение строки "invalid day".

Поскольку сопоставляемые значения рассматриваются в том порядке, в котором они заданы, и используется первое совпадение, случай по умолчанию, соответствующий любому значению, должен идти последним. Любые сопоставляемые случаи после значения по умолчанию будут помечены как недоступные и будет выведено предупреждение.

При написании простых выражений соответствия, подобных этому, рекомендуется использовать аннотацию @switch для переменной i. Эта аннотация содержит предупреждение во время компиляции, если switch не может быть скомпилирован в tableswitch или lookupswitch, которые лучше подходят с точки зрения производительности.

Использование значения по умолчанию

Когда необходимо получить доступ к универсальному значению по умолчанию в match выражении, просто укажите вместо _ имя переменной в левой части оператора case, а затем используйте это имя переменной в правой части оператора при необходимости:

i match {
  case 0 => println("1")
  case 1 => println("2")
  case what => println(s"You gave me: $what")
}
i match
  case 0 => println("1")
  case 1 => println("2")
  case what => println(s"You gave me: $what")

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

val N = 42
i match {
  case 0 => println("1")
  case 1 => println("2")
  case N => println("42")
  case n => println(s"You gave me: $n" )
}
val N = 42
i match
  case 0 => println("1")
  case 1 => println("2")
  case N => println("42")
  case n => println(s"You gave me: $n" )

Если i равно 42, то оно будет соответствовать case N и напечатает строку "42". И не достигнет случая по умолчанию.

Обработка нескольких возможных совпадений в одной строке

Как уже упоминалось, match выражения многофункциональны. В этом примере показано, как в каждом операторе case использовать несколько возможных совпадений:

val evenOrOdd = i match {
  case 1 | 3 | 5 | 7 | 9 => println("odd")
  case 2 | 4 | 6 | 8 | 10 => println("even")
  case _ => println("some other number")
}
val evenOrOdd = i match
  case 1 | 3 | 5 | 7 | 9 => println("odd")
  case 2 | 4 | 6 | 8 | 10 => println("even")
  case _ => println("some other number")

Использование if стражников в case предложениях

В case выражениях также можно использовать стражников. В этом примере второй и третий case используют стражников для сопоставления нескольких целочисленных значений:

i match {
  case 1 => println("one, a lonely number")
  case x if x == 2 || x == 3 => println("two’s company, three’s a crowd")
  case x if x > 3 => println("4+, that’s a party")
  case _ => println("i’m guessing your number is zero or less")
}
i match
  case 1 => println("one, a lonely number")
  case x if x == 2 || x == 3 => println("two’s company, three’s a crowd")
  case x if x > 3 => println("4+, that’s a party")
  case _ => println("i’m guessing your number is zero or less")

Вот еще один пример, который показывает, как сопоставить заданное значение с диапазоном чисел:

i match {
  case a if 0 to 9 contains a => println(s"0-9 range: $a")
  case b if 10 to 19 contains b => println(s"10-19 range: $b")
  case c if 20 to 29 contains c => println(s"20-29 range: $c")
  case _ => println("Hmmm...")
}
i match
  case a if 0 to 9 contains a => println(s"0-9 range: $a")
  case b if 10 to 19 contains b => println(s"10-19 range: $b")
  case c if 20 to 29 contains c => println(s"20-29 range: $c")
  case _ => println("Hmmm...")

Case классы и сопоставление с образцом

Вы также можете извлекать поля из case классов — и классов, которые имеют корректно написанные методы apply/unapply — и использовать их в своих условиях. Вот пример использования простого case класса Person:

case class Person(name: String)

def speak(p: Person) = p match {
  case Person(name) if name == "Fred" => println(s"$name says, Yubba dubba doo")
  case Person(name) if name == "Bam Bam" => println(s"$name says, Bam bam!")
  case _ => println("Watch the Flintstones!")
}

speak(Person("Fred"))      // "Fred says, Yubba dubba doo"
speak(Person("Bam Bam"))   // "Bam Bam says, Bam bam!"
case class Person(name: String)

def speak(p: Person) = p match
  case Person(name) if name == "Fred" => println(s"$name says, Yubba dubba doo")
  case Person(name) if name == "Bam Bam" => println(s"$name says, Bam bam!")
  case _ => println("Watch the Flintstones!")

speak(Person("Fred"))      // "Fred says, Yubba dubba doo"
speak(Person("Bam Bam"))   // "Bam Bam says, Bam bam!"

Использование match выражения в качестве тела метода

Поскольку match выражения возвращают значение, их можно использовать в качестве тела метода. Метод isTruthy принимает в качестве входного параметра значение Matchable и возвращает Boolean на основе сопоставления с образцом:

def isTruthy(a: Matchable) = a match {
  case 0 | "" | false => false
  case _              => true
}
def isTruthy(a: Matchable) = a match
  case 0 | "" | false => false
  case _              => true

Входной параметр a имеет тип Matchable, являющийся основой всех типов Scala, для которых может выполняться сопоставление с образцом. Метод реализован путем сопоставления на входе в метод, обрабатывая два варианта: первый проверяет, является ли заданное значение целым числом 0, пустой строкой или false и в этом случае возвращается false. Для любого другого значения в случае по умолчанию мы возвращаем true. Примеры ниже показывают, как работает этот метод:

isTruthy(0)      // false
isTruthy(false)  // false
isTruthy("")     // false
isTruthy(1)      // true
isTruthy(" ")    // true
isTruthy(2F)     // true

Использование сопоставления с образцом в качестве тела метода очень распространено.

Использование различных шаблонов в сопоставлении с образцом

Для выражения match можно использовать множество различных шаблонов. Например:

  • Сравнение с константой (такое как case 3 =>)
  • Сравнение с последовательностями (такое как case List(els : _*) =>)
  • Сравнение с кортежами (такое как case (x, y) =>)
  • Сравнение с конструктором класса (такое как case Person(first, last) =>)
  • Сравнение по типу (такое как case p: Person =>)

Все эти виды шаблонов показаны в следующем методе pattern, который принимает входной параметр типа Matchable и возвращает String:

def pattern(x: Matchable): String = x match {

  // сравнение с константой
  case 0 => "zero"
  case true => "true"
  case "hello" => "you said 'hello'"
  case Nil => "an empty List"

  // сравнение с последовательностями
  case List(0, _, _) => "a 3-element list with 0 as the first element"
  case List(1, _*) => "list, starts with 1, has any number of elements"
  case Vector(1, _*) => "vector, starts w/ 1, has any number of elements"

  // сравнение с кортежами
  case (a, b) => s"got $a and $b"
  case (a, b, c) => s"got $a, $b, and $c"

  // сравнение с конструктором класса
  case Person(first, "Alexander") => s"Alexander, first name = $first"
  case Dog("Zeus") => "found a dog named Zeus"

  // сравнение по типу
  case s: String => s"got a string: $s"
  case i: Int => s"got an int: $i"
  case f: Float => s"got a float: $f"
  case a: Array[Int] => s"array of int: ${a.mkString(",")}"
  case as: Array[String] => s"string array: ${as.mkString(",")}"
  case d: Dog => s"dog: ${d.name}"
  case list: List[?] => s"got a List: $list"
  case m: Map[?, ?] => m.toString

  // значение по умолчанию с подстановочным знаком
  case _ => "Unknown"
}
def pattern(x: Matchable): String = x match

  // сравнение с константой
  case 0 => "zero"
  case true => "true"
  case "hello" => "you said 'hello'"
  case Nil => "an empty List"

  // сравнение с последовательностями
  case List(0, _, _) => "a 3-element list with 0 as the first element"
  case List(1, _*) => "list, starts with 1, has any number of elements"
  case Vector(1, _*) => "vector, starts w/ 1, has any number of elements"

  // сравнение с кортежами
  case (a, b) => s"got $a and $b"
  case (a, b, c) => s"got $a, $b, and $c"

  // сравнение с конструктором класса
  case Person(first, "Alexander") => s"Alexander, first name = $first"
  case Dog("Zeus") => "found a dog named Zeus"

  // сравнение по типу
  case s: String => s"got a string: $s"
  case i: Int => s"got an int: $i"
  case f: Float => s"got a float: $f"
  case a: Array[Int] => s"array of int: ${a.mkString(",")}"
  case as: Array[String] => s"string array: ${as.mkString(",")}"
  case d: Dog => s"dog: ${d.name}"
  case list: List[?] => s"got a List: $list"
  case m: Map[?, ?] => m.toString

  // значение по умолчанию с подстановочным знаком
  case _ => "Unknown"

try/catch/finally

Как и в Java, в Scala есть конструкция try/catch/finally, позволяющая перехватывать исключения и управлять ими. Для обеспечения согласованности Scala использует тот же синтаксис, что и выражения match, и поддерживает сопоставление с образцом для различных возможных исключений.

В следующем примере openAndReadAFile - это метод, который выполняет то, что следует из его названия: он открывает файл и считывает из него текст, присваивая результат изменяемой переменной text:

var text = ""
try {
  text = openAndReadAFile(filename)
} catch {
  case fnf: FileNotFoundException => fnf.printStackTrace()
  case ioe: IOException => ioe.printStackTrace()
} finally {
  // здесь необходимо закрыть ресурсы
  println("Came to the 'finally' clause.")
}
var text = ""
try
  text = openAndReadAFile(filename)
catch
  case fnf: FileNotFoundException => fnf.printStackTrace()
  case ioe: IOException => ioe.printStackTrace()
finally
  // здесь необходимо закрыть ресурсы
  println("Came to the 'finally' clause.")

Предполагая, что метод openAndReadAFile использует Java java.io.* классы для чтения файла и не перехватывает его исключения, попытка открыть и прочитать файл может привести как к FileNotFoundException, так и к IOException, и эти два исключения перехватываются в блоке catch этого примера.

Contributors to this page: