Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 769
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
254 Глава 12 • Пакеты, импорты и экспорты
При импортировании кода Scala позволяет также переименовывать или скрывать члены. Для этого импорт заключается в фигурные скобки с ука
занием перед получившимся в результате блоком того объекта, из которого импортируются его члены. Рассмотрим несколько примеров:
import Fruits.{Apple, Orange}
Здесь из объекта
Fruits импортируются только его члены
Apple и
Orange import Fruits.{Apple as McIntosh, Orange}
Здесь из объекта
Fruits импортируются два члена,
Apple и
Orange
. Но объ
ект
Apple переименовывается в
McIntosh
, поэтому к нему можно обращаться либо
Fruits.Apple
, либо
McIntosh
. Директива переименования всегда имеет вид
<исходное_имя>
as
<новое_имя>
. Если вам нужно импортировать и пере
именовать только одно имя, фигурные скобки можно не ставить:
import java.sql.Date as SDate
Здесь под именем
SDate импортируется класс данных SQL, чтобы можно было в то же время импортировать обычный класс для работы с датами Java просто как
Date import java.sql as S
Здесь под именем
S
импортируется пакет java.sql
, чтобы можно было вос
пользоваться кодом вида
S.Date import Fruits.{*}
Здесь импортируются все члены объекта
Fruits
. Это означает то же самое, что и import
Fruits.*
import Fruits.{Apple as McIntosh, *}
Здесь импортируются все члены объекта
Fruits
, но
Apple переименовывается в
McIntosh import Fruits.{Pear as _, *}
Здесь импортируются все члены объекта
Fruits
, за исключением
Pear
. Ди
ректива вида
<исходное_имя>
=>
_
исключает
<исходное_имя>
из импортиру
емых имен. В определенном смысле переименование чеголибо в
_
говорит о полном сокрытии переименованного члена. Это помогает избегать неодно
значностей. Предположим, имеется два пакета,
Fruits и
Laptops
, и в каждом
12 .4 . Неявное импортирование 255
из них определен класс
Apple
. Если нужно получить только ноутбук под названием Apple, а не фрукт, то можно воспользоваться двумя импортами по запросу:
import Laptops.*
import Fruits.{Apple as _, *}
Будут импортированы все члены
Laptops и все члены
Fruits
, за исключением
Apple
Эти примеры демонстрируют поразительную гибкость, которую предлагает
Scala в вопросах избирательного импортирования членов, возможно, даже под другими именами. Таким образом, директива import может состоять из следующих селекторов:
z z
простого имени x, которое включается в набор импортируемых имен;
z z
директивы переименования x as
y. Член по имени x будет виден под именем y;
z z
директивы сокрытия x as
_
. Имя x исключается из набора импортируе
мых имен;
z z
элемента «поймать все» (catchall)
*
. Импортируются все члены, за ис
ключением тех, которые были упомянуты в предыдущей директиве. Если указан элемент «поймать все», то в списке селекторов импортирования он должен стоять последним.
Самые простые import
директивы, показанные в начале данного раздела, могут рассматриваться как специальные сокращения директив import с се
лекторами. Например, import p.*
— эквивалент import p.{*}
, а import p.n
— эквивалент import p.{n}
12 .4 . Неявное импортирование
Scala неявно добавляет импортируемый код в каждую программу. По сути, происходит то, что произошло бы при добавлении в самое начало каж
дого исходного файла с расширением
.scala следующих трех директив import
:
import java.lang.* // все из пакета java.lang import scala.* // все из пакета scala import Predef.* // все из объекта Predef
256 Глава 12 • Пакеты, импорты и экспорты
В пакете java.lang содержатся стандартные классы Java. Он всегда не
явно импортируется в исходные файлы Scala
1
. Неявное импортирование java.lang позволяет вам, например, использовать вместо java.lang.Thread просто идентификатор
Thread
Теперь уже вряд ли приходится сомневаться в том, что в пакете scala на
ходится стандартная библиотека Scala, в которой содержатся многие самые востребованные классы и объекты. Поскольку пакет scala испортируется неявно, то можно, к примеру, вместо scala.Int указать просто
Int
В объекте
Predef содержится множество определений псевдонимов типов, методов и преобразований, которые обычно используются в программах на Scala. Например,
Predef импортируется неявно, поэтому можно вместо
Predef.assert задействовать просто идентификатор assert
Эти три директивы import трактуются особым образом, позволяющим тому импорту, который указан позже, перекрывать указанный ранее. К примеру, класс
StringBuilder определен в обоих пакетах scala и java.lang
. Импорт scala перекрывает импорт java.lang
, поэтому простое имя
StringBuilder будет ссылаться на scala.StringBuilder
, а не на java.lang.StringBuilder
12 .5 . Модификаторы доступа
Члены пакетов, классов или объектов могут быть помечены модификаторами доступа private и protected
. Они ограничивают доступ к членам, позволяя обращаться к ним только из определенных областей кода. Трактовка моди
фикаторов доступа Scala примерно соответствует принятой в Java, но при этом имеет ряд весьма важных отличий, которые рассматриваются в данном разделе.
Приватные члены
Приватные члены трактуются в Scala точно так же, как и в Java. Член с по
меткой private виден только внутри класса или объекта, в котором со
держится его определение. В Scala это правило распространяется и на вну
тренние классы. Данная трактовка более последовательна, но отличается от принятой в Java. Рассмотрим пример, показанный в листинге 12.10.
1
Изначально имелась также реализация Scala на платформе .NET, где вместо этого импортировалось пространство имен System, .NETаналог пакета java.lang.
12 .5 . Модификаторы доступа 257
Листинг 12.10. Отличие приватного доступа в Scala от такого же доступа в Java class Outer:
class Inner:
private def f = "f"
class InnerMost:
f // OK
(new Inner).f // ошибка: нет доступа к f
В Scala обращение
(new
Inner).f недопустимо, поскольку приватное объяв
ление f
сделано в классе
Inner
, а попытка обращения делается не из данного класса. В отличие от этого первое обращение к f
в классе
InnerMost вполне допустимо, поскольку содержится в теле класса
Inner
. В Java допустимы оба обращения, так как в данном языке разрешается обращение из внешнего класса к приватным членам его внутренних классов.
Защищенные члены
Доступ к защищенным членам в Scala также менее свободен, чем в Java.
В Scala обратиться к защищенному члену можно только из подклассов того класса, в котором был определен этот член. В Java обращение возможно и из других классов того же самого пакета. В Scala есть еще один способ достиже
ния того же самого эффекта
1
, поэтому модификатор protected можно оста
вить без изменений. Защищенные виды доступа показаны в листинге 12.11.
Листинг 12.11. Отличие защищенного доступа в Scala от такого же доступа в Java package p:
class Super:
protected def f = "f"
class Sub extends Super:
f class Other:
(new Super).f // ошибка: нет доступа к f
В листинге 12.11 обращение к f
в классе
Sub вполне допустимо, поскольку объявление f
было сделано с модификатором protected в
Super
, а
Sub
—
1
Используя спецификаторы, рассматриваемые ниже, в подразделе «Область защиты».
258 Глава 12 • Пакеты, импорты и экспорты подкласс
Super
. В отличие от этого обращение к f
в
Other недопустимо, по
скольку
Other не является наследником
Super
. В Java последнее обращение все равно будет разрешено, так как
Other находится в том же самом пакете, что и
Super
Публичные члены
В Scala нет явного модификатора для публичных членов: любой член, не помеченный как private или protected
, является публичным. К публичным членам можно обращаться откуда угодно.
Область защиты
Модификаторы доступа в Scala могут дополняться спецификаторами. Мо
дификатор вида private[X]
или protected[X]
означает, что доступ закрыт или защищен вплоть до
X
, где
X
определяет некий внешний пакет, класс или объектодиночку.
Специфицированные модификаторы доступа дают возможность весьма чет
ко обозначить границы управления видимостью. В частности, они позволяют выразить понятия доступности, имеющиеся в Java, такие как приватность пакета, защищенность пакета или закрытость вплоть до самого внешнего класса, которые невозможно выразить напрямую с помощью простых моди
фикаторов, используемых в Scala. Но помимо этого, они позволяют выразить правила доступности, которые не могут быть выражены в Java.
В листинге 12.12 представлен пример с использованием множества специ
фикаторов доступа. Здесь класс
Navigator помечен как private[bobsrockets]
Это значит, он имеет область видимости, охватывающую все классы и объ
екты, которые содержатся в пакете bobsrockets
. В частности, доступ к
Navigator разрешен в объекте
Vehicle
, поскольку
Vehicle содержится в пакете launch
, который, в свою очередь, содержится в пакете bobsrockets
В то же время весь код, находящийся за пределами пакета bobsrockets
, не может получить доступ к классу
Navigator
Листинг 12.12. Придание гибкости областям защиты с помощью спецификаторов доступа package bobsrockets package navigation:
private[bobsrockets] class Navigator:
12 .5 . Модификаторы доступа 259
protected[navigation] def useStarChart() = {}
class LegOfJourney:
private[Navigator] val distance = 100
package launch:
import navigation.*
object Vehicle:
private[launch] val guide = new Navigator
Этот прием особенно полезен при разработке крупных проектов, содер
жащих несколько пакетов. Он позволяет определять элементы, видимость которых распространяется на несколько подчиненных пакетов проекта, оставляя их невидимыми для клиентов, являющихся внешними по отноше
нию к данному проекту
1
Разумеется, действие спецификатора private может распространяться и на непосредственно окружающий пакет. В листинге 12.12 показан пример мо
дификатора доступа guide в объекте
Vehicle
. Такой модификатор доступа эквивалентен имеющемуся в Java доступу, ограниченному пределами одного пакета.
Все спецификаторы также могут применяться к модификатору protected со значениями, аналогичными тем, с которыми они применяются к моди
фикатору private
. То есть модификатор protected[X]
в классе
C
позволяет получить доступ к определению с подобной пометкой во всех подклассах
C
, а также во внешнем пакете, классе или объекте с названием
X
. Например, метод useStarChart в приведенном выше листинге 12.12 доступен из всех подклассов
Navigator
, а также из всего кода, содержащегося во внешнем пакете navigation
. В результате получается точное соответствие значению модификатора protected в Java.
Спецификаторы модификатора private могут также ссылаться на окру
жающий (внешний) класс или объект. Например, показанная в ли
стинге 12.12 переменная distance в классе
LegOfJourney имеет помет
ку private[Navigator]
, следовательно, видима из любого места в классе
Navigator
. Тем самым ей придаются такие же возможности видимости, как и приватным членам внутренних классов в Java. Модификатор private[C]
, где
C
— самый внешний класс, аналогичен простому модификатору private в Java.
В качестве резюме в табл. 12.1 приведен список действий спецификаторов модификатора private
. В каждой строке показан модификатор private
1
Эта техника возможна в Java благодаря системе модулей, представленной в JDK 9.
260 Глава 12 • Пакеты, импорты и экспорты со специ фикатором и раскрыто его значение при применении в отношении переменной distance
, объявленной в классе
LegOfJourney в листинге 12.12.
Таблица 12.1. Действия спецификаторов private в отношении LegOfJourney .distance
Спецификатор
Действие
Без указания модификатора доступа
Открытый доступ private[bobsrockets]
Доступ в пределах внешнего пакета private[navigation]
Аналог имеющейся в Java видимости в пределах пакета private[Navigator]
Аналог имеющегося в Java модификато
ра private private[LegOfJourney]
Аналог имеющегося в Scala модификато
ра private
Видимость и объекты-компаньоны
В Java статические члены и члены экземпляра принадлежат одному и тому же классу, поэтому модификаторы доступа применяются к ним одинаково.
Вы уже видели, что в Scala статических членов нет, вместо них может быть объекткомпаньон, содержащий члены, существующие в единственном экземпляре. Например, в листинге 12.13 объект
Rocket
— компаньон класса
Rocket
Листинг 12.13. Обращение к приватным членам класса- и объекта-компаньона class Rocket:
import Rocket.fuel private def canGoHomeAgain = fuel > 20
object Rocket:
private def fuel = 10
def chooseStrategy(rocket: Rocket) =
if rocket.canGoHomeAgain then goHome()
else pickAStar()
def goHome() = {}
def pickAStar() = {}
Что касается приватного или защищенного доступа, то в правилах доступа, действующих в Scala, объектам и классамкомпаньонам даются особые
12 .6 . Определения верхнего уровня 261
привилегии. Класс делится всеми своими правами доступа со своим объ
ектомкомпаньоном, и наоборот. В частности, объект может обращаться ко всем приватным членам своего классакомпаньона точно так же, как класс может обращаться ко всем приватным членам своего объектаком
паньона.
Например, в показанном выше листинге 12.13 класс
Rocket может обращать
ся к методу fuel
, который объявлен приватным в объекте
Rocket
. Аналогично этому объект
Rocket может обращаться к приватному методу canGoHomeAgain в классе
Rocket
Одно из исключений, которое нарушает аналогию между Scala и Java, ка
сается защищенных статических членов. Защищенный статический член
Javaкласса
C
может быть доступен во всех подклассах
C
. В отличие от этого в наличии защищенного члена в объектекомпаньоне нет никакого смысла, поскольку у объектоводиночек нет никаких подклассов.
12 .6 . Определения верхнего уровня
До сих пор единственным встречающимся вам кодом, добавляемым к паке
там, были классы, трейты и одиночные объекты. Они, несомненно, являются наиболее распространенными определениями, помещаемыми на самом верх
нем уровне пакета. Но Scala не ограничивает вас только этим перечнем — лю
бые виды определений, которые можно помещать внутри класса, могут при
сутствовать и на верхнем уровне пакета. На самый верхний уровень пакета можно смело помещать любой вспомогательный метод, который хотелось бы иметь в области видимости всего пакета.
Для этого поместите определение в пакет, как вы бы сделали в случае с классом, чертой или объектом. Пример показан в листинге 12.14. Файл
ShowFruit.scala объявляет вспомогательный метод showFruit из листин
га 12.8 как участник пакета bobsdelights
1 ... 24 25 26 27 28 29 30 31 ... 64
Листинг 12.14. Объект пакета
// в файле ShowFruit.scala package bobsdelights def showFruit(fruit: Fruit) =
import fruit.*
s"${name}s are $color"
// в файле PrintMenu.scala package printmenu
262 Глава 12 • Пакеты, импорты и экспорты import bobsdelights.Fruits import bobsdelights.showFruit object PrintMenu:
def main(args: Array[String]) =
println(
for fruit <- Fruits.menu yield showFruit(fruit)
)
При наличии этого определения любой другой код в любом пакете может импортировать метод точно так же, как класс. Например, в данном листинге показан самостоятельный объект
PrintMenu
, который находится в другом пакете.
PrintMenu может импортировать вспомогательный метод showFruit так же, как и класс
Fruit
Забегая вперед, следует отметить, что есть и другие способы использова
ния определений верхнего уровня, которые еще не были вам показаны.
Эти определения часто применяются для содержания псевдонимов типов, предназначенных для всего пакета (см. главу 20) и методов расширения
(см. главу 22). Пакет scala включает определения верхнего уровня, которые доступны всему коду Scala.
12 .7 . Экспорты
В разделе 10.11 мы рекомендовали предпочитать композицию, а не наследо
вание, особенно если вашей основной целью является повторное использо
вание кода. Это применение принципа наименьшей мощности: композиция рассматривает компоненты как «черные ящики», а наследование путем переопределения влияет на их внутреннюю работу. Иногда тесная связь, подразумеваемая наследованием, является лучшим решением проблемы, но там, где в этом нет необходимости, лучше использовать более слабую связь композиции.
В большинстве популярных объектноориентированных языков програм
мирования проще использовать наследование. Например, в Scala 2 для него требовалось только предложение extends
, в то время как композиция требо
вала подробного описания последовательности серверов пересылки. Таким образом, подавляющая часть объектноориентированных языков подтал
кивала программистов к решению, которое зачастую оказывается слишком мощным. Экспорты в Scala 3 направлены на устранение этого дисбаланса.
Эта новая функция помогает выразить отношения композиции так же крат
12 .7 . Экспорты 263
ко и просто, как и отношения наследования. Она обеспечивает большую гибкость, чем директива extends
, поскольку в ней можно переименовывать или исключать элементы.
В качестве примера представьте, что вы хотите создать тип для представле
ния целых положительных чисел. Вы могли бы определить его следующим образом
1
:
case class PosInt(value: Int):
require(value > 0)
Этот класс позволяет указать в типе, что целое число является положитель
ным. Однако для выполнения любых арифметических операций с
Int в этом коде вам потребуется получить доступ к значению value
:
val x = PosInt(88)
x.value + 1 // 89
Вы можете сделать это удобнее, реализовав метод
+
на
PosInt
, который де
легируется методу
+
базового значения value
, например, так:
case class PosInt(value: Int):
require(value > 0)
def +(x: Int): Int = value + x
С добавлением этого метода пересылки теперь можно выполнять сложение целых чисел в
PosInts без необходимости доступа к значению value
:
val x = PosInt(77)
x + 1 // 78
Вы могли бы сделать
PosInt еще более удобным, реализовав все методы
Int
, но их более ста. Если бы вы могли определить
PosInt как подкласс
Int
, вы бы унаследовали все эти методы и вам бы не понадобилась их повторная реализация. Но поскольку
Int является окончательным, вы не можете этого сделать. Вот почему класс
PosInt должен использовать композицию и деле
гирование вместо наследования.
В Scala 3 вы можете использовать ключевое слово export для указания нуж
ных вам методов пересылки, и компилятор сгенерирует их для вас. Вот как можно создать класс
PosInt
, который объявляет методы пересылки к соот
ветствующим именованным методам базового значения value
:
1
Два альтернативных способа создания этого типа, которые позволяют избежать упаковки, — это
AnyVals и непрозрачные типы.
AnyVals будут рассмотрены в раз
деле 17.4.