В 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
с отображаемыми значениями.
Вот как работает выражение:
- Выражение
for
начинает перебирать значения в диапазоне(10, 11, 12)
. Сначала оно работает со значением10
, умножает его на2
, затем выдает результат -20
. - Далее берет
11
— второе значение в диапазоне. Умножает его на2
, а затем выдает значение22
. Можно представить эти полученные значения как накопление во временном хранилище. - Наконец, цикл берет число
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:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java