Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 723
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 7.2. Вычисление наибольшего общего делителя с применением цикла while def gcdLoop(x: Long, y: Long): Long =
var a = x var b = y while a != 0 do val temp = a a = b % a b = temp b
148 Глава 7 • Встроенные управляющие конструкции
Конструкция while называется циклом, а не выражением, потому что она не возвращает значение. Типом результата является
Unit
. Получается так, что фактически существует только одно значение, имеющее тип
Unit
. Оно на
зывается unitзначением и записывается как
()
. Существование
()
отличает имеющийся в Scala класс
Unit от используемого в Java типа void
. Попробуйте сделать это в REPL:
scala> def greet() = println("hi")
def greet(): Unit scala> val iAmUnit = greet() == ()
hi val iAmUnit: Boolean = true
Поскольку тип выражения println("hi")
определяется как
Unit
, то greet определяется как процедура с результирующим типом
Unit
. Поэтому greet возвращает
()
. Это подтверждается в следующей строке, где переменная iAmUnit возвращает true
, потому что результат greet равен
()
Начиная с третьего выпуска, Scala больше не предлагает цикл do-while
, в котором условие проверялось после тела цикла, а не до него. Вместо этого вы можете поместить операторы тела цикла первыми после while
, закончить логическим условием и затем поставить do()
. В листинге 7.3 показан Scala
скрипт, использующий этот подход для отображения строк, считанных из стандартного ввода, до тех пор, пока не будет введена пустая строка.
Листинг 7.3. Выполнение тела цикла хотя бы один раз без do-while import scala.io.StdIn.readLine while val line = readLine()
println(s"Read: $line")
line != ""
do ()
Еще одна уместная здесь конструкция, приводящая к unitзначению, — это переназначение var
. Например, если вы попытаетесь прочитать строки в Scala, используя следующую идиому цикла while из Java (а также C и C++), вы столкнетесь с проблемой:
var line = "" // Этот код не скомпилируется!
while (line = scala.io.StdIn.readLine()) != "" do println(s"Read: $line")
Если вы попытаетесь скомпилировать этот код, Scala выдаст вам ошибку о том, что вы не можете сравнивать значения типа
Unit и
String
, исполь
зуя
!=
. В то время как в Java присваивание приводит к присваиваемому
7 .2 . Циклы while 149
значению (в данном случае это строка из стандартного ввода), в Scala при
сваивание всегда приводит к unitзначению
()
. Таким образом, значение присваивания "line
=
readLine()"
всегда будет
()
и никогда не будет ""
В результате условие этого цикла while никогда не было бы ложным и, сле
довательно, цикл никогда не завершался бы.
В результате цикла while никакое значение не возвращается, поэтому за
частую его не включают в чисто функциональные языки. В таких языках имеются выражения, а не циклы. И тем не менее цикл while включен в Scala, поскольку иногда императивные решения бывает легче читать, особенно тем программистам, которые много работали с императивными языками. Напри
мер, если нужно закодировать алгоритм, повторяющий процесс до тех пор, пока не изменятся какиелибо условия, то цикл while позволяет выразить это напрямую, в то время как функциональную альтернативу наподобие ис
пользования рекурсии читателям кода распознавать оказывается сложнее.
Например, в листинге 7.4 показан альтернативный способ определения наи
большего общего знаменателя двух чисел
1
. При условии присваивания x
и y
в функции gcd таких же значений, как и в функции gcdLoop
, показанной в листинге 7.2, будет выдан точно такой же результат. Разница между этими двумя подходами состоит в том, что функция gcdLoop написана в импера
тивном стиле с использованием var
переменных и цикла while
, а функция gcd
— в более функциональном стиле с применением рекурсии (
gcd вызывает саму себя), для чего не нужны var
переменные.
Листинг 7.4. Вычисление наибольшего общего делителя с применением рекурсии def gcd(x: Long, y: Long): Long =
if y == 0 then x else gcd(y, x % y)
В целом мы рекомендуем относиться к циклам while в своем коде с огляд
кой, как и к использованию в нем var
переменных. Фактически циклы while и var
переменные зачастую идут рука об руку. Поскольку циклы не дают результата в виде значения, то для внесения в программу какихлибо измене
ний цикл while обычно будет нуждаться либо в обновлении var
переменных, либо в выполнении вводавывода. В этом можно убедиться, посмотрев на ра
боту показанного ранее примера gcdLoop
. По мере выполнения своей задачи цикл while обновляет значения var
переменных a
и b
. Поэтому мы советуем
1
Здесь в функции gcd используется точно такой же подход, который применялся в одноименной функции в листинге 6.3 в целях вычисления наибольших общих делителей для класса
Rational
. Основное отличие заключается в том, что вместо
Int
значений gcd код работает с
Long
значениями.
150 Глава 7 • Встроенные управляющие конструкции проявлять особую осмотрительность при использовании в коде циклов while
Если нет достаточных оснований для применения цикла while или do-while
, то попробуйте найти способ сделать то же самое без их участия.
7 .3 . Выражения for
Используемые в Scala выражения for являются для итераций чемто напо
добие швейцарского армейского ножа. Чтобы можно было реализовать ши
рокий спектр итераций, эти выражения позволяют различными способами составлять комбинации из довольно простых ингредиентов. Простое приме
нение дает возможность решать простые задачи вроде поэлементного обхода последовательности целых чисел. Более сложные выражения могут выпол
нять обход элементов нескольких коллекций различных видов, фильтровать элементы на основе произвольных условий и создавать новые коллекции.
Обход элементов коллекций
Самое простое, что можно сделать с выражением for
, — это выполнить обход всех элементов коллекции. Например, в листинге 7.5 показан код, который выводит имена всех файлов, содержащихся в текущем каталоге. Вводвывод выполняется с помощью API Java. Сначала в текущем каталоге,
"."
, соз
дается объект java.io.File и вызывается его метод listFiles
. Последний возвращает массив объектов
File
, по одному на каталог или файл, содержа
щийся в текущем каталоге. Получившийся в результате массив сохраняется в переменной filesHere
Листинг 7.5. Получение списка файлов в каталоге с применением выражения for val filesHere = (new java.io.File(".")).listFiles for file <- filesHere do println(file)
С помощью синтаксиса file
<–
filesHere
, называемого генератором, выпол
няется обход элементов массива filesHere
. При каждой итерации значением элемента инициализируется новая val
переменная по имени file
. Компиля
тор приходит к выводу, что типом file является
File
, поскольку filesHere имеет тип
Array[File]
. Для каждой итерации выполняется тело выражения for
, имеющее код println(file)
. Метод toString
, определенный в классе
File
, выдает имя файла или каталога, поэтому будут выведены имена всех файлов и каталогов текущего каталога.
7 .3 . Выражения for 151
Синтаксис выражения for работает не только с массивами, но и с коллек
циями любого типа
1
. Особый и весьма удобный случай — применение типа
Range
, который был упомянут в табл. 5.4. Можно создавать объекты
Range
, используя синтаксис вида
1
to
5
, и выполнять их обход с помощью for
. Про
стой пример имеет следующий вид:
scala> for i <- 1 to 4 do println(s"Iteration $i")
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Если не нужно включать верхнюю границу диапазона в перечисляемые зна
чения, то вместо to используется until
:
scala> for i <- 1 until 4 do println(s"Iteration $i")
Iteration 1
Iteration 2
Iteration 3
Перебор целых чисел наподобие этого встречается в Scala довольно часто, но намного реже, чем в других языках, где данным свойством можно вос
пользоваться для перебора элементов массива:
// В Scala такой код встречается довольно редко...
for i <- 0 to filesHere.length — 1 do println(filesHere(i))
В это выражение for введена переменная i
, которой по очереди присваива
ется каждое целое число от
0
до filesHere.length
—
1
, и для каждой уста
новки i
выполняется тело выражения. Для каждого значения i
из массива filesHere извлекается и обрабатывается i
й элемент.
Такого вида итерации меньше распространены в Scala потому, что есть возможность выполнить непосредственный обход элементов коллекции.
При этом код становится короче и исключаются многие ошибки смеще
ния на единицу, которые могут возникнуть при обходе элементов массива.
С чего нужно начинать — с 0 или 1? Что нужно прибавлять к завершающему
1
Точнее, выражение справа от символа
<–
в выражении for может быть любого типа, имеющего определенные методы (в данном случае foreach
) с соответствующими сигнатурами. Подробная информация о том, как компилятор Scala обрабатывает выражения for
, дана в главе 2.
152 Глава 7 • Встроенные управляющие конструкции индексу, –1, +1 или вообще ничего? На подобные вопросы ответить неслож
но, но также просто дать и неверный ответ. Безопаснее их вообще исключить.
Фильтрация
Иногда перебирать коллекцию целиком не нужно, а требуется отфильтровать ее в некое подмножество. В выражении for это можно сделать путем добавле
ния фильтра в виде условия if
, указанного в выражении for внутри круглых скобок. Например, код, показанный в листинге 7.6, выводит список только тех файлов текущего каталога, имена которых заканчиваются на
.scala
Листинг 7.6. Поиск файлов с расширением .scala с помощью for с фильтром val filesHere = (new java.io.File(".")).listFiles for file <- filesHere if file.getName.endsWith(".scala") do println(file)
Для достижения той же цели можно применить альтернативный вариант:
for file <- filesHere do if file.getName.endsWith(".scala") then println(file)
Этот код выдает на выходе то же самое, что и предыдущий, и выглядит, веро
ятно, более привычно для программистов с опытом работы на императивных языках. Но императивная форма — только вариант, поскольку данное выраже
ние for выполняется в целях получения побочных эффектов, выражающихся в выводе данных, и выдает результат в виде
Unit
значения
()
. Чуть позже в этом разделе будет показано, что for называется выражением, так как по итогу его выполнения получается представляющий интерес результат, то есть коллекция, чей тип определяется компонентами
<–
выражения for
Если потребуется, то в выражение можно включить еще больше фильтров.
В него просто нужно добавлять условия if
. Например, чтобы обеспечить безопасность, код в листинге 7.7 выводит только файлы, исключая катало
ги. Для этого добавляется фильтр, который проверяет имеющийся у файла метод isFile
Листинг 7.7. Использование в выражении for нескольких фильтров for file <– filesHere if file.isFile if file.getName.endsWith(".scala")
do println(file)
7 .3 . Выражения for 153
Вложенные итерации
Если добавить несколько операторов
<–
, то будут получены вложенные ци
клы. Например, выражение for
, показанное в листинге 7.8, имеет два таких цикла. Внешний перебирает элементы массива filesHere
, а внутренний — элементы fileLines(file)
для каждого файла, имя которого заканчивается на
.scala
Листинг 7.8. Использование в выражении for нескольких генераторов def fileLines(file: java.io.File) =
scala.io.Source.fromFile(file).getLines().toArray def grep(pattern: String) =
for file <- filesHere if file.getName.endsWith(".scala")
line <- fileLines(file)
if line.trim.matches(pattern)
do println(s"$file: ${line.trim}")
grep(".*gcd.*")
Привязки промежуточных переменных
Обратите внимание на повторение в предыдущем коде выражения line.trim
Данное вычисление довольно сложное, и потому выполнить его лучше один раз. Сделать это позволяет привязка результата к новой переменной с по
мощью знака равенства (
=
). Связанная переменная вводится и используется точно так же, как и val
переменная, но ключевое слово val не ставится. При
мер показан в листинге 7.9.
Листинг 7.9. Промежуточное присваивание в выражении for def grep(pattern: String) =
for file <- filesHere if file.getName.endsWith(".scala")
line <- fileLines(file)
trimmed = line.trim if trimmed.matches(pattern)
do println(s"$file: $trimmed")
grep(".*gcd.*")
В листинге 7.9 переменная по имени trimmed вводится в ходе выполнения вы
ражения for
. Ее инициализирует результат вызова метода line.trim
. Затем остальная часть кода выражения for использует новую переменную в двух местах: в выражении if и в методе println
154 Глава 7 • Встроенные управляющие конструкции
Создание новой коллекции
Хотя во всех рассмотренных до сих пор примерах вы работали со значения
ми, получаемыми при обходе элементов, после чего о них уже не вспоминали, у вас есть возможность создать значение, чтобы запомнить результат каждой итерации.
Для этого нужно, как описано в шаге 12 главы 3, перед телом выражения for поставить ключевое слово yield
. Рассмотрим, к примеру, функцию, которая определяет файлы с расширением
.scala и сохраняет их имена в массиве:
def scalaFiles =
for file <- filesHere if file.getName.endsWith(".scala")
yield file
При каждом выполнении тела выражения for создается одно значение, в дан
ном случае это просто file
. Когда выполнение выражения for завершится, результат будет включать все выданные значения, содержащиеся в единой коллекции. Тип получающейся коллекции задается на основе вида коллек
ции, обрабатываемой операторами итерации. В данном случае результат будет иметь тип
Array[File]
, поскольку filesHere является массивом, а вы
даваемые выражением значения относятся к типу
File
В качестве другого примера выражение for
, показанное в листинге 7.10, сначала преобразует объект типа
Array[File]
по имени filesHere
, в котором содержатся имена всех файлов, которые есть в текущем каталоге, в объект, содержащий только имена файлов с расширением
.scala
. Для каждого из элементов создается объект типа
Array[String]
, являющийся результатом выполнения метода fileLines
, определение которого показано в листин
ге 7.8. Каждый элемент этого объекта
Array[String]
содержит одну строку из текущего обрабатываемого файла. Данный объект превращается в другой объект типа
Array[String]
, содержащий только те строки, обработанные методом trim
, которые включают подстроку "for"
. И наконец, для каждого из них выдается целочисленное значение длины. Результатом этого выра
жения for становится объект, имеющий тип
Array[Int]
и содержащий эти значения длины.
Листинг 7.10. Преобразование объекта типа Array[File] в объект типа Array[Int] с помощью выражения for val forLineLengths =
for file <- filesHere if file.getName.endsWith(".scala")
7 .4 . Обработка исключений с помощью выражений try 155
line <- fileLines(file)
trimmed = line.trim if trimmed.matches(".*for.*")
yield trimmed.length
Итак, основные свойства выражения for
, применяемого в Scala, рассмотрены, но мы прошлись по ним слишком поверхностно.
7 .4 . Обработка исключений с помощью выражений try
Исключения в Scala ведут себя практически так же, как во многих других языках. Вместо того чтобы возвращать значение (как это обычно происхо
дит), метод может прервать работу с генерацией исключения. Код, вызвав
ший метод, может либо перехватить и обработать данное исключение, либо прекратить собственное выполнение, при этом передав исключение тому коду, который его вызвал. Исключение распространяется подобным образом, раскручивая стек вызова, до тех пор, пока не встретится обрабатывающий его метод или вообще не останется ни одного метода.
Генерация исключений
Генерация исключений в Scala выглядит так же, как в Java. Создается объект исключения, который затем бросается с помощью ключевого слова throw
:
throw new IllegalArgumentException
Как бы парадоксально это ни звучало, в Scala throw
— это выражение, у ко
торого есть результирующий тип. Рассмотрим пример, где этот тип играет важную роль:
def half(n: Int) =
if n % 2 == 0 then n / 2
else throw new RuntimeException("n must be even")
Здесь получается, что если n
— четное число, то переменная half вернет по
ловину от n
. Если нечетное, то исключение сгенерируется еще до того, как переменная half вернет какоелибо значение. Поэтому генерируемое исклю
чение можно без малейших опасений рассматривать как абсолютно любое
156 Глава 7 • Встроенные управляющие конструкции значение. Любой контекст, который пытается воспользоваться значением, возвращаемым из throw
, никогда не сможет этого сделать, и потому никакого вреда ожидать не приходится.
С технической точки зрения сгенерированное исключение имеет тип
Nothing
. Генерацией исключения можно воспользоваться, даже если оно никогда и ни во что не будет вычислено. Подобные технические нюансы могут показаться несколько странными, но в случаях, подобных преды
дущему примеру, зачастую могут пригодиться. Одно ответвление от if вычисляет значение, а другое выдает исключение и вычисляется в
Nothing
Тогда типом всего выражения if является тип, вычисленный в той ветви, в которой проводилось вычисление. Тип
Nothing дополнительно будет рассмотрен в разделе 17.3.
Перехват исключений
Перехват исключений выполняется с применением синтаксиса, показанного в листинге 7.11. Для выражений catch был выбран синтаксис с прицелом на совместимость с весьма важной частью Scala — сопоставлением с образцом.
Этот механизм — весьма эффективное средство, которое вкратце рассматри
вается в данной главе, а более подробно — в главе 13.
var a = x var b = y while a != 0 do val temp = a a = b % a b = temp b
148 Глава 7 • Встроенные управляющие конструкции
Конструкция while называется циклом, а не выражением, потому что она не возвращает значение. Типом результата является
Unit
. Получается так, что фактически существует только одно значение, имеющее тип
Unit
. Оно на
зывается unitзначением и записывается как
()
. Существование
()
отличает имеющийся в Scala класс
Unit от используемого в Java типа void
. Попробуйте сделать это в REPL:
scala> def greet() = println("hi")
def greet(): Unit scala> val iAmUnit = greet() == ()
hi val iAmUnit: Boolean = true
Поскольку тип выражения println("hi")
определяется как
Unit
, то greet определяется как процедура с результирующим типом
Unit
. Поэтому greet возвращает
()
. Это подтверждается в следующей строке, где переменная iAmUnit возвращает true
, потому что результат greet равен
()
Начиная с третьего выпуска, Scala больше не предлагает цикл do-while
, в котором условие проверялось после тела цикла, а не до него. Вместо этого вы можете поместить операторы тела цикла первыми после while
, закончить логическим условием и затем поставить do()
. В листинге 7.3 показан Scala
скрипт, использующий этот подход для отображения строк, считанных из стандартного ввода, до тех пор, пока не будет введена пустая строка.
Листинг 7.3. Выполнение тела цикла хотя бы один раз без do-while import scala.io.StdIn.readLine while val line = readLine()
println(s"Read: $line")
line != ""
do ()
Еще одна уместная здесь конструкция, приводящая к unitзначению, — это переназначение var
. Например, если вы попытаетесь прочитать строки в Scala, используя следующую идиому цикла while из Java (а также C и C++), вы столкнетесь с проблемой:
var line = "" // Этот код не скомпилируется!
while (line = scala.io.StdIn.readLine()) != "" do println(s"Read: $line")
Если вы попытаетесь скомпилировать этот код, Scala выдаст вам ошибку о том, что вы не можете сравнивать значения типа
Unit и
String
, исполь
зуя
!=
. В то время как в Java присваивание приводит к присваиваемому
7 .2 . Циклы while 149
значению (в данном случае это строка из стандартного ввода), в Scala при
сваивание всегда приводит к unitзначению
()
. Таким образом, значение присваивания "line
=
readLine()"
всегда будет
()
и никогда не будет ""
В результате условие этого цикла while никогда не было бы ложным и, сле
довательно, цикл никогда не завершался бы.
В результате цикла while никакое значение не возвращается, поэтому за
частую его не включают в чисто функциональные языки. В таких языках имеются выражения, а не циклы. И тем не менее цикл while включен в Scala, поскольку иногда императивные решения бывает легче читать, особенно тем программистам, которые много работали с императивными языками. Напри
мер, если нужно закодировать алгоритм, повторяющий процесс до тех пор, пока не изменятся какиелибо условия, то цикл while позволяет выразить это напрямую, в то время как функциональную альтернативу наподобие ис
пользования рекурсии читателям кода распознавать оказывается сложнее.
Например, в листинге 7.4 показан альтернативный способ определения наи
большего общего знаменателя двух чисел
1
. При условии присваивания x
и y
в функции gcd таких же значений, как и в функции gcdLoop
, показанной в листинге 7.2, будет выдан точно такой же результат. Разница между этими двумя подходами состоит в том, что функция gcdLoop написана в импера
тивном стиле с использованием var
переменных и цикла while
, а функция gcd
— в более функциональном стиле с применением рекурсии (
gcd вызывает саму себя), для чего не нужны var
переменные.
Листинг 7.4. Вычисление наибольшего общего делителя с применением рекурсии def gcd(x: Long, y: Long): Long =
if y == 0 then x else gcd(y, x % y)
В целом мы рекомендуем относиться к циклам while в своем коде с огляд
кой, как и к использованию в нем var
переменных. Фактически циклы while и var
переменные зачастую идут рука об руку. Поскольку циклы не дают результата в виде значения, то для внесения в программу какихлибо измене
ний цикл while обычно будет нуждаться либо в обновлении var
переменных, либо в выполнении вводавывода. В этом можно убедиться, посмотрев на ра
боту показанного ранее примера gcdLoop
. По мере выполнения своей задачи цикл while обновляет значения var
переменных a
и b
. Поэтому мы советуем
1
Здесь в функции gcd используется точно такой же подход, который применялся в одноименной функции в листинге 6.3 в целях вычисления наибольших общих делителей для класса
Rational
. Основное отличие заключается в том, что вместо
Int
значений gcd код работает с
Long
значениями.
150 Глава 7 • Встроенные управляющие конструкции проявлять особую осмотрительность при использовании в коде циклов while
Если нет достаточных оснований для применения цикла while или do-while
, то попробуйте найти способ сделать то же самое без их участия.
7 .3 . Выражения for
Используемые в Scala выражения for являются для итераций чемто напо
добие швейцарского армейского ножа. Чтобы можно было реализовать ши
рокий спектр итераций, эти выражения позволяют различными способами составлять комбинации из довольно простых ингредиентов. Простое приме
нение дает возможность решать простые задачи вроде поэлементного обхода последовательности целых чисел. Более сложные выражения могут выпол
нять обход элементов нескольких коллекций различных видов, фильтровать элементы на основе произвольных условий и создавать новые коллекции.
Обход элементов коллекций
Самое простое, что можно сделать с выражением for
, — это выполнить обход всех элементов коллекции. Например, в листинге 7.5 показан код, который выводит имена всех файлов, содержащихся в текущем каталоге. Вводвывод выполняется с помощью API Java. Сначала в текущем каталоге,
"."
, соз
дается объект java.io.File и вызывается его метод listFiles
. Последний возвращает массив объектов
File
, по одному на каталог или файл, содержа
щийся в текущем каталоге. Получившийся в результате массив сохраняется в переменной filesHere
Листинг 7.5. Получение списка файлов в каталоге с применением выражения for val filesHere = (new java.io.File(".")).listFiles for file <- filesHere do println(file)
С помощью синтаксиса file
<–
filesHere
, называемого генератором, выпол
няется обход элементов массива filesHere
. При каждой итерации значением элемента инициализируется новая val
переменная по имени file
. Компиля
тор приходит к выводу, что типом file является
File
, поскольку filesHere имеет тип
Array[File]
. Для каждой итерации выполняется тело выражения for
, имеющее код println(file)
. Метод toString
, определенный в классе
File
, выдает имя файла или каталога, поэтому будут выведены имена всех файлов и каталогов текущего каталога.
7 .3 . Выражения for 151
Синтаксис выражения for работает не только с массивами, но и с коллек
циями любого типа
1
. Особый и весьма удобный случай — применение типа
Range
, который был упомянут в табл. 5.4. Можно создавать объекты
Range
, используя синтаксис вида
1
to
5
, и выполнять их обход с помощью for
. Про
стой пример имеет следующий вид:
scala> for i <- 1 to 4 do println(s"Iteration $i")
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Если не нужно включать верхнюю границу диапазона в перечисляемые зна
чения, то вместо to используется until
:
scala> for i <- 1 until 4 do println(s"Iteration $i")
Iteration 1
Iteration 2
Iteration 3
Перебор целых чисел наподобие этого встречается в Scala довольно часто, но намного реже, чем в других языках, где данным свойством можно вос
пользоваться для перебора элементов массива:
// В Scala такой код встречается довольно редко...
for i <- 0 to filesHere.length — 1 do println(filesHere(i))
В это выражение for введена переменная i
, которой по очереди присваива
ется каждое целое число от
0
до filesHere.length
—
1
, и для каждой уста
новки i
выполняется тело выражения. Для каждого значения i
из массива filesHere извлекается и обрабатывается i
й элемент.
Такого вида итерации меньше распространены в Scala потому, что есть возможность выполнить непосредственный обход элементов коллекции.
При этом код становится короче и исключаются многие ошибки смеще
ния на единицу, которые могут возникнуть при обходе элементов массива.
С чего нужно начинать — с 0 или 1? Что нужно прибавлять к завершающему
1
Точнее, выражение справа от символа
<–
в выражении for может быть любого типа, имеющего определенные методы (в данном случае foreach
) с соответствующими сигнатурами. Подробная информация о том, как компилятор Scala обрабатывает выражения for
, дана в главе 2.
152 Глава 7 • Встроенные управляющие конструкции индексу, –1, +1 или вообще ничего? На подобные вопросы ответить неслож
но, но также просто дать и неверный ответ. Безопаснее их вообще исключить.
Фильтрация
Иногда перебирать коллекцию целиком не нужно, а требуется отфильтровать ее в некое подмножество. В выражении for это можно сделать путем добавле
ния фильтра в виде условия if
, указанного в выражении for внутри круглых скобок. Например, код, показанный в листинге 7.6, выводит список только тех файлов текущего каталога, имена которых заканчиваются на
.scala
Листинг 7.6. Поиск файлов с расширением .scala с помощью for с фильтром val filesHere = (new java.io.File(".")).listFiles for file <- filesHere if file.getName.endsWith(".scala") do println(file)
Для достижения той же цели можно применить альтернативный вариант:
for file <- filesHere do if file.getName.endsWith(".scala") then println(file)
Этот код выдает на выходе то же самое, что и предыдущий, и выглядит, веро
ятно, более привычно для программистов с опытом работы на императивных языках. Но императивная форма — только вариант, поскольку данное выраже
ние for выполняется в целях получения побочных эффектов, выражающихся в выводе данных, и выдает результат в виде
Unit
значения
()
. Чуть позже в этом разделе будет показано, что for называется выражением, так как по итогу его выполнения получается представляющий интерес результат, то есть коллекция, чей тип определяется компонентами
<–
выражения for
Если потребуется, то в выражение можно включить еще больше фильтров.
В него просто нужно добавлять условия if
. Например, чтобы обеспечить безопасность, код в листинге 7.7 выводит только файлы, исключая катало
ги. Для этого добавляется фильтр, который проверяет имеющийся у файла метод isFile
Листинг 7.7. Использование в выражении for нескольких фильтров for file <– filesHere if file.isFile if file.getName.endsWith(".scala")
do println(file)
7 .3 . Выражения for 153
Вложенные итерации
Если добавить несколько операторов
<–
, то будут получены вложенные ци
клы. Например, выражение for
, показанное в листинге 7.8, имеет два таких цикла. Внешний перебирает элементы массива filesHere
, а внутренний — элементы fileLines(file)
для каждого файла, имя которого заканчивается на
.scala
Листинг 7.8. Использование в выражении for нескольких генераторов def fileLines(file: java.io.File) =
scala.io.Source.fromFile(file).getLines().toArray def grep(pattern: String) =
for file <- filesHere if file.getName.endsWith(".scala")
line <- fileLines(file)
if line.trim.matches(pattern)
do println(s"$file: ${line.trim}")
grep(".*gcd.*")
Привязки промежуточных переменных
Обратите внимание на повторение в предыдущем коде выражения line.trim
Данное вычисление довольно сложное, и потому выполнить его лучше один раз. Сделать это позволяет привязка результата к новой переменной с по
мощью знака равенства (
=
). Связанная переменная вводится и используется точно так же, как и val
переменная, но ключевое слово val не ставится. При
мер показан в листинге 7.9.
Листинг 7.9. Промежуточное присваивание в выражении for def grep(pattern: String) =
for file <- filesHere if file.getName.endsWith(".scala")
line <- fileLines(file)
trimmed = line.trim if trimmed.matches(pattern)
do println(s"$file: $trimmed")
grep(".*gcd.*")
В листинге 7.9 переменная по имени trimmed вводится в ходе выполнения вы
ражения for
. Ее инициализирует результат вызова метода line.trim
. Затем остальная часть кода выражения for использует новую переменную в двух местах: в выражении if и в методе println
154 Глава 7 • Встроенные управляющие конструкции
Создание новой коллекции
Хотя во всех рассмотренных до сих пор примерах вы работали со значения
ми, получаемыми при обходе элементов, после чего о них уже не вспоминали, у вас есть возможность создать значение, чтобы запомнить результат каждой итерации.
Для этого нужно, как описано в шаге 12 главы 3, перед телом выражения for поставить ключевое слово yield
. Рассмотрим, к примеру, функцию, которая определяет файлы с расширением
.scala и сохраняет их имена в массиве:
def scalaFiles =
for file <- filesHere if file.getName.endsWith(".scala")
yield file
При каждом выполнении тела выражения for создается одно значение, в дан
ном случае это просто file
. Когда выполнение выражения for завершится, результат будет включать все выданные значения, содержащиеся в единой коллекции. Тип получающейся коллекции задается на основе вида коллек
ции, обрабатываемой операторами итерации. В данном случае результат будет иметь тип
Array[File]
, поскольку filesHere является массивом, а вы
даваемые выражением значения относятся к типу
File
В качестве другого примера выражение for
, показанное в листинге 7.10, сначала преобразует объект типа
Array[File]
по имени filesHere
, в котором содержатся имена всех файлов, которые есть в текущем каталоге, в объект, содержащий только имена файлов с расширением
.scala
. Для каждого из элементов создается объект типа
Array[String]
, являющийся результатом выполнения метода fileLines
, определение которого показано в листин
ге 7.8. Каждый элемент этого объекта
Array[String]
содержит одну строку из текущего обрабатываемого файла. Данный объект превращается в другой объект типа
Array[String]
, содержащий только те строки, обработанные методом trim
, которые включают подстроку "for"
. И наконец, для каждого из них выдается целочисленное значение длины. Результатом этого выра
жения for становится объект, имеющий тип
Array[Int]
и содержащий эти значения длины.
Листинг 7.10. Преобразование объекта типа Array[File] в объект типа Array[Int] с помощью выражения for val forLineLengths =
for file <- filesHere if file.getName.endsWith(".scala")
7 .4 . Обработка исключений с помощью выражений try 155
line <- fileLines(file)
trimmed = line.trim if trimmed.matches(".*for.*")
yield trimmed.length
Итак, основные свойства выражения for
, применяемого в Scala, рассмотрены, но мы прошлись по ним слишком поверхностно.
7 .4 . Обработка исключений с помощью выражений try
Исключения в Scala ведут себя практически так же, как во многих других языках. Вместо того чтобы возвращать значение (как это обычно происхо
дит), метод может прервать работу с генерацией исключения. Код, вызвав
ший метод, может либо перехватить и обработать данное исключение, либо прекратить собственное выполнение, при этом передав исключение тому коду, который его вызвал. Исключение распространяется подобным образом, раскручивая стек вызова, до тех пор, пока не встретится обрабатывающий его метод или вообще не останется ни одного метода.
Генерация исключений
Генерация исключений в Scala выглядит так же, как в Java. Создается объект исключения, который затем бросается с помощью ключевого слова throw
:
throw new IllegalArgumentException
Как бы парадоксально это ни звучало, в Scala throw
— это выражение, у ко
торого есть результирующий тип. Рассмотрим пример, где этот тип играет важную роль:
def half(n: Int) =
if n % 2 == 0 then n / 2
else throw new RuntimeException("n must be even")
Здесь получается, что если n
— четное число, то переменная half вернет по
ловину от n
. Если нечетное, то исключение сгенерируется еще до того, как переменная half вернет какоелибо значение. Поэтому генерируемое исклю
чение можно без малейших опасений рассматривать как абсолютно любое
156 Глава 7 • Встроенные управляющие конструкции значение. Любой контекст, который пытается воспользоваться значением, возвращаемым из throw
, никогда не сможет этого сделать, и потому никакого вреда ожидать не приходится.
С технической точки зрения сгенерированное исключение имеет тип
Nothing
. Генерацией исключения можно воспользоваться, даже если оно никогда и ни во что не будет вычислено. Подобные технические нюансы могут показаться несколько странными, но в случаях, подобных преды
дущему примеру, зачастую могут пригодиться. Одно ответвление от if вычисляет значение, а другое выдает исключение и вычисляется в
Nothing
Тогда типом всего выражения if является тип, вычисленный в той ветви, в которой проводилось вычисление. Тип
Nothing дополнительно будет рассмотрен в разделе 17.3.
Перехват исключений
Перехват исключений выполняется с применением синтаксиса, показанного в листинге 7.11. Для выражений catch был выбран синтаксис с прицелом на совместимость с весьма важной частью Scala — сопоставлением с образцом.
Этот механизм — весьма эффективное средство, которое вкратце рассматри
вается в данной главе, а более подробно — в главе 13.
1 ... 13 14 15 16 17 18 19 20 ... 64