Пакеты и импорт
Scala использует packages
для создания пространств имен, которые позволяют модульно разбивать программы.
Scala поддерживает стиль именования пакетов, используемый в Java,
а также нотацию пространства имен "фигурные скобки", используемую такими языками, как C++ и C#.
Подход Scala к импорту похож на Java, но более гибкий. С помощью Scala можно:
- импортировать пакеты, классы, объекты,
trait
-ы и методы - размещать операторы импорта в любом месте
- скрывать и переименовывать участников при импорте
Эти особенности демонстрируются в следующих примерах.
Создание пакета
Пакеты создаются путем объявления одного или нескольких имен пакетов в начале файла Scala.
Например, если ваше доменное имя acme.com
и вы работаете с пакетом model
приложения с именем myapp
,
объявление пакета выглядит следующим образом:
package com.acme.myapp.model
class Person ...
По соглашению все имена пакетов должны быть строчными,
а формальным соглашением об именах является <top-level-domain>.<domain-name>.<project-name>.<module-name>
.
Хотя это и не обязательно, имена пакетов обычно совпадают с именами иерархии каталогов.
Поэтому, если следовать этому соглашению, класс Person
в этом проекте будет найден
в файле MyApp/src/main/scala/com/acme/myapp/model/Person.scala.
Использование нескольких пакетов в одном файле
Показанный выше синтаксис применяется ко всему исходному файлу:
все определения в файле Person.scala
принадлежат пакету com.acme.myapp.model
в соответствии с package
в начале файла.
В качестве альтернативы можно написать package
, которые применяются только к содержащимся в них определениям:
package users:
package administrators: // полное имя пакета - users.administrators
class AdminUser // полное имя файла - users.administrators.AdminUser
package normalusers: // полное имя пакета - users.normalusers
class NormalUser // полное имя файла - users.normalusers.NormalUser
Обратите внимание, что за именами пакетов следует двоеточие, а определения внутри пакета имеют отступ.
Преимущество этого подхода заключается в том, что он допускает вложение пакетов и обеспечивает более очевидный контроль над областью действия и инкапсуляцией, особенно в пределах одного файла.
Операторы импорта
Операторы импорта используются для доступа к сущностям в других пакетах. Операторы импорта делятся на две основные категории:
- импорт классов,
trait
-ов, объектов, функций и методов - импорт
given
предложений
Первая категория операторов импорта аналогична тому, что использует Java, с немного другим синтаксисом, обеспечивающим большую гибкость. Пример:
import users.* // импортируется все из пакета `users`
import users.User // импортируется только класс `User`
import users.{User, UserPreferences} // импортируются только два члена пакета
import users.{UserPreferences as UPrefs} // переименование импортированного члена
Эти примеры предназначены для того, чтобы дать представление о том, как работает первая категория операторов import
.
Более подробно они объясняются в следующих подразделах.
Операторы импорта также используются для импорта given
экземпляров в область видимости.
Они обсуждаются в конце этой главы.
import
не требуется для доступа к членам одного и того же пакета.
Импорт одного или нескольких членов
В Scala импортировать один элемент из пакета можно следующим образом:
import scala.concurrent.Future
несколько:
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.blocking
При импорте нескольких элементов можно импортировать их более лаконично:
import scala.concurrent.{Future, Promise, blocking}
Если необходимо импортировать все из пакета scala.concurrent
, используется такой синтаксис:
import scala.concurrent.*
Переименование элементов при импорте
Иногда необходимо переименовать объекты при их импорте, чтобы избежать конфликтов имен.
Например, если нужно использовать Scala класс List
вместе с java.util.List
,
то можно переименовать java.util.List
при импорте:
import java.util.{List as JavaList}
Теперь имя JavaList
можно использовать для ссылки на класс java.util.List
и использовать List
для ссылки на Scala класс List
.
Также можно переименовывать несколько элементов одновременно, используя следующий синтаксис:
import java.util.{Date as JDate, HashMap as JHashMap, *}
В этой строке кода говорится следующее:
"Переименуйте классы Date
и HashMap
, как показано,
и импортируйте все остальное из пакета java.util
, не переименовывая".
Скрытие членов при импорте
При импорте часть объектов можно скрывать.
Следующий оператор импорта скрывает класс java.util.Random
,
в то время как все остальное в пакете java.util
импортируется:
import java.util.{Random as _, *}
Если попытаться получить доступ к классу Random
, то выдается ошибка,
но есть доступ ко всем остальным членам пакета java.util
:
val r = new Random // не скомпилируется
new ArrayList // доступ есть
Скрытие нескольких элементов
Чтобы скрыть в import
несколько элементов, их можно перечислить перед использованием *
:
import java.util.{List as _, Map as _, Set as _, *}
Перечисленные классы скрыты, но можно использовать все остальное в java.util
:
val arr = new ArrayList[String]
// arr: ArrayList[String] = []
Поскольку эти Java классы скрыты, можно использовать классы Scala List
, Set
и Map
без конфликта имен:
val a = List(1, 2, 3)
// a: List[Int] = List(1, 2, 3)
val b = Set(1, 2, 3)
// b: Set[Int] = Set(1, 2, 3)
val c = Map(1 -> 1, 2 -> 2)
// c: Map[Int, Int] = Map(1 -> 1, 2 -> 2)
Импорт можно использовать в любом месте
В Scala операторы импорта могут быть объявлены где угодно. Их можно использовать в верхней части файла исходного кода:
package foo
import scala.util.Random
class ClassA:
def printRandom:
val r = new Random // класс Random здесь доступен
// ещё код...
Также операторы импорта можно использовать ближе к тому месту, где они необходимы:
package foo
class ClassA:
import scala.util.Random // внутри ClassA
def printRandom {
val r = new Random
// ещё код...
class ClassB:
// класс Random здесь невидим
val r = new Random // этот код не скомпилится
"Статический" импорт
Если необходимо импортировать элементы способом, аналогичным подходу «статического импорта» в Java, то есть для того, чтобы напрямую обращаться к членам класса, не добавляя к ним префикс с именем класса, используется следующий подход.
Синтаксис для импорта всех статических членов Java класса Math
:
import java.lang.Math.*
Теперь можно получить доступ к статическим методам класса Math
, таким как sin
и cos
,
без необходимости предварять их именем класса:
import java.lang.Math.*
val a = sin(0)
// a: Double = 0.0
val b = cos(PI)
// b: Double = -1.0
Пакеты, импортированные по умолчанию
Два пакета неявно импортируются во все файлы исходного кода:
java.lang.*
scala.*
Члены object Predef
также импортируются по умолчанию.
Например, такие классы, какList
,Vector
,Map
и т. д. можно использовать явно, не импортируя их - они доступны, потому что определены в object Predef
Обработка конфликтов имен
Если необходимо импортировать что-то из корня проекта и возникает конфликт имен,
достаточно просто добавить к имени пакета префикс _root_
:
package accounts
import _root_.accounts.*
Импорт given
Как будет показано в главе "Контекстные абстракции",
для импорта экземпляров given
используется специальная форма оператора import
.
Базовая форма показана в этом примере:
object A:
class TC
given tc as TC
def f(using TC) = ???
object B:
import A.* // import all non-given members
import A.given // import the given instance
В этом коде предложение import A.*
объекта B
импортирует все элементы A
, кроме given
экземпляра tc
.
И наоборот, второй импорт, import A.given
, импортирует только given
экземпляр.
Два предложения импорта также могут быть объединены в одно:
object B:
import A.{given, *}
Селектор с подстановочным знаком *
помещает в область видимости все определения, кроме given
,
тогда как селектор выше помещает в область действия все данные, включая те, которые являются результатом расширений.
Эти правила имеют два основных преимущества:
- более понятно, откуда берутся данные
given
. В частности, невозможно скрыть импортированныеgiven
в длинном списке других импортируемых подстановочных знаков. - есть возможность импортировать все
given
, не импортируя ничего другого. Это особенно важно, посколькуgiven
могут быть анонимными, поэтому обычное использование именованного импорта нецелесообразно.
Импорт по типу
Поскольку given
-ы могут быть анонимными, не всегда практично импортировать их по имени,
и вместо этого обычно используется импорт подстановочных знаков.
Импорт по типу предоставляет собой более конкретную альтернативу импорту с подстановочными знаками,
делая понятным то, что импортируется.
Этот код импортирует из A
любой given
тип, соответствующий TC
:
import A.{given TC}
Если импортируется только один given
, то фигурные скобки можно опустить:
import A.given TC
Импорт данных нескольких типов T1,...,Tn
выражается несколькими given
селекторами:
import A.{given T1, ..., given Tn}
Импорт всех given
экземпляров параметризованного типа достигается аргументами с подстановочными знаками.
Например, есть такой объект:
object Instances:
given intOrd as Ordering[Int]
given listOrd[T: Ordering] as Ordering[List[T]]
given ec as ExecutionContext = ...
given im as Monoid[Int]
Оператор import
ниже импортирует экземпляры intOrd
, listOrd
и ec
, но пропускает экземпляр im
,
поскольку он не соответствует ни одному из указанных шаблонов:
import Instances.{given Ordering[?], given ExecutionContext}
Импорт по типу можно смешивать с импортом по имени.
Если оба присутствуют в предложении import
, импорт по типу идет последним.
Например, это предложение импорта импортирует im
, intOrd
и listOrd
, но не включает ec
:
import Instances.{im, given Ordering[?]}
Пример
В качестве конкретного примера представим, что у нас есть объект MonthConversions
,
который содержит два определения given
:
object MonthConversions:
trait MonthConverter[A]:
def convert(a: A): String
given intMonthConverter: MonthConverter[Int] with
def convert(i: Int): String =
i match
case 1 => "January"
case 2 => "February"
case _ => "Other"
given stringMonthConverter: MonthConverter[String] with
def convert(s: String): String =
s match
case "jan" => "January"
case "feb" => "February"
case _ => "Other"
Чтобы импортировать эти given
-ы в текущую область, используем два оператора import
:
import MonthConversions.*
import MonthConversions.given MonthConverter[?]
Теперь создаем метод, использующий эти экземпляры:
def genericMonthConverter[A](a: A)(using monthConverter: MonthConverter[A]): String =
monthConverter.convert(a)
Вызов метода:
genericMonthConverter(1)
// res1: String = "January"
genericMonthConverter("jan")
// res2: String = "January"
Как уже упоминалось ранее, одно из ключевых преимуществ синтаксиса "import given" состоит в том,
чтобы прояснить, откуда берутся данные в области действия,
и в import
операторах выше ясно, что данные поступают из объекта MonthConversions
.
Ссылки: