layout | title | scala3 | partof | overview-name | type | description | language | num | previous-page | next-page |
---|---|---|---|---|---|---|---|---|---|---|
multipage-overview |
Пакеты и импорт |
true |
scala3-book |
Scala 3 — Book |
chapter |
Обсуждение использования пакетов и импорта для организации кода, создания связанных модулей кода, управления областью действия и предотвращения конфликтов пространств имен. |
ru |
36 |
fun-summary |
collections-intro |
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
Обратите внимание, что за именами пакетов следует двоеточие, а определения внутри пакета имеют отступ.
Преимущество этого подхода заключается в том, что он допускает вложение пакетов и обеспечивает более очевидный контроль над областью видимости и инкапсуляцией, особенно в пределах одного файла.
Операторы импорта используются для доступа к сущностям в других пакетах. Операторы импорта делятся на две основные категории:
- импорт классов, трейтов, объектов, функций и методов
- импорт
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 несколько элементов, их можно перечислить перед использованием постановочного знака:
scala> import java.util.{List as _, Map as _, Set as _, *}
Перечисленные классы скрыты, но можно использовать все остальное в java.util:
scala> new ArrayList[String]
val res0: java.util.ArrayList[String] = []
Поскольку эти Java классы скрыты, можно использовать классы Scala List
, Set
и Map
без конфликта имен:
scala> val a = List(1, 2, 3)
val a: List[Int] = List(1, 2, 3)
scala> val b = Set(1, 2, 3)
val b: Set[Int] = Set(1, 2, 3)
scala> val c = Map(1 -> 1, 2 -> 2)
val c: Map[Int, Int] = Map(1 -> 1, 2 -> 2)
В Scala операторы import
могут быть объявлены где угодно.
Их можно использовать в верхней части файла исходного кода:
package foo
import scala.util.Random
class ClassA:
def printRandom(): Unit =
val r = new Random // класс Random здесь доступен
// ещё код...
Также операторы import
можно использовать ближе к тому месту, где они необходимы:
package foo
class ClassA:
import scala.util.Random // внутри ClassA
def printRandom(): Unit =
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) // 0.0
val b = cos(PI) // -1.0
Два пакета неявно импортируются во все файлы исходного кода:
java.lang.*
scala.*
Члены object Predef
также импортируются по умолчанию.
Например, такие классы, как
List
,Vector
,Map
и т.д. можно использовать явно, не импортируя их - они доступны, потому что определены в objectPredef
Если необходимо импортировать что-то из корня проекта и возникает конфликт имен,
достаточно просто добавить к имени пакета префикс _root_
:
package accounts
import _root_.accounts.*
Как будет показано в главе [“Контекстные абстракции”][contextual],
для импорта экземпляров given
используется специальная форма оператора import
.
Базовая форма показана в этом примере:
object A:
class TC
given tc: TC
def f(using TC) = ???
object B:
import A.* // импорт всех non-given членов
import A.given // импорт экземпляров given
В этом коде предложение import A.*
объекта B
импортирует все элементы A
, кроме given
экземпляра tc
.
И наоборот, второй импорт, import A.given
, импортирует только given
экземпляр.
Два предложения импорта также могут быть объединены в одно:
object B:
import A.{given, *}
Селектор с подстановочным знаком *
помещает в область видимости все определения, кроме given
,
тогда как селектор выше помещает в область действия все данные, включая те, которые являются результатом расширений.
Эти правила имеют два основных преимущества:
- более понятно, откуда берутся данные given. В частности, невозможно скрыть импортированные given в длинном списке других импортируемых подстановочных знаков.
- есть возможность импортировать все given, не импортируя ничего другого. Это особенно важно, поскольку given могут быть анонимными, поэтому обычное использование именованного импорта нецелесообразно.
Поскольку given-ы могут быть анонимными, не всегда практично импортировать их по имени, и вместо этого обычно используется импорт подстановочных знаков. Импорт по типу предоставляет собой более конкретную альтернативу импорту с подстановочными знаками, делая понятным то, что импортируется.
import A.{given TC}
Этот код импортирует из A
любой given
тип, соответствующий TC
.
Импорт данных нескольких типов T1,...,Tn
выражается несколькими given
селекторами:
import A.{given T1, ..., given Tn}
Импорт всех given
экземпляров параметризованного типа достигается аргументами с подстановочными знаками.
Например, есть такой объект
:
object Instances:
given intOrd: Ordering[Int]
given listOrd[T: Ordering]: Ordering[List[T]]
given ec: ExecutionContext = ...
given im: 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"
// остальные случаи здесь ...
given stringMonthConverter: MonthConverter[String] with
def convert(s: String): String =
s match
case "jan" => "January"
case "feb" => "February"
// остальные случаи здесь ...
Чтобы импортировать эти given-ы в текущую область, используем два оператора import
:
import MonthConversions.*
import MonthConversions.{given MonthConverter[?]}
Теперь создаем метод, использующий эти экземпляры:
def genericMonthConverter[A](a: A)(using monthConverter: MonthConverter[A]): String =
monthConverter.convert(a)
Вызов метода:
@main def main =
println(genericMonthConverter(1)) // January
println(genericMonthConverter("jan")) // January
Как уже упоминалось ранее, одно из ключевых преимуществ синтаксиса “import given” состоит в том,
чтобы прояснить, откуда берутся данные в области действия,
и в import
операторах выше ясно, что данные поступают из объекта MonthConversions
.
[contextual]: {% link _overviews/scala3-book/ca-contextual-abstractions-intro.md %}