Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
252 Глава 12 • Пакеты, импорты и экспорты 12 .3 . Импортирование кода В Scala пакеты и их члены могут импортироваться с использованием дирек тивы import . Затем ко всему, что было импортировано, можно получить до ступ, указав простое имя, такое как File , не используя такое развернутое имя, как java.io.File . Рассмотрим, к примеру, код, показанный в листинге 12.7. Листинг 12.7. Превосходные фрукты от Боба, готовые к импорту package bobsdelights abstract class Fruit( val name: String, val color: String ) object Fruits: object Apple extends Fruit("apple", "red") object Orange extends Fruit("orange", "orange") object Pear extends Fruit("pear", "yellowish") val menu = List(Apple, Orange, Pear) Указание директивы import делает члены пакета или объект доступными по их именам, исключая необходимость ставить перед ними префикс с именем пакета или объекта. Рассмотрим ряд простых примеров: // простая форма доступа к Fruit import bobsdelights.Fruit // простая форма доступа ко всем членам bobsdelights import bobsdelights.* // простая форма доступа ко всем членам Fruits import bobsdelights.Fruits.* Первый пример относится к импортированию отдельно взятого Javaтипа, а во втором показан Javaимпорт до востребования (ondemand). Если в Sca la 2 импорты до востребования записывались с замыкающим знаком подчер кивания ( _ ), то в Scala 3 он был заменен знаком звездочки ( * ), чтобы соот вествовать другим языкам. Третья из показанных директив import относится к Javaимпорту статических полей класса. Эти три директивы import дают представление о том, что можно делать с помощью импортирования, но в Scala импортирование носит более уни версальный характер. В частности, оно может быть где угодно, а не только в начале компилируемого модуля. К тому же при импортировании можно 12 .3 . Импортирование кода 253 ссылаться на произвольные значения. Например, возможен импорт, пока занный в листинге 12.8. Листинг 12.8. Импортирование членов обычного объекта (не одиночки) def showFruit(fruit: Fruit) = import fruit.* s"${name}s are $color" Метод showFruit импортирует все члены параметра fruit , относящего ся к типу Fruit . Следующая инструкция println может непосредственно ссылать ся на name и color . Эти две ссылки — эквиваленты ссылок fruit.name и fruit.color . Такой синтаксис пригодится, в частности, при использовании объектов в качестве модулей. Соответствующее описание будет дано в главе 7. Гибкость импортирования в Scala Директива import работает в Scala намного более гибко, чем в Java. Эта гибкость характеризуется тремя принципиальными отличиями. Импорт кода в Scala: • может появляться где угодно; • позволяет, помимо пакетов, ссылаться на объекты (одиночки или обычные); • позволяет изменять имена или скрывать некоторые из импорти рованных членов. Еще один из факторов гибкости импорта кода в Scala заключается в воз можности импортировать пакеты как таковые, без их непакетированного наполнения. Смысл в этом будет только в том случае, если предполагается, что в пакете заключены другие пакеты. Например, в листинге 12.9 импорти руется пакет java.util.regex . Благодаря этому regex можно использовать с указанием его простого имени. Чтобы обратиться к объектуодиночке Pattern из пакета java.util.regex , можно, как показано в данном листинге, просто воспользоваться идентификатором regex.Pattern Листинг 12.9. Импортирование имени пакета import java.util.regex class AStarB: // обращение к java.util.regex.Pattern val pat = regex.Pattern.compile("a*b") 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 |