Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 743
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 7.11. Применение в Scala конструкции try-catch import java.io.FileReader import java.io.FileNotFoundException import java.io.IOException try val f = new FileReader("input.txt")
// использование и закрытие файла catch case ex: FileNotFoundException => // обработка ошибки отсутствия файла case ex: IOException => // обработка других ошибок ввода-вывода
Поведение данного выражения try-catch ничем не отличается от его пове
дения в других языках, использующих исключения. Если при выполнении тела генерируется исключение, то по очереди предпринимается попытка выполнить каждый вариант case
. Если в данном примере исключение имеет тип
FileNotFoundException
, то будет выполнено первое условие, если тип
IOException
— то второе. Если исключение не относится ни к одному из этих типов, то выражение try-catch прервет свое выполнение и исключение будет распространено далее.
7 .4 . Обработка исключений с помощью выражений try 157
ПРИМЕЧАНИЕ
Одно из отличий Scala от Java, которое довольно просто заметить, заклю- чается в том, что язык Scala не требует от вас перехватывать проверяемые исключения или их объявления в условии генерации исключений . При необходимости условие генерации исключений можно объявить с помо- щью аннотации @throws, но делать это не обязательно . Дополнительную информацию о @throws можно найти в разделе 9 .2 .
Условие finally
Если нужно, чтобы некий код выполнялся независимо от того, как именно завершилось выполнение выражения, то можно воспользоваться условием finally
, заключив в него этот код. Например, может понадобиться гаранти
рованное закрытие открытого файла, даже если выход из метода произошел с генерацией исключения. Пример показан в листинге 7.12 1
Листинг 7.12. Применение в Scala условия try-finally import java.io.FileReader val file = new FileReader("input.txt")
try println(file.read()) // использование файла finally file.close() // гарантированное закрытие файла
ПРИМЕЧАНИЕ
В листинге 7 .12 показан характерный для языка способ гарантированного закрытия ресурса, не имеющего отношения к оперативной памяти, напри- мер файла, сокета или подключения к базе данных . Сначала вы получаете ресурс . Затем запускается на выполнение блок try, в котором используется этот ресурс . И наконец, вы закрываете ресурс в блоке finally . В качестве альтернативного варианта достичь той же цели более лаконичным способом в Scala можно с помощью технологии под названием «шаблон временного пользования» (loan pattern) . Он будет рассмотрен в разделе 9 .4 .
Выдача значения
Как и большинство других управляющих конструкций Scala, try-catch- finally выдает значение. Например, в листинге 7.13 показано, как можно
1
Хотя инструкции case оператора catch всегда нужно окружать фигурными скобка
ми или делать отступы в блоке, try и finally не требуют использования фигурных скобок, если в них содержится только одно выражение. Например, можно написать: try t()
catch
{
case e:
Exception
=>
}
finally f()
158 Глава 7 • Встроенные управляющие конструкции попытаться разобрать URL, но при этом воспользоваться значением по умол
чанию в случае плохого формирования URL. Результат получается при выполнении условия try
, если не генерируется исключение, или же при выполнении связанного с ним условия catch
, если исключение генерируется и перехватывается. Значение, вычисленное в условии finally
, при наличии такового, отбрасывается. Как правило, условия finally выполняют какую
либо подчистку, например закрытие файла. Обычно они не должны изменять значение, вычисленное в основном теле или в catch
условии, связанном с try
Листинг 7.13. Условие catch, выдающее значение import java.net.URL
import java.net.MalformedURLException def urlFor(path: String) =
try new URL(path)
catch case e: MalformedURLException =>
new URL("http://www.scala-lang.org")
Если вы знакомы с Java, то стоит отметить, что поведение Scala отличается от поведения Java только тем, что используемая в Java конструкция try-finally не возвращает в результате никакое значение. Как и в Java, если в условие finally включена в явном виде инструкция возвращения значения return или же в нем генерируется исключение, то это возвращаемое значение или исключение будут перевешивать все ранее выданное try или одним из его условий catch
. Например, если взять вот такое несколько надуманное опре
деление функции:
def f(): Int = try return 1 finally return 2
то при вызове f()
будет получен результат
2
. Для сравнения, если взять определение def g(): Int = try 1 finally 2
то при вызове g()
будет получен результат
1
. Обе функции демонстрируют поведение, которое может удивить большинство программистов, поэтому все же лучше обойтись без значений, возвращаемых из условий finally
. Условие finally более предпочтительно считать способом, который гарантирует выпол
нение какоголибо побочного эффекта, например закрытие открытого файла.
7 .5 . Выражения match
Используемое в Scala выражение сопоставления match позволяет выбрать из нескольких альтернатив (вариантов), как это делается в других языках с по
7 .5 . Выражения match 159
мощью инструкции switch
. В общем, выражение match позволяет задейство
вать произвольные шаблоны, которые будут рассмотрены в главе 13. Общая форма может подождать. А пока нужно просто рассматривать использование match для выбора среди ряда альтернатив.
В качестве примера скрипт, показанный в листинге 7.14, считывает из списка аргументов название пищевого продукта и выводит пару к нему. Это выраже
ние match анализирует значение переменной firstArg
, которое установлено на первый аргумент, извлеченный из списка аргументов. Если это строковое значение "salt"
(соль), то оно выводит "pepper"
(перец), а если это "chips"
(чипсы), то "salsa"
(острый соус) и т. д. Вариант по умолчанию указывается с помощью знака подчеркивания (
_
), который является подстановочным символом, часто используемым в Scala в качестве заместителя для неиз
вестного значения.
Листинг 7.14. Выражение сопоставления с побочными эффектами val firstArg = if !args.isEmpty then args(0) else ""
firstArg match case "salt" => println("pepper")
case "chips" => println("salsa")
case "eggs" => println("bacon")
case _ => println("huh?")
Есть несколько важных отличий от используемой в Java инструкции switch
. Одно из них заключается в том, что в case
инструкциях Scala наря
ду с прочим могут применяться любые разновидности констант, а не только константы целочисленного типа, перечисления или строковые константы, как в case
инструкциях Java. В представленном выше листинге в качестве альтернатив используются строки. Еще одно отличие заключается в том, что в конце каждой альтернативы нет инструкции break
. Она присутствует неявно, и нет «выпадения» (fall through) с одной альтернативы на следу
ющую. Общий случай — без «выпадения» — становится короче, а частых ошибок удается избежать, поскольку программисты теперь не «выпадают» нечаянно.
Но, возможно, наиболее существенным отличием от switch
инструкции является то, что выражения сопоставления дают значение. В предыдущем примере в каждой альтернативе в выражении сопоставления на стандартное устройство выводится значение. Как показано в листинге 7.15, данный ва
риант будет работать так же хорошо и выдавать значение вместо того, чтобы выводить его на устройство. Значение, получаемое из этого выражения сопо
ставления, сохраняется в переменной friend
. Кроме того, что код становится короче (по крайней мере на несколько символов), теперь он выполняет две
160 Глава 7 • Встроенные управляющие конструкции отдельные задачи: сначала выбирает продукт питания, а затем выводит его на устройство.
Листинг 7.15. Выражение сопоставления, выдающее значение val firstArg = if !args.isEmpty then args(0) else ""
val friend =
firstArg match case "salt" => "pepper"
case "chips" => "salsa"
case "eggs" => "bacon"
case _ => "huh?"
println(friend)
7 .6 . Программирование без break и continue
Вероятно, вы заметили, что здесь не упоминались ни break
, ни continue
Из Scala эти инструкции исключены, поскольку плохо сочетаются с функ
циональными литералами, которые описываются в следующей главе. Назна
чение инструкции continue в цикле while понятно, но что она будет означать внутри функционального литерала? Так как в Scala поддерживаются оба сти
ля программирования — и императивный, и функциональный, — в данном случае изза упрощения языка прослеживается небольшой перекос в сторону функционального программирования. Но волноваться не стоит. Существу
ет множество способов писать программы, не прибегая к break и continue
, и если воспользоваться функциональными литералами, то варианты с ними зачастую могут быть короче первоначального кода.
Простейший подход заключается в замене каждой инструкции continue условием if
, а каждой инструкции break
— булевой переменной. Последняя показывает, должен ли продолжаться охватывающий цикл while
. Предпо
ложим, ведется поиск в списке аргументов строки, которая заканчивается на
.scala
, но не начинается с дефиса. В Java можно, отдавая предпочтение ци
клам while
, а также инструкциям break и continue
, написать следующий код:
int i = 0; // Это код Java boolean foundIt = false;
while (i < args.length) {
if (args[i].startsWith("-")) {
i = i + 1;
continue;
}
if (args[i].endsWith(".scala")) {
7 .6 . Программирование без break и continue 161
foundIt = true;
break;
}
i = i + 1;
}
Данный фрагмент на Java можно перекодировать непосредственно в код
Scala. Для этого вместо того, чтобы использовать условие if с последующей инструкцией continue
, можно написать условие if
, охватывающее всю остав
шуюся часть цикла while
. Чтобы избавиться от break
, обычно добавляют булеву переменную, которая указывает на необходимость продолжения, но в данном случае можно задействовать уже существующую переменную foundIt
. При использовании этих двух приемов код приобретает вид, по
казанный в листинге 7.16.
Листинг 7.16. Выполнение цикла без break или continue var i = 0
var foundIt = false while i < args.length && !foundIt do if !args(i).startsWith("-") then if args(i).endsWith(".scala") then foundIt = true else i = i + 1
else i = i + 1
Код Scala, показанный в листинге 7.16, очень похож на первоначальный код
Java. Основные части остались на месте и располагаются в том же порядке.
Используются две переназначаемые переменные и цикл while
. Внутри цикла выполняются проверки того, что i
меньше args.length
, а также наличия "–"
и ".scala"
Если в коде листинга 7.16 нужно избавиться от var
переменных, то можно попробовать применить один из подходов, заключающийся в переписывании цикла в рекурсивную функцию. Можно, к примеру, определить функцию searchFrom
, которая получает на входе целочисленное значение, выполняет поиск с указанной им позиции, а затем возвращает индекс желаемого аргу
мента. При использовании данного приема код приобретет вид, показанный в листинге 7.17.
Листинг 7.17. Рекурсивная альтернатива циклу с применением var-переменных def searchFrom(i: Int): Int =
if i >= args.length then -1
162 Глава 7 • Встроенные управляющие конструкции else if args(i).startsWith("-") then searchFrom(i + 1)
else if args(i).endsWith(".scala") then i else searchFrom(i + 1)
val i = searchFrom(0)
В версии, показанной в данном листинге, в имени функции отображено ее назначение, изложенное в понятной человеку форме, а в качестве замены цикла применяется рекурсия. Каждая инструкция continue заменена рекур
сивным вызовом, в котором в качестве аргумента используется выражение i
+
1
, позволяющее эффективно переходить к следующему целочисленному значению. Многие программисты, привыкшие к рекурсиям, считают этот стиль программирования более наглядным.
ПРИМЕЧАНИЕ
В действительности компилятор Scala не будет выдавать для кода, пока- занного в листинге 7 .17, рекурсивную функцию . Поскольку все рекурсив- ные вызовы находятся в хвостовой позиции, то компилятор создаст код, похожий на цикл while . Каждый рекурсивный вызов будет реализован как возврат к началу функции . Оптимизация хвостовых вызовов рассматрива- ется в разделе 8 .10 .
7 .7 . Область видимости переменных
Теперь, когда стала понятна суть встроенных управляющих конструкций
Scala, мы воспользуемся ими, чтобы объяснить, как в этом языке работает область видимости.
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ
Если вы программировали на Java, то увидите, что правила области види- мости в Scala почти идентичны правилам, которые действуют в Java . Един- ственное различие между языками состоит в том, что в Scala позволяется определять переменные с одинаковыми именами во вложенных областях видимости . Поэтому Java-программистам может быть интересно по крайней мере бегло просмотреть данный раздел .
Область видимости — это область программы в Scala, в пределах которой идентификатор некоторой переменной продолжает быть связанным с этой переменной и возвращать ее значение. Самый распространенный пример определения области видимости — применение отступа. Все, что находится на данном уровне отступа, теряет видимость за его пределами. Рассмотрим в качестве иллюстрации функцию, показанную в листинге 7.18.
7 .7 . Область видимости переменных 163
Листинг 7.18. Область видимости переменных при выводе таблицы умножения def printMultiTable() =
var i = 1
// видима только i while i <= 10 do var j = 1
// видимы i и j while j <= 10 do val prod = (i * j).toString
// видимы i, j и prod var k = prod.length
// видимы i, j, prod и k while k < 4 do print(" ")
k += 1
print(prod)
j += 1
// i и j все еще видимы; prod и k — нет println()
i += 1
// i все еще видима; j, prod и k — нет
Показанная здесь функция printMultiTable выводит таблицу умножения
1
В первой инструкции этой функции вводится переменная i
, которая ини
циализируется целым числом 1. Затем имя i
можно использовать во всей остальной части функции.
Следующая инструкция в printMultiTable является циклом while
:
while i <= 10 do var j = 1
Переменная i
может использоваться здесь, поскольку попрежнему находит
ся в области видимости. В первой инструкции внутри цикла while вводится
1
Функция printMultiTable
, показанная в листинге 7.18, написана в императивном стиле. В следующем разделе мы преобразуем его в функциональный стиль.
164 Глава 7 • Встроенные управляющие конструкции еще одна переменная, которой дается имя j
, и она также инициализируется значением
1
. Так как переменная j
была определена внутри отступов цикла while
, она может использоваться только внутри данного цикла while
. При попытке чтолибо сделать с j
в конце цикла while после комментария, со
общающего, что j
, prod и k
уже вне области видимости, ваша программа не будет скомпилирована.
Все переменные, определенные в этом примере: i
, j
, prod и k
— локальные.
Они локальны по отношению к функциям, в которых определены. При каждом вызове функции используется новый набор локальных переменных.
После определения переменной определить новую переменную с таким же именем в той же области видимости уже нельзя. Например, следующий скрипт с двумя переменными по имени a
в одной и той же области видимости скомпилирован не будет:
val a = 1
val a = 2 // Не скомпилируется println(a)
В то же время во внешней области видимости вполне возможно определить переменную с точно таким же именем, что и во внутренней области види
мости. Следующий скрипт будет скомпилирован и сможет быть запущен:
val a = 1;
if a == 1 then val a = 2 // Компилируется без проблем println(a)
println(a)
Данный скрипт при выполнении выведет
2
, а затем
1
, поскольку перемен
ная a
, определенная внутри выражения if
, — это уже другая переменная, область видимости которой распространяется только до конца блока с от
ступами
1
. Следует отметить одно различие между Scala и Java. Оно состо
ит в том, что Java не позволит создать во внутренней области видимости переменную, имя которой совпадает с именем переменной во внешней области видимости. В программе на Scala внутренняя переменная, как говорят, перекрывает внешнюю переменную с точно таким же именем, поскольку внешняя переменная становится невидимой во внутренней об
ласти видимости.
1
Кстати, в данном случае после первого определения нужно поставить точку с за
пятой, поскольку в противном случае действующий в Scala механизм, который подразумевает их использование, не сработает.
7 .8 . Рефакторинг кода, написанного в императивном стиле 165
Вы уже, вероятно, замечали чтолибо подобное эффекту перекрытия в REPL:
scala> val a = 1
a: Int = 1
scala> val a = 2
a: Int = 2
scala> println(a)
2
Там имена переменных можно использовать повторно как вам угодно. Среди прочего это позволяет вам передумать, если при первом определении пере
менной в REPL была допущена ошибка. Подобная возможность появляется благодаря тому, что REPL концептуально создает для каждой введенной вами инструкции новую вложенную область видимости.
Следует помнить: отслеживание может сильно запутать читателей, посколь
ку имена переменных приобретают во вложенных областях видимости со
вершенно новый смысл. Зачастую вместо того, чтобы перекрывать внешнюю переменную, лучше выбрать для переменной новое узнаваемое имя.
7 .8 . Рефакторинг кода, написанного в императивном стиле
Чтобы помочь вам вникнуть в функциональный стиль, в данном разделе мы проведем рефакторинг императивного подхода к выводу таблицы умно
жения, показанной в листинге 7.18. Наша функциональная альтернатива представлена в листинге 7.19.
// использование и закрытие файла catch case ex: FileNotFoundException => // обработка ошибки отсутствия файла case ex: IOException => // обработка других ошибок ввода-вывода
Поведение данного выражения try-catch ничем не отличается от его пове
дения в других языках, использующих исключения. Если при выполнении тела генерируется исключение, то по очереди предпринимается попытка выполнить каждый вариант case
. Если в данном примере исключение имеет тип
FileNotFoundException
, то будет выполнено первое условие, если тип
IOException
— то второе. Если исключение не относится ни к одному из этих типов, то выражение try-catch прервет свое выполнение и исключение будет распространено далее.
7 .4 . Обработка исключений с помощью выражений try 157
ПРИМЕЧАНИЕ
Одно из отличий Scala от Java, которое довольно просто заметить, заклю- чается в том, что язык Scala не требует от вас перехватывать проверяемые исключения или их объявления в условии генерации исключений . При необходимости условие генерации исключений можно объявить с помо- щью аннотации @throws, но делать это не обязательно . Дополнительную информацию о @throws можно найти в разделе 9 .2 .
Условие finally
Если нужно, чтобы некий код выполнялся независимо от того, как именно завершилось выполнение выражения, то можно воспользоваться условием finally
, заключив в него этот код. Например, может понадобиться гаранти
рованное закрытие открытого файла, даже если выход из метода произошел с генерацией исключения. Пример показан в листинге 7.12 1
Листинг 7.12. Применение в Scala условия try-finally import java.io.FileReader val file = new FileReader("input.txt")
try println(file.read()) // использование файла finally file.close() // гарантированное закрытие файла
ПРИМЕЧАНИЕ
В листинге 7 .12 показан характерный для языка способ гарантированного закрытия ресурса, не имеющего отношения к оперативной памяти, напри- мер файла, сокета или подключения к базе данных . Сначала вы получаете ресурс . Затем запускается на выполнение блок try, в котором используется этот ресурс . И наконец, вы закрываете ресурс в блоке finally . В качестве альтернативного варианта достичь той же цели более лаконичным способом в Scala можно с помощью технологии под названием «шаблон временного пользования» (loan pattern) . Он будет рассмотрен в разделе 9 .4 .
Выдача значения
Как и большинство других управляющих конструкций Scala, try-catch- finally выдает значение. Например, в листинге 7.13 показано, как можно
1
Хотя инструкции case оператора catch всегда нужно окружать фигурными скобка
ми или делать отступы в блоке, try и finally не требуют использования фигурных скобок, если в них содержится только одно выражение. Например, можно написать: try t()
catch
{
case e:
Exception
=>
}
finally f()
158 Глава 7 • Встроенные управляющие конструкции попытаться разобрать URL, но при этом воспользоваться значением по умол
чанию в случае плохого формирования URL. Результат получается при выполнении условия try
, если не генерируется исключение, или же при выполнении связанного с ним условия catch
, если исключение генерируется и перехватывается. Значение, вычисленное в условии finally
, при наличии такового, отбрасывается. Как правило, условия finally выполняют какую
либо подчистку, например закрытие файла. Обычно они не должны изменять значение, вычисленное в основном теле или в catch
условии, связанном с try
Листинг 7.13. Условие catch, выдающее значение import java.net.URL
import java.net.MalformedURLException def urlFor(path: String) =
try new URL(path)
catch case e: MalformedURLException =>
new URL("http://www.scala-lang.org")
Если вы знакомы с Java, то стоит отметить, что поведение Scala отличается от поведения Java только тем, что используемая в Java конструкция try-finally не возвращает в результате никакое значение. Как и в Java, если в условие finally включена в явном виде инструкция возвращения значения return или же в нем генерируется исключение, то это возвращаемое значение или исключение будут перевешивать все ранее выданное try или одним из его условий catch
. Например, если взять вот такое несколько надуманное опре
деление функции:
def f(): Int = try return 1 finally return 2
то при вызове f()
будет получен результат
2
. Для сравнения, если взять определение def g(): Int = try 1 finally 2
то при вызове g()
будет получен результат
1
. Обе функции демонстрируют поведение, которое может удивить большинство программистов, поэтому все же лучше обойтись без значений, возвращаемых из условий finally
. Условие finally более предпочтительно считать способом, который гарантирует выпол
нение какоголибо побочного эффекта, например закрытие открытого файла.
7 .5 . Выражения match
Используемое в Scala выражение сопоставления match позволяет выбрать из нескольких альтернатив (вариантов), как это делается в других языках с по
7 .5 . Выражения match 159
мощью инструкции switch
. В общем, выражение match позволяет задейство
вать произвольные шаблоны, которые будут рассмотрены в главе 13. Общая форма может подождать. А пока нужно просто рассматривать использование match для выбора среди ряда альтернатив.
В качестве примера скрипт, показанный в листинге 7.14, считывает из списка аргументов название пищевого продукта и выводит пару к нему. Это выраже
ние match анализирует значение переменной firstArg
, которое установлено на первый аргумент, извлеченный из списка аргументов. Если это строковое значение "salt"
(соль), то оно выводит "pepper"
(перец), а если это "chips"
(чипсы), то "salsa"
(острый соус) и т. д. Вариант по умолчанию указывается с помощью знака подчеркивания (
_
), который является подстановочным символом, часто используемым в Scala в качестве заместителя для неиз
вестного значения.
Листинг 7.14. Выражение сопоставления с побочными эффектами val firstArg = if !args.isEmpty then args(0) else ""
firstArg match case "salt" => println("pepper")
case "chips" => println("salsa")
case "eggs" => println("bacon")
case _ => println("huh?")
Есть несколько важных отличий от используемой в Java инструкции switch
. Одно из них заключается в том, что в case
инструкциях Scala наря
ду с прочим могут применяться любые разновидности констант, а не только константы целочисленного типа, перечисления или строковые константы, как в case
инструкциях Java. В представленном выше листинге в качестве альтернатив используются строки. Еще одно отличие заключается в том, что в конце каждой альтернативы нет инструкции break
. Она присутствует неявно, и нет «выпадения» (fall through) с одной альтернативы на следу
ющую. Общий случай — без «выпадения» — становится короче, а частых ошибок удается избежать, поскольку программисты теперь не «выпадают» нечаянно.
Но, возможно, наиболее существенным отличием от switch
инструкции является то, что выражения сопоставления дают значение. В предыдущем примере в каждой альтернативе в выражении сопоставления на стандартное устройство выводится значение. Как показано в листинге 7.15, данный ва
риант будет работать так же хорошо и выдавать значение вместо того, чтобы выводить его на устройство. Значение, получаемое из этого выражения сопо
ставления, сохраняется в переменной friend
. Кроме того, что код становится короче (по крайней мере на несколько символов), теперь он выполняет две
160 Глава 7 • Встроенные управляющие конструкции отдельные задачи: сначала выбирает продукт питания, а затем выводит его на устройство.
Листинг 7.15. Выражение сопоставления, выдающее значение val firstArg = if !args.isEmpty then args(0) else ""
val friend =
firstArg match case "salt" => "pepper"
case "chips" => "salsa"
case "eggs" => "bacon"
case _ => "huh?"
println(friend)
7 .6 . Программирование без break и continue
Вероятно, вы заметили, что здесь не упоминались ни break
, ни continue
Из Scala эти инструкции исключены, поскольку плохо сочетаются с функ
циональными литералами, которые описываются в следующей главе. Назна
чение инструкции continue в цикле while понятно, но что она будет означать внутри функционального литерала? Так как в Scala поддерживаются оба сти
ля программирования — и императивный, и функциональный, — в данном случае изза упрощения языка прослеживается небольшой перекос в сторону функционального программирования. Но волноваться не стоит. Существу
ет множество способов писать программы, не прибегая к break и continue
, и если воспользоваться функциональными литералами, то варианты с ними зачастую могут быть короче первоначального кода.
Простейший подход заключается в замене каждой инструкции continue условием if
, а каждой инструкции break
— булевой переменной. Последняя показывает, должен ли продолжаться охватывающий цикл while
. Предпо
ложим, ведется поиск в списке аргументов строки, которая заканчивается на
.scala
, но не начинается с дефиса. В Java можно, отдавая предпочтение ци
клам while
, а также инструкциям break и continue
, написать следующий код:
int i = 0; // Это код Java boolean foundIt = false;
while (i < args.length) {
if (args[i].startsWith("-")) {
i = i + 1;
continue;
}
if (args[i].endsWith(".scala")) {
7 .6 . Программирование без break и continue 161
foundIt = true;
break;
}
i = i + 1;
}
Данный фрагмент на Java можно перекодировать непосредственно в код
Scala. Для этого вместо того, чтобы использовать условие if с последующей инструкцией continue
, можно написать условие if
, охватывающее всю остав
шуюся часть цикла while
. Чтобы избавиться от break
, обычно добавляют булеву переменную, которая указывает на необходимость продолжения, но в данном случае можно задействовать уже существующую переменную foundIt
. При использовании этих двух приемов код приобретает вид, по
казанный в листинге 7.16.
Листинг 7.16. Выполнение цикла без break или continue var i = 0
var foundIt = false while i < args.length && !foundIt do if !args(i).startsWith("-") then if args(i).endsWith(".scala") then foundIt = true else i = i + 1
else i = i + 1
Код Scala, показанный в листинге 7.16, очень похож на первоначальный код
Java. Основные части остались на месте и располагаются в том же порядке.
Используются две переназначаемые переменные и цикл while
. Внутри цикла выполняются проверки того, что i
меньше args.length
, а также наличия "–"
и ".scala"
Если в коде листинга 7.16 нужно избавиться от var
переменных, то можно попробовать применить один из подходов, заключающийся в переписывании цикла в рекурсивную функцию. Можно, к примеру, определить функцию searchFrom
, которая получает на входе целочисленное значение, выполняет поиск с указанной им позиции, а затем возвращает индекс желаемого аргу
мента. При использовании данного приема код приобретет вид, показанный в листинге 7.17.
Листинг 7.17. Рекурсивная альтернатива циклу с применением var-переменных def searchFrom(i: Int): Int =
if i >= args.length then -1
162 Глава 7 • Встроенные управляющие конструкции else if args(i).startsWith("-") then searchFrom(i + 1)
else if args(i).endsWith(".scala") then i else searchFrom(i + 1)
val i = searchFrom(0)
В версии, показанной в данном листинге, в имени функции отображено ее назначение, изложенное в понятной человеку форме, а в качестве замены цикла применяется рекурсия. Каждая инструкция continue заменена рекур
сивным вызовом, в котором в качестве аргумента используется выражение i
+
1
, позволяющее эффективно переходить к следующему целочисленному значению. Многие программисты, привыкшие к рекурсиям, считают этот стиль программирования более наглядным.
ПРИМЕЧАНИЕ
В действительности компилятор Scala не будет выдавать для кода, пока- занного в листинге 7 .17, рекурсивную функцию . Поскольку все рекурсив- ные вызовы находятся в хвостовой позиции, то компилятор создаст код, похожий на цикл while . Каждый рекурсивный вызов будет реализован как возврат к началу функции . Оптимизация хвостовых вызовов рассматрива- ется в разделе 8 .10 .
7 .7 . Область видимости переменных
Теперь, когда стала понятна суть встроенных управляющих конструкций
Scala, мы воспользуемся ими, чтобы объяснить, как в этом языке работает область видимости.
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ
Если вы программировали на Java, то увидите, что правила области види- мости в Scala почти идентичны правилам, которые действуют в Java . Един- ственное различие между языками состоит в том, что в Scala позволяется определять переменные с одинаковыми именами во вложенных областях видимости . Поэтому Java-программистам может быть интересно по крайней мере бегло просмотреть данный раздел .
Область видимости — это область программы в Scala, в пределах которой идентификатор некоторой переменной продолжает быть связанным с этой переменной и возвращать ее значение. Самый распространенный пример определения области видимости — применение отступа. Все, что находится на данном уровне отступа, теряет видимость за его пределами. Рассмотрим в качестве иллюстрации функцию, показанную в листинге 7.18.
7 .7 . Область видимости переменных 163
Листинг 7.18. Область видимости переменных при выводе таблицы умножения def printMultiTable() =
var i = 1
// видима только i while i <= 10 do var j = 1
// видимы i и j while j <= 10 do val prod = (i * j).toString
// видимы i, j и prod var k = prod.length
// видимы i, j, prod и k while k < 4 do print(" ")
k += 1
print(prod)
j += 1
// i и j все еще видимы; prod и k — нет println()
i += 1
// i все еще видима; j, prod и k — нет
Показанная здесь функция printMultiTable выводит таблицу умножения
1
В первой инструкции этой функции вводится переменная i
, которая ини
циализируется целым числом 1. Затем имя i
можно использовать во всей остальной части функции.
Следующая инструкция в printMultiTable является циклом while
:
while i <= 10 do var j = 1
Переменная i
может использоваться здесь, поскольку попрежнему находит
ся в области видимости. В первой инструкции внутри цикла while вводится
1
Функция printMultiTable
, показанная в листинге 7.18, написана в императивном стиле. В следующем разделе мы преобразуем его в функциональный стиль.
164 Глава 7 • Встроенные управляющие конструкции еще одна переменная, которой дается имя j
, и она также инициализируется значением
1
. Так как переменная j
была определена внутри отступов цикла while
, она может использоваться только внутри данного цикла while
. При попытке чтолибо сделать с j
в конце цикла while после комментария, со
общающего, что j
, prod и k
уже вне области видимости, ваша программа не будет скомпилирована.
Все переменные, определенные в этом примере: i
, j
, prod и k
— локальные.
Они локальны по отношению к функциям, в которых определены. При каждом вызове функции используется новый набор локальных переменных.
После определения переменной определить новую переменную с таким же именем в той же области видимости уже нельзя. Например, следующий скрипт с двумя переменными по имени a
в одной и той же области видимости скомпилирован не будет:
val a = 1
val a = 2 // Не скомпилируется println(a)
В то же время во внешней области видимости вполне возможно определить переменную с точно таким же именем, что и во внутренней области види
мости. Следующий скрипт будет скомпилирован и сможет быть запущен:
val a = 1;
if a == 1 then val a = 2 // Компилируется без проблем println(a)
println(a)
Данный скрипт при выполнении выведет
2
, а затем
1
, поскольку перемен
ная a
, определенная внутри выражения if
, — это уже другая переменная, область видимости которой распространяется только до конца блока с от
ступами
1
. Следует отметить одно различие между Scala и Java. Оно состо
ит в том, что Java не позволит создать во внутренней области видимости переменную, имя которой совпадает с именем переменной во внешней области видимости. В программе на Scala внутренняя переменная, как говорят, перекрывает внешнюю переменную с точно таким же именем, поскольку внешняя переменная становится невидимой во внутренней об
ласти видимости.
1
Кстати, в данном случае после первого определения нужно поставить точку с за
пятой, поскольку в противном случае действующий в Scala механизм, который подразумевает их использование, не сработает.
7 .8 . Рефакторинг кода, написанного в императивном стиле 165
Вы уже, вероятно, замечали чтолибо подобное эффекту перекрытия в REPL:
scala> val a = 1
a: Int = 1
scala> val a = 2
a: Int = 2
scala> println(a)
2
Там имена переменных можно использовать повторно как вам угодно. Среди прочего это позволяет вам передумать, если при первом определении пере
менной в REPL была допущена ошибка. Подобная возможность появляется благодаря тому, что REPL концептуально создает для каждой введенной вами инструкции новую вложенную область видимости.
Следует помнить: отслеживание может сильно запутать читателей, посколь
ку имена переменных приобретают во вложенных областях видимости со
вершенно новый смысл. Зачастую вместо того, чтобы перекрывать внешнюю переменную, лучше выбрать для переменной новое узнаваемое имя.
7 .8 . Рефакторинг кода, написанного в императивном стиле
Чтобы помочь вам вникнуть в функциональный стиль, в данном разделе мы проведем рефакторинг императивного подхода к выводу таблицы умно
жения, показанной в листинге 7.18. Наша функциональная альтернатива представлена в листинге 7.19.
1 ... 14 15 16 17 18 19 20 21 ... 64